aboutsummaryrefslogtreecommitdiff
path: root/test/functional/test_framework/wallet.py
diff options
context:
space:
mode:
Diffstat (limited to 'test/functional/test_framework/wallet.py')
-rw-r--r--test/functional/test_framework/wallet.py121
1 files changed, 94 insertions, 27 deletions
diff --git a/test/functional/test_framework/wallet.py b/test/functional/test_framework/wallet.py
index cea59c6d69..9158dd0942 100644
--- a/test/functional/test_framework/wallet.py
+++ b/test/functional/test_framework/wallet.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-# Copyright (c) 2020 The Bitcoin Core developers
+# Copyright (c) 2020-2021 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""A limited-functionality wallet, which may replace a real wallet in tests"""
@@ -9,7 +9,14 @@ from decimal import Decimal
from enum import Enum
from random import choice
from typing import Optional
-from test_framework.address import ADDRESS_BCRT1_P2WSH_OP_TRUE
+from test_framework.address import (
+ base58_to_byte,
+ create_deterministic_address_bcrt1_p2tr_op_true,
+ key_to_p2pkh,
+ key_to_p2sh_p2wpkh,
+ key_to_p2wpkh,
+)
+from test_framework.descriptors import descsum_create
from test_framework.key import ECKey
from test_framework.messages import (
COIN,
@@ -23,15 +30,22 @@ from test_framework.messages import (
from test_framework.script import (
CScript,
LegacySignatureHash,
- OP_CHECKSIG,
- OP_TRUE,
+ LEAF_VERSION_TAPSCRIPT,
OP_NOP,
+ OP_TRUE,
SIGHASH_ALL,
)
+from test_framework.script_util import (
+ key_to_p2pk_script,
+ key_to_p2pkh_script,
+ key_to_p2sh_p2wpkh_script,
+ key_to_p2wpkh_script,
+ keyhash_to_p2pkh_script,
+ scripthash_to_p2sh_script,
+)
from test_framework.util import (
assert_equal,
assert_greater_than_or_equal,
- satoshi_round,
)
DEFAULT_FEE = Decimal("0.0001")
@@ -40,7 +54,7 @@ class MiniWalletMode(Enum):
"""Determines the transaction type the MiniWallet is creating and spending.
For most purposes, the default mode ADDRESS_OP_TRUE should be sufficient;
- it simply uses a fixed bech32 P2WSH address whose coins are spent with a
+ it simply uses a fixed bech32m P2TR address whose coins are spent with a
witness stack of OP_TRUE, i.e. following an anyone-can-spend policy.
However, if the transactions need to be modified by the user (e.g. prepending
scriptSig for testing opcodes that are activated by a soft-fork), or the txs
@@ -50,7 +64,7 @@ class MiniWalletMode(Enum):
| output | | tx is | can modify | needs
mode | description | address | standard | scriptSig | signing
----------------+-------------------+-----------+----------+------------+----------
- ADDRESS_OP_TRUE | anyone-can-spend | bech32 | yes | no | no
+ ADDRESS_OP_TRUE | anyone-can-spend | bech32m | yes | no | no
RAW_OP_TRUE | anyone-can-spend | - (raw) | no | yes | no
RAW_P2PK | pay-to-public-key | - (raw) | yes | yes | yes
"""
@@ -74,9 +88,9 @@ class MiniWallet:
self._priv_key = ECKey()
self._priv_key.set((1).to_bytes(32, 'big'), True)
pub_key = self._priv_key.get_pubkey()
- self._scriptPubKey = bytes(CScript([pub_key.get_bytes(), OP_CHECKSIG]))
+ self._scriptPubKey = key_to_p2pk_script(pub_key.get_bytes())
elif mode == MiniWalletMode.ADDRESS_OP_TRUE:
- self._address = ADDRESS_BCRT1_P2WSH_OP_TRUE
+ self._address, self._internal_key = create_deterministic_address_bcrt1_p2tr_op_true()
self._scriptPubKey = bytes.fromhex(self._test_node.validateaddress(self._address)['scriptPubKey'])
def rescan_utxos(self):
@@ -85,13 +99,13 @@ class MiniWallet:
res = self._test_node.scantxoutset(action="start", scanobjects=[self.get_descriptor()])
assert_equal(True, res['success'])
for utxo in res['unspents']:
- self._utxos.append({'txid': utxo['txid'], 'vout': utxo['vout'], 'value': utxo['amount']})
+ self._utxos.append({'txid': utxo['txid'], 'vout': utxo['vout'], 'value': utxo['amount'], 'height': utxo['height']})
def scan_tx(self, tx):
"""Scan the tx for self._scriptPubKey outputs and add them to self._utxos"""
for out in tx['vout']:
if out['scriptPubKey']['hex'] == self._scriptPubKey.hex():
- self._utxos.append({'txid': tx['txid'], 'vout': out['n'], 'value': out['value']})
+ self._utxos.append({'txid': tx['txid'], 'vout': out['n'], 'value': out['value'], 'height': 0})
def sign_tx(self, tx, fixed_length=True):
"""Sign tx that has been created by MiniWallet in P2PK mode"""
@@ -108,16 +122,17 @@ class MiniWallet:
break
tx.vin[0].scriptSig = CScript([der_sig + bytes(bytearray([SIGHASH_ALL]))])
- def generate(self, num_blocks):
+ def generate(self, num_blocks, **kwargs):
"""Generate blocks with coinbase outputs to the internal address, and append the outputs to the internal list"""
- blocks = self._test_node.generatetodescriptor(num_blocks, self.get_descriptor())
+ blocks = self._test_node.generatetodescriptor(num_blocks, self.get_descriptor(), **kwargs)
for b in blocks:
- cb_tx = self._test_node.getblock(blockhash=b, verbosity=2)['tx'][0]
- self._utxos.append({'txid': cb_tx['txid'], 'vout': 0, 'value': cb_tx['vout'][0]['value']})
+ block_info = self._test_node.getblock(blockhash=b, verbosity=2)
+ cb_tx = block_info['tx'][0]
+ self._utxos.append({'txid': cb_tx['txid'], 'vout': 0, 'value': cb_tx['vout'][0]['value'], 'height': block_info['height']})
return blocks
def get_descriptor(self):
- return self._test_node.getdescriptorinfo(f'raw({self._scriptPubKey.hex()})')['descriptor']
+ return descsum_create(f'raw({self._scriptPubKey.hex()})')
def get_address(self):
return self._address
@@ -128,10 +143,9 @@ class MiniWallet:
Args:
txid: get the first utxo we find from a specific transaction
-
- Note: Can be used to get the change output immediately after a send_self_transfer
"""
index = -1 # by default the last utxo
+ self._utxos = sorted(self._utxos, key=lambda k: (k['value'], -k['height'])) # Put the largest utxo last
if txid:
utxo = next(filter(lambda utxo: txid == utxo['txid'], self._utxos))
index = self._utxos.index(utxo)
@@ -146,21 +160,38 @@ class MiniWallet:
self.sendrawtransaction(from_node=kwargs['from_node'], tx_hex=tx['hex'])
return tx
+ def send_to(self, *, from_node, scriptPubKey, amount, fee=1000):
+ """
+ Create and send a tx with an output to a given scriptPubKey/amount,
+ plus a change output to our internal address. To keep things simple, a
+ fixed fee given in Satoshi is used.
+
+ Note that this method fails if there is no single internal utxo
+ available that can cover the cost for the amount and the fixed fee
+ (the utxo with the largest value is taken).
+
+ Returns a tuple (txid, n) referring to the created external utxo outpoint.
+ """
+ tx = self.create_self_transfer(from_node=from_node, fee_rate=0, mempool_valid=False)['tx']
+ assert_greater_than_or_equal(tx.vout[0].nValue, amount + fee)
+ tx.vout[0].nValue -= (amount + fee) # change output -> MiniWallet
+ tx.vout.append(CTxOut(amount, scriptPubKey)) # arbitrary output -> to be returned
+ txid = self.sendrawtransaction(from_node=from_node, tx_hex=tx.serialize().hex())
+ return txid, 1
+
def create_self_transfer(self, *, fee_rate=Decimal("0.003"), from_node, utxo_to_spend=None, mempool_valid=True, locktime=0, sequence=0):
"""Create and return a tx with the specified fee_rate. Fee may be exact or at most one satoshi higher than needed."""
- self._utxos = sorted(self._utxos, key=lambda k: k['value'])
- utxo_to_spend = utxo_to_spend or self._utxos.pop() # Pick the largest utxo (if none provided) and hope it covers the fee
+ utxo_to_spend = utxo_to_spend or self.get_utxo()
if self._priv_key is None:
- vsize = Decimal(96) # anyone-can-spend
+ vsize = Decimal(104) # anyone-can-spend
else:
vsize = Decimal(168) # P2PK (73 bytes scriptSig + 35 bytes scriptPubKey + 60 bytes other)
- send_value = satoshi_round(utxo_to_spend['value'] - fee_rate * (vsize / 1000))
- fee = utxo_to_spend['value'] - send_value
+ send_value = int(COIN * (utxo_to_spend['value'] - fee_rate * (vsize / 1000)))
assert send_value > 0
tx = CTransaction()
tx.vin = [CTxIn(COutPoint(int(utxo_to_spend['txid'], 16), utxo_to_spend['vout']), nSequence=sequence)]
- tx.vout = [CTxOut(int(send_value * COIN), self._scriptPubKey)]
+ tx.vout = [CTxOut(send_value, self._scriptPubKey)]
tx.nLockTime = locktime
if not self._address:
# raw script
@@ -169,17 +200,17 @@ class MiniWallet:
self.sign_tx(tx)
else:
# anyone-can-spend
- tx.vin[0].scriptSig = CScript([OP_NOP] * 35) # pad to identical size
+ tx.vin[0].scriptSig = CScript([OP_NOP] * 43) # pad to identical size
else:
tx.wit.vtxinwit = [CTxInWitness()]
- tx.wit.vtxinwit[0].scriptWitness.stack = [CScript([OP_TRUE])]
+ tx.wit.vtxinwit[0].scriptWitness.stack = [CScript([OP_TRUE]), bytes([LEAF_VERSION_TAPSCRIPT]) + self._internal_key]
tx_hex = tx.serialize().hex()
tx_info = from_node.testmempoolaccept([tx_hex])[0]
assert_equal(mempool_valid, tx_info['allowed'])
if mempool_valid:
assert_equal(tx_info['vsize'], vsize)
- assert_equal(tx_info['fees']['base'], fee)
+ assert_equal(tx_info['fees']['base'], utxo_to_spend['value'] - Decimal(send_value) / COIN)
return {'txid': tx_info['txid'], 'wtxid': tx_info['wtxid'], 'hex': tx_hex, 'tx': tx}
def sendrawtransaction(self, *, from_node, tx_hex):
@@ -188,6 +219,42 @@ class MiniWallet:
return txid
+def getnewdestination(address_type='bech32'):
+ """Generate a random destination of the specified type and return the
+ corresponding public key, scriptPubKey and address. Supported types are
+ 'legacy', 'p2sh-segwit' and 'bech32'. Can be used when a random
+ destination is needed, but no compiled wallet is available (e.g. as
+ replacement to the getnewaddress/getaddressinfo RPCs)."""
+ key = ECKey()
+ key.generate()
+ pubkey = key.get_pubkey().get_bytes()
+ if address_type == 'legacy':
+ scriptpubkey = key_to_p2pkh_script(pubkey)
+ address = key_to_p2pkh(pubkey)
+ elif address_type == 'p2sh-segwit':
+ scriptpubkey = key_to_p2sh_p2wpkh_script(pubkey)
+ address = key_to_p2sh_p2wpkh(pubkey)
+ elif address_type == 'bech32':
+ scriptpubkey = key_to_p2wpkh_script(pubkey)
+ address = key_to_p2wpkh(pubkey)
+ # TODO: also support bech32m (need to generate x-only-pubkey)
+ else:
+ assert False
+ return pubkey, scriptpubkey, address
+
+
+def address_to_scriptpubkey(address):
+ """Converts a given address to the corresponding output script (scriptPubKey)."""
+ payload, version = base58_to_byte(address)
+ if version == 111: # testnet pubkey hash
+ return keyhash_to_p2pkh_script(payload)
+ elif version == 196: # testnet script hash
+ return scripthash_to_p2sh_script(payload)
+ # TODO: also support other address formats
+ else:
+ assert False
+
+
def make_chain(node, address, privkeys, parent_txid, parent_value, n=0, parent_locking_script=None, fee=DEFAULT_FEE):
"""Build a transaction that spends parent_txid.vout[n] and produces one output with
amount = parent_value with a fee deducted.