class RESTTest(UmkoinTestFramework): def set_test_params(self): self.num_nodes = 2 self.extra_args = [["-rest", "-blockfilterindex=1"], []] # whitelist peers to speed up tx relay / mempool sync for args in self.extra_args: args.append("[email protected]") self.supports_cli = False def test_rest_request(self, uri, http_method='GET', req_type=ReqType.JSON, body='', status=200, ret_type=RetType.JSON): rest_uri = '/rest' + uri if req_type == ReqType.JSON: rest_uri += '.json' elif req_type == ReqType.BIN: rest_uri += '.bin' elif req_type == ReqType.HEX: rest_uri += '.hex' conn = http.client.HTTPConnection(self.url.hostname, self.url.port) self.log.debug(f'{http_method} {rest_uri} {body}') if http_method == 'GET': conn.request('GET', rest_uri) elif http_method == 'POST': conn.request('POST', rest_uri, body) resp = conn.getresponse() assert_equal(resp.status, status) if ret_type == RetType.OBJ: return resp elif ret_type == RetType.BYTES: return resp.read() elif ret_type == RetType.JSON: return json.loads(resp.read().decode('utf-8'), parse_float=Decimal) def run_test(self): self.url = urllib.parse.urlparse(self.nodes[0].url) self.wallet = MiniWallet(self.nodes[0]) self.wallet.rescan_utxos() self.log.info("Broadcast test transaction and sync nodes") txid, _ = self.wallet.send_to(from_node=self.nodes[0], scriptPubKey=getnewdestination()[1], amount=int(0.1 * COIN)) self.sync_all() self.log.info("Test the /tx URI") json_obj = self.test_rest_request(f"/tx/{txid}") assert_equal(json_obj['txid'], txid) # Check hex format response hex_response = self.test_rest_request(f"/tx/{txid}", req_type=ReqType.HEX, ret_type=RetType.OBJ) assert_greater_than_or_equal( int(hex_response.getheader('content-length')), json_obj['size'] * 2) spent = ( json_obj['vin'][0]['txid'], json_obj['vin'][0]['vout'] ) # get the vin to later check for utxo (should be spent by then) # get n of 0.1 outpoint n, = filter_output_indices_by_value(json_obj['vout'], Decimal('0.1')) spending = (txid, n) # Test /tx with an invalid and an unknown txid resp = self.test_rest_request(uri=f"/tx/{INVALID_PARAM}", ret_type=RetType.OBJ, status=400) assert_equal(resp.read().decode('utf-8').rstrip(), f"Invalid hash: {INVALID_PARAM}") resp = self.test_rest_request(uri=f"/tx/{UNKNOWN_PARAM}", ret_type=RetType.OBJ, status=404) assert_equal(resp.read().decode('utf-8').rstrip(), f"{UNKNOWN_PARAM} not found") self.log.info("Query an unspent TXO using the /getutxos URI") self.generate(self.wallet, 1) bb_hash = self.nodes[0].getbestblockhash() # Check chainTip response json_obj = self.test_rest_request( f"/getutxos/{spending[0]}-{spending[1]}") assert_equal(json_obj['chaintipHash'], bb_hash) # Make sure there is one utxo assert_equal(len(json_obj['utxos']), 1) assert_equal(json_obj['utxos'][0]['value'], Decimal('0.1')) self.log.info("Query a spent TXO using the /getutxos URI") json_obj = self.test_rest_request(f"/getutxos/{spent[0]}-{spent[1]}") # Check chainTip response assert_equal(json_obj['chaintipHash'], bb_hash) # Make sure there is no utxo in the response because this outpoint has been spent assert_equal(len(json_obj['utxos']), 0) # Check bitmap assert_equal(json_obj['bitmap'], "0") self.log.info("Query two TXOs using the /getutxos URI") json_obj = self.test_rest_request( f"/getutxos/{spending[0]}-{spending[1]}/{spent[0]}-{spent[1]}") assert_equal(len(json_obj['utxos']), 1) assert_equal(json_obj['bitmap'], "10") self.log.info( "Query the TXOs using the /getutxos URI with a binary response") bin_request = b'\x01\x02' for txid, n in [spending, spent]: bin_request += bytes.fromhex(txid) bin_request += pack("i", n) bin_response = self.test_rest_request("/getutxos", http_method='POST', req_type=ReqType.BIN, body=bin_request, ret_type=RetType.BYTES) output = BytesIO(bin_response) chain_height, = unpack("<i", output.read(4)) response_hash = output.read(32)[::-1].hex() assert_equal( bb_hash, response_hash ) # check if getutxo's chaintip during calculation was fine assert_equal( chain_height, 201 ) # chain height must be 201 (pre-mined chain [200] + generated block [1]) self.log.info("Test the /getutxos URI with and without /checkmempool") # Create a transaction, check that it's found with /checkmempool, but # not found without. Then confirm the transaction and check that it's # found with or without /checkmempool. # do a tx and don't sync txid, _ = self.wallet.send_to(from_node=self.nodes[0], scriptPubKey=getnewdestination()[1], amount=int(0.1 * COIN)) json_obj = self.test_rest_request(f"/tx/{txid}") # get the spent output to later check for utxo (should be spent by then) spent = (json_obj['vin'][0]['txid'], json_obj['vin'][0]['vout']) # get n of 0.1 outpoint n, = filter_output_indices_by_value(json_obj['vout'], Decimal('0.1')) spending = (txid, n) json_obj = self.test_rest_request( f"/getutxos/{spending[0]}-{spending[1]}") assert_equal(len(json_obj['utxos']), 0) json_obj = self.test_rest_request( f"/getutxos/checkmempool/{spending[0]}-{spending[1]}") assert_equal(len(json_obj['utxos']), 1) json_obj = self.test_rest_request(f"/getutxos/{spent[0]}-{spent[1]}") assert_equal(len(json_obj['utxos']), 1) json_obj = self.test_rest_request( f"/getutxos/checkmempool/{spent[0]}-{spent[1]}") assert_equal(len(json_obj['utxos']), 0) self.generate(self.nodes[0], 1) json_obj = self.test_rest_request( f"/getutxos/{spending[0]}-{spending[1]}") assert_equal(len(json_obj['utxos']), 1) json_obj = self.test_rest_request( f"/getutxos/checkmempool/{spending[0]}-{spending[1]}") assert_equal(len(json_obj['utxos']), 1) # Do some invalid requests self.test_rest_request("/getutxos", http_method='POST', req_type=ReqType.JSON, body='{"checkmempool', status=400, ret_type=RetType.OBJ) self.test_rest_request("/getutxos", http_method='POST', req_type=ReqType.BIN, body='{"checkmempool', status=400, ret_type=RetType.OBJ) self.test_rest_request("/getutxos/checkmempool", http_method='POST', req_type=ReqType.JSON, status=400, ret_type=RetType.OBJ) # Test limits long_uri = '/'.join([f"{txid}-{n_}" for n_ in range(20)]) self.test_rest_request(f"/getutxos/checkmempool/{long_uri}", http_method='POST', status=400, ret_type=RetType.OBJ) long_uri = '/'.join([f'{txid}-{n_}' for n_ in range(15)]) self.test_rest_request(f"/getutxos/checkmempool/{long_uri}", http_method='POST', status=200) self.generate(self.nodes[0], 1) # generate block to not affect upcoming tests self.log.info("Test the /block, /blockhashbyheight and /headers URIs") bb_hash = self.nodes[0].getbestblockhash() # Check result if block does not exists assert_equal(self.test_rest_request(f"/headers/1/{UNKNOWN_PARAM}"), []) self.test_rest_request(f"/block/{UNKNOWN_PARAM}", status=404, ret_type=RetType.OBJ) # Check result if block is not in the active chain self.nodes[0].invalidateblock(bb_hash) assert_equal(self.test_rest_request(f'/headers/1/{bb_hash}'), []) self.test_rest_request(f'/block/{bb_hash}') self.nodes[0].reconsiderblock(bb_hash) # Check binary format response = self.test_rest_request(f"/block/{bb_hash}", req_type=ReqType.BIN, ret_type=RetType.OBJ) assert_greater_than(int(response.getheader('content-length')), BLOCK_HEADER_SIZE) response_bytes = response.read() # Compare with block header response_header = self.test_rest_request(f"/headers/1/{bb_hash}", req_type=ReqType.BIN, ret_type=RetType.OBJ) assert_equal(int(response_header.getheader('content-length')), BLOCK_HEADER_SIZE) response_header_bytes = response_header.read() assert_equal(response_bytes[:BLOCK_HEADER_SIZE], response_header_bytes) # Check block hex format response_hex = self.test_rest_request(f"/block/{bb_hash}", req_type=ReqType.HEX, ret_type=RetType.OBJ) assert_greater_than(int(response_hex.getheader('content-length')), BLOCK_HEADER_SIZE * 2) response_hex_bytes = response_hex.read().strip(b'\n') assert_equal(response_bytes.hex().encode(), response_hex_bytes) # Compare with hex block header response_header_hex = self.test_rest_request(f"/headers/1/{bb_hash}", req_type=ReqType.HEX, ret_type=RetType.OBJ) assert_greater_than( int(response_header_hex.getheader('content-length')), BLOCK_HEADER_SIZE * 2) response_header_hex_bytes = response_header_hex.read( BLOCK_HEADER_SIZE * 2) assert_equal(response_bytes[:BLOCK_HEADER_SIZE].hex().encode(), response_header_hex_bytes) # Check json format block_json_obj = self.test_rest_request(f"/block/{bb_hash}") assert_equal(block_json_obj['hash'], bb_hash) assert_equal( self.test_rest_request( f"/blockhashbyheight/{block_json_obj['height']}")['blockhash'], bb_hash) # Check hex/bin format resp_hex = self.test_rest_request( f"/blockhashbyheight/{block_json_obj['height']}", req_type=ReqType.HEX, ret_type=RetType.OBJ) assert_equal(resp_hex.read().decode('utf-8').rstrip(), bb_hash) resp_bytes = self.test_rest_request( f"/blockhashbyheight/{block_json_obj['height']}", req_type=ReqType.BIN, ret_type=RetType.BYTES) blockhash = resp_bytes[::-1].hex() assert_equal(blockhash, bb_hash) # Check invalid blockhashbyheight requests resp = self.test_rest_request(f"/blockhashbyheight/{INVALID_PARAM}", ret_type=RetType.OBJ, status=400) assert_equal(resp.read().decode('utf-8').rstrip(), f"Invalid height: {INVALID_PARAM}") resp = self.test_rest_request("/blockhashbyheight/1000000", ret_type=RetType.OBJ, status=404) assert_equal(resp.read().decode('utf-8').rstrip(), "Block height out of range") resp = self.test_rest_request("/blockhashbyheight/-1", ret_type=RetType.OBJ, status=400) assert_equal(resp.read().decode('utf-8').rstrip(), "Invalid height: -1") self.test_rest_request("/blockhashbyheight/", ret_type=RetType.OBJ, status=400) # Compare with json block header json_obj = self.test_rest_request(f"/headers/1/{bb_hash}") assert_equal(len(json_obj), 1) # ensure that there is one header in the json response assert_equal(json_obj[0]['hash'], bb_hash) # request/response hash should be the same # Compare with normal RPC block response rpc_block_json = self.nodes[0].getblock(bb_hash) for key in [ 'hash', 'confirmations', 'height', 'version', 'merkleroot', 'time', 'nonce', 'bits', 'difficulty', 'chainwork', 'previousblockhash' ]: assert_equal(json_obj[0][key], rpc_block_json[key]) # See if we can get 5 headers in one response self.generate(self.nodes[1], 5) json_obj = self.test_rest_request(f"/headers/5/{bb_hash}") assert_equal(len(json_obj), 5) # now we should have 5 header objects json_obj = self.test_rest_request( f"/blockfilterheaders/basic/5/{bb_hash}") first_filter_header = json_obj[0] assert_equal(len(json_obj), 5) # now we should have 5 filter header objects json_obj = self.test_rest_request(f"/blockfilter/basic/{bb_hash}") # Compare with normal RPC blockfilter response rpc_blockfilter = self.nodes[0].getblockfilter(bb_hash) assert_equal(first_filter_header, rpc_blockfilter['header']) assert_equal(json_obj['filter'], rpc_blockfilter['filter']) # Test number parsing for num in [ '5a', '-5', '0', '2001', '99999999999999999999999999999999999' ]: assert_equal( bytes( f'Header count is invalid or out of acceptable range (1-2000): {num}\r\n', 'ascii'), self.test_rest_request(f"/headers/{num}/{bb_hash}", ret_type=RetType.BYTES, status=400), ) self.log.info("Test tx inclusion in the /mempool and /block URIs") # Make 3 chained txs and mine them on node 1 txs = [] input_txid = txid for _ in range(3): utxo_to_spend = self.wallet.get_utxo(txid=input_txid) txs.append( self.wallet.send_self_transfer( from_node=self.nodes[0], utxo_to_spend=utxo_to_spend)['txid']) input_txid = txs[-1] self.sync_all() # Check that there are exactly 3 transactions in the TX memory pool before generating the block json_obj = self.test_rest_request("/mempool/info") assert_equal(json_obj['size'], 3) # the size of the memory pool should be greater than 3x ~100 bytes assert_greater_than(json_obj['bytes'], 300) # Check that there are our submitted transactions in the TX memory pool json_obj = self.test_rest_request("/mempool/contents") for i, tx in enumerate(txs): assert tx in json_obj assert_equal(json_obj[tx]['spentby'], txs[i + 1:i + 2]) assert_equal(json_obj[tx]['depends'], txs[i - 1:i]) # Now mine the transactions newblockhash = self.generate(self.nodes[1], 1) # Check if the 3 tx show up in the new block json_obj = self.test_rest_request(f"/block/{newblockhash[0]}") non_coinbase_txs = { tx['txid'] for tx in json_obj['tx'] if 'coinbase' not in tx['vin'][0] } assert_equal(non_coinbase_txs, set(txs)) # Verify that the non-coinbase tx has "prevout" key set for tx_obj in json_obj["tx"]: for vin in tx_obj["vin"]: if "coinbase" not in vin: assert "prevout" in vin assert_equal(vin["prevout"]["generated"], False) else: assert "prevout" not in vin # Check the same but without tx details json_obj = self.test_rest_request( f"/block/notxdetails/{newblockhash[0]}") for tx in txs: assert tx in json_obj['tx'] self.log.info("Test the /chaininfo URI") bb_hash = self.nodes[0].getbestblockhash() json_obj = self.test_rest_request("/chaininfo") assert_equal(json_obj['bestblockhash'], bb_hash)
class ReplaceByFeeTest(BGLTestFramework): def set_test_params(self): self.num_nodes = 1 self.extra_args = [ [ "-acceptnonstdtxn=1", "-maxorphantx=1000", "-limitancestorcount=50", "-limitancestorsize=101", "-limitdescendantcount=200", "-limitdescendantsize=101", ], ] self.supports_cli = False def run_test(self): self.wallet = MiniWallet(self.nodes[0]) # the pre-mined test framework chain contains coinbase outputs to the # MiniWallet's default address ADDRESS_BCRT1_P2WSH_OP_TRUE in blocks # 76-100 (see method BitcoinTestFramework._initialize_chain()) self.wallet.rescan_utxos() self.log.info("Running test simple doublespend...") self.test_simple_doublespend() self.log.info("Running test doublespend chain...") self.test_doublespend_chain() self.log.info("Running test doublespend tree...") self.test_doublespend_tree() self.log.info("Running test replacement feeperkb...") self.test_replacement_feeperkb() self.log.info("Running test spends of conflicting outputs...") self.test_spends_of_conflicting_outputs() self.log.info("Running test new unconfirmed inputs...") self.test_new_unconfirmed_inputs() self.log.info("Running test too many replacements...") self.test_too_many_replacements() self.log.info("Running test opt-in...") self.test_opt_in() self.log.info("Running test RPC...") self.test_rpc() self.log.info("Running test prioritised transactions...") self.test_prioritised_transactions() self.log.info("Running test no inherited signaling...") self.test_no_inherited_signaling() self.log.info("Running test replacement relay fee...") self.test_replacement_relay_fee() self.log.info("Passed") def make_utxo(self, node, amount, confirmed=True, scriptPubKey=DUMMY_P2WPKH_SCRIPT): """Create a txout with a given amount and scriptPubKey confirmed - txouts created will be confirmed in the blockchain; unconfirmed otherwise. """ txid, n = self.wallet.send_to(from_node=node, scriptPubKey=scriptPubKey, amount=amount) # If requested, ensure txouts are confirmed. if confirmed: mempool_size = len(node.getrawmempool()) while mempool_size > 0: self.generate(node, 1) new_size = len(node.getrawmempool()) # Error out if we have something stuck in the mempool, as this # would likely be a bug. assert new_size < mempool_size mempool_size = new_size return COutPoint(int(txid, 16), n) def test_simple_doublespend(self): """Simple doublespend""" # we use MiniWallet to create a transaction template with inputs correctly set, # and modify the output (amount, scriptPubKey) according to our needs tx_template = self.wallet.create_self_transfer( from_node=self.nodes[0])['tx'] tx1a = deepcopy(tx_template) tx1a.vout = [CTxOut(1 * COIN, DUMMY_P2WPKH_SCRIPT)] tx1a_hex = tx1a.serialize().hex() tx1a_txid = self.nodes[0].sendrawtransaction(tx1a_hex, 0) # Should fail because we haven't changed the fee tx1b = deepcopy(tx_template) tx1b.vout = [CTxOut(1 * COIN, DUMMY_2_P2WPKH_SCRIPT)] tx1b_hex = tx1b.serialize().hex() # This will raise an exception due to insufficient fee assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx1b_hex, 0) # Extra 0.1 BTC fee tx1b.vout[0].nValue -= int(0.1 * COIN) tx1b_hex = tx1b.serialize().hex() # Works when enabled tx1b_txid = self.nodes[0].sendrawtransaction(tx1b_hex, 0) mempool = self.nodes[0].getrawmempool() assert tx1a_txid not in mempool assert tx1b_txid in mempool assert_equal(tx1b_hex, self.nodes[0].getrawtransaction(tx1b_txid)) def test_doublespend_chain(self): """Doublespend of a long chain""" initial_nValue = 5 * COIN tx0_outpoint = self.make_utxo(self.nodes[0], initial_nValue) prevout = tx0_outpoint remaining_value = initial_nValue chain_txids = [] while remaining_value > 1 * COIN: remaining_value -= int(0.1 * COIN) tx = CTransaction() tx.vin = [CTxIn(prevout, nSequence=0)] tx.vout = [ CTxOut(remaining_value, CScript([1, OP_DROP] * 15 + [1])) ] tx_hex = tx.serialize().hex() txid = self.nodes[0].sendrawtransaction(tx_hex, 0) chain_txids.append(txid) prevout = COutPoint(int(txid, 16), 0) # Whether the double-spend is allowed is evaluated by including all # child fees - 4 BTC - so this attempt is rejected. dbl_tx = CTransaction() dbl_tx.vin = [CTxIn(tx0_outpoint, nSequence=0)] dbl_tx.vout = [CTxOut(initial_nValue - 3 * COIN, DUMMY_P2WPKH_SCRIPT)] dbl_tx_hex = dbl_tx.serialize().hex() # This will raise an exception due to insufficient fee assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, dbl_tx_hex, 0) # Accepted with sufficient fee dbl_tx = CTransaction() dbl_tx.vin = [CTxIn(tx0_outpoint, nSequence=0)] dbl_tx.vout = [CTxOut(int(0.1 * COIN), DUMMY_P2WPKH_SCRIPT)] dbl_tx_hex = dbl_tx.serialize().hex() self.nodes[0].sendrawtransaction(dbl_tx_hex, 0) mempool = self.nodes[0].getrawmempool() for doublespent_txid in chain_txids: assert doublespent_txid not in mempool def test_doublespend_tree(self): """Doublespend of a big tree of transactions""" initial_nValue = 5 * COIN tx0_outpoint = self.make_utxo(self.nodes[0], initial_nValue) def branch(prevout, initial_value, max_txs, tree_width=5, fee=0.00001 * COIN, _total_txs=None): if _total_txs is None: _total_txs = [0] if _total_txs[0] >= max_txs: return txout_value = (initial_value - fee) // tree_width if txout_value < fee: return vout = [ CTxOut(txout_value, CScript([i + 1])) for i in range(tree_width) ] tx = CTransaction() tx.vin = [CTxIn(prevout, nSequence=0)] tx.vout = vout tx_hex = tx.serialize().hex() assert len(tx.serialize()) < 100000 txid = self.nodes[0].sendrawtransaction(tx_hex, 0) yield tx _total_txs[0] += 1 txid = int(txid, 16) for i, txout in enumerate(tx.vout): for x in branch(COutPoint(txid, i), txout_value, max_txs, tree_width=tree_width, fee=fee, _total_txs=_total_txs): yield x fee = int(0.00001 * COIN) n = MAX_REPLACEMENT_LIMIT tree_txs = list(branch(tx0_outpoint, initial_nValue, n, fee=fee)) assert_equal(len(tree_txs), n) # Attempt double-spend, will fail because too little fee paid dbl_tx = CTransaction() dbl_tx.vin = [CTxIn(tx0_outpoint, nSequence=0)] dbl_tx.vout = [CTxOut(initial_nValue - fee * n, DUMMY_P2WPKH_SCRIPT)] dbl_tx_hex = dbl_tx.serialize().hex() # This will raise an exception due to insufficient fee assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, dbl_tx_hex, 0) # 0.1 BTC fee is enough dbl_tx = CTransaction() dbl_tx.vin = [CTxIn(tx0_outpoint, nSequence=0)] dbl_tx.vout = [ CTxOut(initial_nValue - fee * n - int(0.1 * COIN), DUMMY_P2WPKH_SCRIPT) ] dbl_tx_hex = dbl_tx.serialize().hex() self.nodes[0].sendrawtransaction(dbl_tx_hex, 0) mempool = self.nodes[0].getrawmempool() for tx in tree_txs: tx.rehash() assert tx.hash not in mempool # Try again, but with more total transactions than the "max txs # double-spent at once" anti-DoS limit. for n in (MAX_REPLACEMENT_LIMIT + 1, MAX_REPLACEMENT_LIMIT * 2): fee = int(0.00001 * COIN) tx0_outpoint = self.make_utxo(self.nodes[0], initial_nValue) tree_txs = list(branch(tx0_outpoint, initial_nValue, n, fee=fee)) assert_equal(len(tree_txs), n) dbl_tx = CTransaction() dbl_tx.vin = [CTxIn(tx0_outpoint, nSequence=0)] dbl_tx.vout = [ CTxOut(initial_nValue - 2 * fee * n, DUMMY_P2WPKH_SCRIPT) ] dbl_tx_hex = dbl_tx.serialize().hex() # This will raise an exception assert_raises_rpc_error(-26, "too many potential replacements", self.nodes[0].sendrawtransaction, dbl_tx_hex, 0) for tx in tree_txs: tx.rehash() self.nodes[0].getrawtransaction(tx.hash) def test_replacement_feeperkb(self): """Replacement requires fee-per-KB to be higher""" tx0_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN)) tx1a = CTransaction() tx1a.vin = [CTxIn(tx0_outpoint, nSequence=0)] tx1a.vout = [CTxOut(1 * COIN, DUMMY_P2WPKH_SCRIPT)] tx1a_hex = tx1a.serialize().hex() self.nodes[0].sendrawtransaction(tx1a_hex, 0) # Higher fee, but the fee per KB is much lower, so the replacement is # rejected. tx1b = CTransaction() tx1b.vin = [CTxIn(tx0_outpoint, nSequence=0)] tx1b.vout = [CTxOut(int(0.001 * COIN), CScript([b'a' * 999000]))] tx1b_hex = tx1b.serialize().hex() # This will raise an exception due to insufficient fee assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx1b_hex, 0) def test_spends_of_conflicting_outputs(self): """Replacements that spend conflicting tx outputs are rejected""" utxo1 = self.make_utxo(self.nodes[0], int(1.2 * COIN)) utxo2 = self.make_utxo(self.nodes[0], 3 * COIN) tx1a = CTransaction() tx1a.vin = [CTxIn(utxo1, nSequence=0)] tx1a.vout = [CTxOut(int(1.1 * COIN), DUMMY_P2WPKH_SCRIPT)] tx1a_hex = tx1a.serialize().hex() tx1a_txid = self.nodes[0].sendrawtransaction(tx1a_hex, 0) tx1a_txid = int(tx1a_txid, 16) # Direct spend an output of the transaction we're replacing. tx2 = CTransaction() tx2.vin = [CTxIn(utxo1, nSequence=0), CTxIn(utxo2, nSequence=0)] tx2.vin.append(CTxIn(COutPoint(tx1a_txid, 0), nSequence=0)) tx2.vout = tx1a.vout tx2_hex = tx2.serialize().hex() # This will raise an exception assert_raises_rpc_error(-26, "bad-txns-spends-conflicting-tx", self.nodes[0].sendrawtransaction, tx2_hex, 0) # Spend tx1a's output to test the indirect case. tx1b = CTransaction() tx1b.vin = [CTxIn(COutPoint(tx1a_txid, 0), nSequence=0)] tx1b.vout = [CTxOut(1 * COIN, DUMMY_P2WPKH_SCRIPT)] tx1b_hex = tx1b.serialize().hex() tx1b_txid = self.nodes[0].sendrawtransaction(tx1b_hex, 0) tx1b_txid = int(tx1b_txid, 16) tx2 = CTransaction() tx2.vin = [ CTxIn(utxo1, nSequence=0), CTxIn(utxo2, nSequence=0), CTxIn(COutPoint(tx1b_txid, 0)) ] tx2.vout = tx1a.vout tx2_hex = tx2.serialize().hex() # This will raise an exception assert_raises_rpc_error(-26, "bad-txns-spends-conflicting-tx", self.nodes[0].sendrawtransaction, tx2_hex, 0) def test_new_unconfirmed_inputs(self): """Replacements that add new unconfirmed inputs are rejected""" confirmed_utxo = self.make_utxo(self.nodes[0], int(1.1 * COIN)) unconfirmed_utxo = self.make_utxo(self.nodes[0], int(0.1 * COIN), False) tx1 = CTransaction() tx1.vin = [CTxIn(confirmed_utxo)] tx1.vout = [CTxOut(1 * COIN, DUMMY_P2WPKH_SCRIPT)] tx1_hex = tx1.serialize().hex() self.nodes[0].sendrawtransaction(tx1_hex, 0) tx2 = CTransaction() tx2.vin = [CTxIn(confirmed_utxo), CTxIn(unconfirmed_utxo)] tx2.vout = tx1.vout tx2_hex = tx2.serialize().hex() # This will raise an exception assert_raises_rpc_error(-26, "replacement-adds-unconfirmed", self.nodes[0].sendrawtransaction, tx2_hex, 0) def test_too_many_replacements(self): """Replacements that evict too many transactions are rejected""" # Try directly replacing more than MAX_REPLACEMENT_LIMIT # transactions # Start by creating a single transaction with many outputs initial_nValue = 10 * COIN utxo = self.make_utxo(self.nodes[0], initial_nValue) fee = int(0.0001 * COIN) split_value = int((initial_nValue - fee) / (MAX_REPLACEMENT_LIMIT + 1)) outputs = [] for _ in range(MAX_REPLACEMENT_LIMIT + 1): outputs.append(CTxOut(split_value, CScript([1]))) splitting_tx = CTransaction() splitting_tx.vin = [CTxIn(utxo, nSequence=0)] splitting_tx.vout = outputs splitting_tx_hex = splitting_tx.serialize().hex() txid = self.nodes[0].sendrawtransaction(splitting_tx_hex, 0) txid = int(txid, 16) # Now spend each of those outputs individually for i in range(MAX_REPLACEMENT_LIMIT + 1): tx_i = CTransaction() tx_i.vin = [CTxIn(COutPoint(txid, i), nSequence=0)] tx_i.vout = [CTxOut(split_value - fee, DUMMY_P2WPKH_SCRIPT)] tx_i_hex = tx_i.serialize().hex() self.nodes[0].sendrawtransaction(tx_i_hex, 0) # Now create doublespend of the whole lot; should fail. # Need a big enough fee to cover all spending transactions and have # a higher fee rate double_spend_value = (split_value - 100 * fee) * (MAX_REPLACEMENT_LIMIT + 1) inputs = [] for i in range(MAX_REPLACEMENT_LIMIT + 1): inputs.append(CTxIn(COutPoint(txid, i), nSequence=0)) double_tx = CTransaction() double_tx.vin = inputs double_tx.vout = [CTxOut(double_spend_value, CScript([b'a']))] double_tx_hex = double_tx.serialize().hex() # This will raise an exception assert_raises_rpc_error(-26, "too many potential replacements", self.nodes[0].sendrawtransaction, double_tx_hex, 0) # If we remove an input, it should pass double_tx = CTransaction() double_tx.vin = inputs[0:-1] double_tx.vout = [CTxOut(double_spend_value, CScript([b'a']))] double_tx_hex = double_tx.serialize().hex() self.nodes[0].sendrawtransaction(double_tx_hex, 0) def test_opt_in(self): """Replacing should only work if orig tx opted in""" tx0_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN)) # Create a non-opting in transaction tx1a = CTransaction() tx1a.vin = [CTxIn(tx0_outpoint, nSequence=0xffffffff)] tx1a.vout = [CTxOut(1 * COIN, DUMMY_P2WPKH_SCRIPT)] tx1a_hex = tx1a.serialize().hex() tx1a_txid = self.nodes[0].sendrawtransaction(tx1a_hex, 0) # This transaction isn't shown as replaceable assert_equal( self.nodes[0].getmempoolentry(tx1a_txid)['bip125-replaceable'], False) # Shouldn't be able to double-spend tx1b = CTransaction() tx1b.vin = [CTxIn(tx0_outpoint, nSequence=0)] tx1b.vout = [CTxOut(int(0.9 * COIN), DUMMY_P2WPKH_SCRIPT)] tx1b_hex = tx1b.serialize().hex() # This will raise an exception assert_raises_rpc_error(-26, "txn-mempool-conflict", self.nodes[0].sendrawtransaction, tx1b_hex, 0) tx1_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN)) # Create a different non-opting in transaction tx2a = CTransaction() tx2a.vin = [CTxIn(tx1_outpoint, nSequence=0xfffffffe)] tx2a.vout = [CTxOut(1 * COIN, DUMMY_P2WPKH_SCRIPT)] tx2a_hex = tx2a.serialize().hex() tx2a_txid = self.nodes[0].sendrawtransaction(tx2a_hex, 0) # Still shouldn't be able to double-spend tx2b = CTransaction() tx2b.vin = [CTxIn(tx1_outpoint, nSequence=0)] tx2b.vout = [CTxOut(int(0.9 * COIN), DUMMY_P2WPKH_SCRIPT)] tx2b_hex = tx2b.serialize().hex() # This will raise an exception assert_raises_rpc_error(-26, "txn-mempool-conflict", self.nodes[0].sendrawtransaction, tx2b_hex, 0) # Now create a new transaction that spends from tx1a and tx2a # opt-in on one of the inputs # Transaction should be replaceable on either input tx1a_txid = int(tx1a_txid, 16) tx2a_txid = int(tx2a_txid, 16) tx3a = CTransaction() tx3a.vin = [ CTxIn(COutPoint(tx1a_txid, 0), nSequence=0xffffffff), CTxIn(COutPoint(tx2a_txid, 0), nSequence=0xfffffffd) ] tx3a.vout = [ CTxOut(int(0.9 * COIN), CScript([b'c'])), CTxOut(int(0.9 * COIN), CScript([b'd'])) ] tx3a_hex = tx3a.serialize().hex() tx3a_txid = self.nodes[0].sendrawtransaction(tx3a_hex, 0) # This transaction is shown as replaceable assert_equal( self.nodes[0].getmempoolentry(tx3a_txid)['bip125-replaceable'], True) tx3b = CTransaction() tx3b.vin = [CTxIn(COutPoint(tx1a_txid, 0), nSequence=0)] tx3b.vout = [CTxOut(int(0.5 * COIN), DUMMY_P2WPKH_SCRIPT)] tx3b_hex = tx3b.serialize().hex() tx3c = CTransaction() tx3c.vin = [CTxIn(COutPoint(tx2a_txid, 0), nSequence=0)] tx3c.vout = [CTxOut(int(0.5 * COIN), DUMMY_P2WPKH_SCRIPT)] tx3c_hex = tx3c.serialize().hex() self.nodes[0].sendrawtransaction(tx3b_hex, 0) # If tx3b was accepted, tx3c won't look like a replacement, # but make sure it is accepted anyway self.nodes[0].sendrawtransaction(tx3c_hex, 0) def test_prioritised_transactions(self): # Ensure that fee deltas used via prioritisetransaction are # correctly used by replacement logic # 1. Check that feeperkb uses modified fees tx0_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN)) tx1a = CTransaction() tx1a.vin = [CTxIn(tx0_outpoint, nSequence=0)] tx1a.vout = [CTxOut(1 * COIN, DUMMY_P2WPKH_SCRIPT)] tx1a_hex = tx1a.serialize().hex() tx1a_txid = self.nodes[0].sendrawtransaction(tx1a_hex, 0) # Higher fee, but the actual fee per KB is much lower. tx1b = CTransaction() tx1b.vin = [CTxIn(tx0_outpoint, nSequence=0)] tx1b.vout = [CTxOut(int(0.001 * COIN), CScript([b'a' * 740000]))] tx1b_hex = tx1b.serialize().hex() # Verify tx1b cannot replace tx1a. assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx1b_hex, 0) # Use prioritisetransaction to set tx1a's fee to 0. self.nodes[0].prioritisetransaction(txid=tx1a_txid, fee_delta=int(-0.1 * COIN)) # Now tx1b should be able to replace tx1a tx1b_txid = self.nodes[0].sendrawtransaction(tx1b_hex, 0) assert tx1b_txid in self.nodes[0].getrawmempool() # 2. Check that absolute fee checks use modified fee. tx1_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN)) tx2a = CTransaction() tx2a.vin = [CTxIn(tx1_outpoint, nSequence=0)] tx2a.vout = [CTxOut(1 * COIN, DUMMY_P2WPKH_SCRIPT)] tx2a_hex = tx2a.serialize().hex() self.nodes[0].sendrawtransaction(tx2a_hex, 0) # Lower fee, but we'll prioritise it tx2b = CTransaction() tx2b.vin = [CTxIn(tx1_outpoint, nSequence=0)] tx2b.vout = [CTxOut(int(1.01 * COIN), DUMMY_P2WPKH_SCRIPT)] tx2b.rehash() tx2b_hex = tx2b.serialize().hex() # Verify tx2b cannot replace tx2a. assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx2b_hex, 0) # Now prioritise tx2b to have a higher modified fee self.nodes[0].prioritisetransaction(txid=tx2b.hash, fee_delta=int(0.1 * COIN)) # tx2b should now be accepted tx2b_txid = self.nodes[0].sendrawtransaction(tx2b_hex, 0) assert tx2b_txid in self.nodes[0].getrawmempool() def test_rpc(self): us0 = self.wallet.get_utxo() ins = [us0] outs = {ADDRESS_BCRT1_UNSPENDABLE: Decimal(1.0000000)} rawtx0 = self.nodes[0].createrawtransaction(ins, outs, 0, True) rawtx1 = self.nodes[0].createrawtransaction(ins, outs, 0, False) json0 = self.nodes[0].decoderawtransaction(rawtx0) json1 = self.nodes[0].decoderawtransaction(rawtx1) assert_equal(json0["vin"][0]["sequence"], 4294967293) assert_equal(json1["vin"][0]["sequence"], 4294967295) if self.is_wallet_compiled(): self.init_wallet(node=0) rawtx2 = self.nodes[0].createrawtransaction([], outs) frawtx2a = self.nodes[0].fundrawtransaction( rawtx2, {"replaceable": True}) frawtx2b = self.nodes[0].fundrawtransaction( rawtx2, {"replaceable": False}) json0 = self.nodes[0].decoderawtransaction(frawtx2a['hex']) json1 = self.nodes[0].decoderawtransaction(frawtx2b['hex']) assert_equal(json0["vin"][0]["sequence"], 4294967293) assert_equal(json1["vin"][0]["sequence"], 4294967294) def test_no_inherited_signaling(self): confirmed_utxo = self.wallet.get_utxo() # Create an explicitly opt-in parent transaction optin_parent_tx = self.wallet.send_self_transfer( from_node=self.nodes[0], utxo_to_spend=confirmed_utxo, sequence=BIP125_SEQUENCE_NUMBER, fee_rate=Decimal('0.01'), ) assert_equal( True, self.nodes[0].getmempoolentry( optin_parent_tx['txid'])['bip125-replaceable']) replacement_parent_tx = self.wallet.create_self_transfer( from_node=self.nodes[0], utxo_to_spend=confirmed_utxo, sequence=BIP125_SEQUENCE_NUMBER, fee_rate=Decimal('0.02'), ) # Test if parent tx can be replaced. res = self.nodes[0].testmempoolaccept( rawtxs=[replacement_parent_tx['hex']])[0] # Parent can be replaced. assert_equal(res['allowed'], True) # Create an opt-out child tx spending the opt-in parent parent_utxo = self.wallet.get_utxo(txid=optin_parent_tx['txid']) optout_child_tx = self.wallet.send_self_transfer( from_node=self.nodes[0], utxo_to_spend=parent_utxo, sequence=0xffffffff, fee_rate=Decimal('0.01'), ) # Reports true due to inheritance assert_equal( True, self.nodes[0].getmempoolentry( optout_child_tx['txid'])['bip125-replaceable']) replacement_child_tx = self.wallet.create_self_transfer( from_node=self.nodes[0], utxo_to_spend=parent_utxo, sequence=0xffffffff, fee_rate=Decimal('0.02'), mempool_valid=False, ) # Broadcast replacement child tx # BIP 125 : # 1. The original transactions signal replaceability explicitly or through inheritance as described in the above # Summary section. # The original transaction (`optout_child_tx`) doesn't signal RBF but its parent (`optin_parent_tx`) does. # The replacement transaction (`replacement_child_tx`) should be able to replace the original transaction. # See CVE-2021-31876 for further explanations. assert_equal( True, self.nodes[0].getmempoolentry( optin_parent_tx['txid'])['bip125-replaceable']) assert_raises_rpc_error(-26, 'txn-mempool-conflict', self.nodes[0].sendrawtransaction, replacement_child_tx["hex"], 0) self.log.info( 'Check that the child tx can still be replaced (via a tx that also replaces the parent)' ) replacement_parent_tx = self.wallet.send_self_transfer( from_node=self.nodes[0], utxo_to_spend=confirmed_utxo, sequence=0xffffffff, fee_rate=Decimal('0.03'), ) # Check that child is removed and update wallet utxo state assert_raises_rpc_error(-5, 'Transaction not in mempool', self.nodes[0].getmempoolentry, optout_child_tx['txid']) self.wallet.get_utxo(txid=optout_child_tx['txid']) def test_replacement_relay_fee(self): tx = self.wallet.send_self_transfer(from_node=self.nodes[0])['tx'] # Higher fee, higher feerate, different txid, but the replacement does not provide a relay # fee conforming to node's `incrementalrelayfee` policy of 1000 sat per KB. tx.vout[0].nValue -= 1 assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx.serialize().hex())
class CoinStatsIndexTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 2 self.supports_cli = False self.extra_args = [[], ["-coinstatsindex"]] def run_test(self): self.wallet = MiniWallet(self.nodes[0]) self._test_coin_stats_index() self._test_use_index_option() self._test_reorg_index() self._test_index_rejects_hash_serialized() def block_sanity_check(self, block_info): block_subsidy = 50 assert_equal( block_info['prevout_spent'] + block_subsidy, block_info['new_outputs_ex_coinbase'] + block_info['coinbase'] + block_info['unspendable']) def _test_coin_stats_index(self): node = self.nodes[0] index_node = self.nodes[1] # Both none and muhash options allow the usage of the index index_hash_options = ['none', 'muhash'] # Generate a normal transaction and mine it self.generate(self.wallet, COINBASE_MATURITY + 1) self.wallet.send_self_transfer(from_node=node) self.generate(node, 1) self.log.info( "Test that gettxoutsetinfo() output is consistent with or without coinstatsindex option" ) res0 = node.gettxoutsetinfo('none') # The fields 'disk_size' and 'transactions' do not exist on the index del res0['disk_size'], res0['transactions'] for hash_option in index_hash_options: res1 = index_node.gettxoutsetinfo(hash_option) # The fields 'block_info' and 'total_unspendable_amount' only exist on the index del res1['block_info'], res1['total_unspendable_amount'] res1.pop('muhash', None) # Everything left should be the same assert_equal(res1, res0) self.log.info( "Test that gettxoutsetinfo() can get fetch data on specific heights with index" ) # Generate a new tip self.generate(node, 5) for hash_option in index_hash_options: # Fetch old stats by height res2 = index_node.gettxoutsetinfo(hash_option, 102) del res2['block_info'], res2['total_unspendable_amount'] res2.pop('muhash', None) assert_equal(res0, res2) # Fetch old stats by hash res3 = index_node.gettxoutsetinfo(hash_option, res0['bestblock']) del res3['block_info'], res3['total_unspendable_amount'] res3.pop('muhash', None) assert_equal(res0, res3) # It does not work without coinstatsindex assert_raises_rpc_error( -8, "Querying specific block heights requires coinstatsindex", node.gettxoutsetinfo, hash_option, 102) self.log.info("Test gettxoutsetinfo() with index and verbose flag") for hash_option in index_hash_options: # Genesis block is unspendable res4 = index_node.gettxoutsetinfo(hash_option, 0) assert_equal(res4['total_unspendable_amount'], 50) assert_equal( res4['block_info'], { 'unspendable': 50, 'prevout_spent': 0, 'new_outputs_ex_coinbase': 0, 'coinbase': 0, 'unspendables': { 'genesis_block': 50, 'bip30': 0, 'scripts': 0, 'unclaimed_rewards': 0 } }) self.block_sanity_check(res4['block_info']) # Test an older block height that included a normal tx res5 = index_node.gettxoutsetinfo(hash_option, 102) assert_equal(res5['total_unspendable_amount'], 50) assert_equal( res5['block_info'], { 'unspendable': 0, 'prevout_spent': 50, 'new_outputs_ex_coinbase': Decimal('49.99968800'), 'coinbase': Decimal('50.00031200'), 'unspendables': { 'genesis_block': 0, 'bip30': 0, 'scripts': 0, 'unclaimed_rewards': 0, } }) self.block_sanity_check(res5['block_info']) # Generate and send a normal tx with two outputs tx1_txid, tx1_vout = self.wallet.send_to( from_node=node, scriptPubKey=self.wallet.get_scriptPubKey(), amount=21 * COIN, ) # Find the right position of the 21 BTC output tx1_out_21 = self.wallet.get_utxo(txid=tx1_txid, vout=tx1_vout) # Generate and send another tx with an OP_RETURN output (which is unspendable) tx2 = self.wallet.create_self_transfer(utxo_to_spend=tx1_out_21)['tx'] tx2.vout = [ CTxOut(int(Decimal('20.99') * COIN), CScript([OP_RETURN] + [OP_FALSE] * 30)) ] tx2_hex = tx2.serialize().hex() self.nodes[0].sendrawtransaction(tx2_hex) # Include both txs in a block self.generate(self.nodes[0], 1) for hash_option in index_hash_options: # Check all amounts were registered correctly res6 = index_node.gettxoutsetinfo(hash_option, 108) assert_equal(res6['total_unspendable_amount'], Decimal('70.99000000')) assert_equal( res6['block_info'], { 'unspendable': Decimal('20.99000000'), 'prevout_spent': 71, 'new_outputs_ex_coinbase': Decimal('49.99999000'), 'coinbase': Decimal('50.01001000'), 'unspendables': { 'genesis_block': 0, 'bip30': 0, 'scripts': Decimal('20.99000000'), 'unclaimed_rewards': 0, } }) self.block_sanity_check(res6['block_info']) # Create a coinbase that does not claim full subsidy and also # has two outputs cb = create_coinbase(109, nValue=35) cb.vout.append(CTxOut(5 * COIN, CScript([OP_FALSE]))) cb.rehash() # Generate a block that includes previous coinbase tip = self.nodes[0].getbestblockhash() block_time = self.nodes[0].getblock(tip)['time'] + 1 block = create_block(int(tip, 16), cb, block_time) block.solve() self.nodes[0].submitblock(block.serialize().hex()) self.sync_all() for hash_option in index_hash_options: res7 = index_node.gettxoutsetinfo(hash_option, 109) assert_equal(res7['total_unspendable_amount'], Decimal('80.99000000')) assert_equal( res7['block_info'], { 'unspendable': 10, 'prevout_spent': 0, 'new_outputs_ex_coinbase': 0, 'coinbase': 40, 'unspendables': { 'genesis_block': 0, 'bip30': 0, 'scripts': 0, 'unclaimed_rewards': 10 } }) self.block_sanity_check(res7['block_info']) self.log.info("Test that the index is robust across restarts") res8 = index_node.gettxoutsetinfo('muhash') self.restart_node(1, extra_args=self.extra_args[1]) res9 = index_node.gettxoutsetinfo('muhash') assert_equal(res8, res9) self.generate(index_node, 1, sync_fun=self.no_op) res10 = index_node.gettxoutsetinfo('muhash') assert (res8['txouts'] < res10['txouts']) self.log.info("Test that the index works with -reindex") self.restart_node(1, extra_args=["-coinstatsindex", "-reindex"]) res11 = index_node.gettxoutsetinfo('muhash') assert_equal(res11, res10) self.log.info( "Test that -reindex-chainstate is disallowed with coinstatsindex") self.nodes[1].assert_start_raises_init_error( expected_msg= 'Error: -reindex-chainstate option is not compatible with -coinstatsindex. ' 'Please temporarily disable coinstatsindex while using -reindex-chainstate, or replace -reindex-chainstate with -reindex to fully rebuild all indexes.', extra_args=['-coinstatsindex', '-reindex-chainstate'], ) def _test_use_index_option(self): self.log.info("Test use_index option for nodes running the index") self.connect_nodes(0, 1) self.nodes[0].waitforblockheight(110) res = self.nodes[0].gettxoutsetinfo('muhash') option_res = self.nodes[1].gettxoutsetinfo(hash_type='muhash', hash_or_height=None, use_index=False) del res['disk_size'], option_res['disk_size'] assert_equal(res, option_res) def _test_reorg_index(self): self.log.info("Test that index can handle reorgs") # Generate two block, let the index catch up, then invalidate the blocks index_node = self.nodes[1] reorg_blocks = self.generatetoaddress(index_node, 2, getnewdestination()[2]) reorg_block = reorg_blocks[1] res_invalid = index_node.gettxoutsetinfo('muhash') index_node.invalidateblock(reorg_blocks[0]) assert_equal(index_node.gettxoutsetinfo('muhash')['height'], 110) # Add two new blocks block = self.generate(index_node, 2, sync_fun=self.no_op)[1] res = index_node.gettxoutsetinfo(hash_type='muhash', hash_or_height=None, use_index=False) # Test that the result of the reorged block is not returned for its old block height res2 = index_node.gettxoutsetinfo(hash_type='muhash', hash_or_height=112) assert_equal(res["bestblock"], block) assert_equal(res["muhash"], res2["muhash"]) assert (res["muhash"] != res_invalid["muhash"]) # Test that requesting reorged out block by hash is still returning correct results res_invalid2 = index_node.gettxoutsetinfo(hash_type='muhash', hash_or_height=reorg_block) assert_equal(res_invalid2["muhash"], res_invalid["muhash"]) assert (res["muhash"] != res_invalid2["muhash"]) # Add another block, so we don't depend on reconsiderblock remembering which # blocks were touched by invalidateblock self.generate(index_node, 1) # Ensure that removing and re-adding blocks yields consistent results block = index_node.getblockhash(99) index_node.invalidateblock(block) index_node.reconsiderblock(block) res3 = index_node.gettxoutsetinfo(hash_type='muhash', hash_or_height=112) assert_equal(res2, res3) def _test_index_rejects_hash_serialized(self): self.log.info( "Test that the rpc raises if the legacy hash is passed with the index" ) msg = "hash_serialized_2 hash type cannot be queried for a specific block" assert_raises_rpc_error(-8, msg, self.nodes[1].gettxoutsetinfo, hash_type='hash_serialized_2', hash_or_height=111) for use_index in {True, False, None}: assert_raises_rpc_error(-8, msg, self.nodes[1].gettxoutsetinfo, hash_type='hash_serialized_2', hash_or_height=111, use_index=use_index)
class ReplaceByFeeTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 2 self.extra_args = [ [ "-maxorphantx=1000", "-limitancestorcount=50", "-limitancestorsize=101", "-limitdescendantcount=200", "-limitdescendantsize=101", ], # second node has default mempool parameters [], ] self.supports_cli = False def run_test(self): self.wallet = MiniWallet(self.nodes[0]) # the pre-mined test framework chain contains coinbase outputs to the # MiniWallet's default address in blocks 76-100 (see method # BitcoinTestFramework._initialize_chain()) self.wallet.rescan_utxos() self.log.info("Running test simple doublespend...") self.test_simple_doublespend() self.log.info("Running test doublespend chain...") self.test_doublespend_chain() self.log.info("Running test doublespend tree...") self.test_doublespend_tree() self.log.info("Running test replacement feeperkb...") self.test_replacement_feeperkb() self.log.info("Running test spends of conflicting outputs...") self.test_spends_of_conflicting_outputs() self.log.info("Running test new unconfirmed inputs...") self.test_new_unconfirmed_inputs() self.log.info("Running test too many replacements...") self.test_too_many_replacements() self.log.info( "Running test too many replacements using default mempool params..." ) self.test_too_many_replacements_with_default_mempool_params() self.log.info("Running test opt-in...") self.test_opt_in() self.log.info("Running test RPC...") self.test_rpc() self.log.info("Running test prioritised transactions...") self.test_prioritised_transactions() self.log.info("Running test no inherited signaling...") self.test_no_inherited_signaling() self.log.info("Running test replacement relay fee...") self.test_replacement_relay_fee() self.log.info("Running test full replace by fee...") self.test_fullrbf() self.log.info("Passed") def make_utxo(self, node, amount, *, confirmed=True, scriptPubKey=None): """Create a txout with a given amount and scriptPubKey confirmed - txout created will be confirmed in the blockchain; unconfirmed otherwise. """ txid, n = self.wallet.send_to(from_node=node, scriptPubKey=scriptPubKey or self.wallet.get_scriptPubKey(), amount=amount) if confirmed: mempool_size = len(node.getrawmempool()) while mempool_size > 0: self.generate(node, 1) new_size = len(node.getrawmempool()) # Error out if we have something stuck in the mempool, as this # would likely be a bug. assert new_size < mempool_size mempool_size = new_size return self.wallet.get_utxo(txid=txid, vout=n) def test_simple_doublespend(self): """Simple doublespend""" # we use MiniWallet to create a transaction template with inputs correctly set, # and modify the output (amount, scriptPubKey) according to our needs tx = self.wallet.create_self_transfer()["tx"] tx1a_txid = self.nodes[0].sendrawtransaction(tx.serialize().hex()) # Should fail because we haven't changed the fee tx.vout[0].scriptPubKey[-1] ^= 1 # This will raise an exception due to insufficient fee assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx.serialize().hex(), 0) # Extra 0.1 BTC fee tx.vout[0].nValue -= int(0.1 * COIN) tx1b_hex = tx.serialize().hex() # Works when enabled tx1b_txid = self.nodes[0].sendrawtransaction(tx1b_hex, 0) mempool = self.nodes[0].getrawmempool() assert tx1a_txid not in mempool assert tx1b_txid in mempool assert_equal(tx1b_hex, self.nodes[0].getrawtransaction(tx1b_txid)) def test_doublespend_chain(self): """Doublespend of a long chain""" initial_nValue = 5 * COIN tx0_outpoint = self.make_utxo(self.nodes[0], initial_nValue) prevout = tx0_outpoint remaining_value = initial_nValue chain_txids = [] while remaining_value > 1 * COIN: remaining_value -= int(0.1 * COIN) prevout = self.wallet.send_self_transfer( from_node=self.nodes[0], utxo_to_spend=prevout, sequence=0, fee=Decimal("0.1"), )["new_utxo"] chain_txids.append(prevout["txid"]) # Whether the double-spend is allowed is evaluated by including all # child fees - 4 BTC - so this attempt is rejected. dbl_tx = self.wallet.create_self_transfer( utxo_to_spend=tx0_outpoint, sequence=0, fee=Decimal("3"), )["tx"] dbl_tx_hex = dbl_tx.serialize().hex() # This will raise an exception due to insufficient fee assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, dbl_tx_hex, 0) # Accepted with sufficient fee dbl_tx.vout[0].nValue = int(0.1 * COIN) dbl_tx_hex = dbl_tx.serialize().hex() self.nodes[0].sendrawtransaction(dbl_tx_hex, 0) mempool = self.nodes[0].getrawmempool() for doublespent_txid in chain_txids: assert doublespent_txid not in mempool def test_doublespend_tree(self): """Doublespend of a big tree of transactions""" initial_nValue = 5 * COIN tx0_outpoint = self.make_utxo(self.nodes[0], initial_nValue) def branch(prevout, initial_value, max_txs, tree_width=5, fee=0.00001 * COIN, _total_txs=None): if _total_txs is None: _total_txs = [0] if _total_txs[0] >= max_txs: return txout_value = (initial_value - fee) // tree_width if txout_value < fee: return tx = self.wallet.send_self_transfer_multi( utxos_to_spend=[prevout], from_node=self.nodes[0], sequence=0, num_outputs=tree_width, amount_per_output=txout_value, ) yield tx["txid"] _total_txs[0] += 1 for utxo in tx["new_utxos"]: for x in branch(utxo, txout_value, max_txs, tree_width=tree_width, fee=fee, _total_txs=_total_txs): yield x fee = int(0.00001 * COIN) n = MAX_REPLACEMENT_LIMIT tree_txs = list(branch(tx0_outpoint, initial_nValue, n, fee=fee)) assert_equal(len(tree_txs), n) # Attempt double-spend, will fail because too little fee paid dbl_tx_hex = self.wallet.create_self_transfer( utxo_to_spend=tx0_outpoint, sequence=0, fee=(Decimal(fee) / COIN) * n, )["hex"] # This will raise an exception due to insufficient fee assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, dbl_tx_hex, 0) # 0.1 BTC fee is enough dbl_tx_hex = self.wallet.create_self_transfer( utxo_to_spend=tx0_outpoint, sequence=0, fee=(Decimal(fee) / COIN) * n + Decimal("0.1"), )["hex"] self.nodes[0].sendrawtransaction(dbl_tx_hex, 0) mempool = self.nodes[0].getrawmempool() for txid in tree_txs: assert txid not in mempool # Try again, but with more total transactions than the "max txs # double-spent at once" anti-DoS limit. for n in (MAX_REPLACEMENT_LIMIT + 1, MAX_REPLACEMENT_LIMIT * 2): fee = int(0.00001 * COIN) tx0_outpoint = self.make_utxo(self.nodes[0], initial_nValue) tree_txs = list(branch(tx0_outpoint, initial_nValue, n, fee=fee)) assert_equal(len(tree_txs), n) dbl_tx_hex = self.wallet.create_self_transfer( utxo_to_spend=tx0_outpoint, sequence=0, fee=2 * (Decimal(fee) / COIN) * n, )["hex"] # This will raise an exception assert_raises_rpc_error(-26, "too many potential replacements", self.nodes[0].sendrawtransaction, dbl_tx_hex, 0) for txid in tree_txs: self.nodes[0].getrawtransaction(txid) def test_replacement_feeperkb(self): """Replacement requires fee-per-KB to be higher""" tx0_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN)) self.wallet.send_self_transfer( from_node=self.nodes[0], utxo_to_spend=tx0_outpoint, sequence=0, fee=Decimal("0.1"), ) # Higher fee, but the fee per KB is much lower, so the replacement is # rejected. tx1b_hex = self.wallet.create_self_transfer_multi( utxos_to_spend=[tx0_outpoint], sequence=0, num_outputs=100, amount_per_output=1000, )["hex"] # This will raise an exception due to insufficient fee assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx1b_hex, 0) def test_spends_of_conflicting_outputs(self): """Replacements that spend conflicting tx outputs are rejected""" utxo1 = self.make_utxo(self.nodes[0], int(1.2 * COIN)) utxo2 = self.make_utxo(self.nodes[0], 3 * COIN) tx1a_utxo = self.wallet.send_self_transfer( from_node=self.nodes[0], utxo_to_spend=utxo1, sequence=0, fee=Decimal("0.1"), )["new_utxo"] # Direct spend an output of the transaction we're replacing. tx2_hex = self.wallet.create_self_transfer_multi( utxos_to_spend=[utxo1, utxo2, tx1a_utxo], sequence=0, amount_per_output=int(COIN * tx1a_utxo["value"]), )["hex"] # This will raise an exception assert_raises_rpc_error(-26, "bad-txns-spends-conflicting-tx", self.nodes[0].sendrawtransaction, tx2_hex, 0) # Spend tx1a's output to test the indirect case. tx1b_utxo = self.wallet.send_self_transfer( from_node=self.nodes[0], utxo_to_spend=tx1a_utxo, sequence=0, fee=Decimal("0.1"), )["new_utxo"] tx2_hex = self.wallet.create_self_transfer_multi( utxos_to_spend=[utxo1, utxo2, tx1b_utxo], sequence=0, amount_per_output=int(COIN * tx1a_utxo["value"]), )["hex"] # This will raise an exception assert_raises_rpc_error(-26, "bad-txns-spends-conflicting-tx", self.nodes[0].sendrawtransaction, tx2_hex, 0) def test_new_unconfirmed_inputs(self): """Replacements that add new unconfirmed inputs are rejected""" confirmed_utxo = self.make_utxo(self.nodes[0], int(1.1 * COIN)) unconfirmed_utxo = self.make_utxo(self.nodes[0], int(0.1 * COIN), confirmed=False) self.wallet.send_self_transfer( from_node=self.nodes[0], utxo_to_spend=confirmed_utxo, sequence=0, fee=Decimal("0.1"), ) tx2_hex = self.wallet.create_self_transfer_multi( utxos_to_spend=[confirmed_utxo, unconfirmed_utxo], sequence=0, amount_per_output=1 * COIN, )["hex"] # This will raise an exception assert_raises_rpc_error(-26, "replacement-adds-unconfirmed", self.nodes[0].sendrawtransaction, tx2_hex, 0) def test_too_many_replacements(self): """Replacements that evict too many transactions are rejected""" # Try directly replacing more than MAX_REPLACEMENT_LIMIT # transactions # Start by creating a single transaction with many outputs initial_nValue = 10 * COIN utxo = self.make_utxo(self.nodes[0], initial_nValue) fee = int(0.0001 * COIN) split_value = int((initial_nValue - fee) / (MAX_REPLACEMENT_LIMIT + 1)) splitting_tx_utxos = self.wallet.send_self_transfer_multi( from_node=self.nodes[0], utxos_to_spend=[utxo], sequence=0, num_outputs=MAX_REPLACEMENT_LIMIT + 1, amount_per_output=split_value, )["new_utxos"] # Now spend each of those outputs individually for utxo in splitting_tx_utxos: self.wallet.send_self_transfer( from_node=self.nodes[0], utxo_to_spend=utxo, sequence=0, fee=Decimal(fee) / COIN, ) # Now create doublespend of the whole lot; should fail. # Need a big enough fee to cover all spending transactions and have # a higher fee rate double_spend_value = (split_value - 100 * fee) * (MAX_REPLACEMENT_LIMIT + 1) double_tx = self.wallet.create_self_transfer_multi( utxos_to_spend=splitting_tx_utxos, sequence=0, amount_per_output=double_spend_value, )["tx"] double_tx_hex = double_tx.serialize().hex() # This will raise an exception assert_raises_rpc_error(-26, "too many potential replacements", self.nodes[0].sendrawtransaction, double_tx_hex, 0) # If we remove an input, it should pass double_tx.vin.pop() double_tx_hex = double_tx.serialize().hex() self.nodes[0].sendrawtransaction(double_tx_hex, 0) def test_too_many_replacements_with_default_mempool_params(self): """ Test rule 5 of BIP125 (do not allow replacements that cause more than 100 evictions) without having to rely on non-default mempool parameters. In order to do this, create a number of "root" UTXOs, and then hang enough transactions off of each root UTXO to exceed the MAX_REPLACEMENT_LIMIT. Then create a conflicting RBF replacement transaction. """ normal_node = self.nodes[1] wallet = MiniWallet(normal_node) wallet.rescan_utxos() # Clear mempools to avoid cross-node sync failure. for node in self.nodes: self.generate(node, 1) # This has to be chosen so that the total number of transactions can exceed # MAX_REPLACEMENT_LIMIT without having any one tx graph run into the descendant # limit; 10 works. num_tx_graphs = 10 # (Number of transactions per graph, BIP125 rule 5 failure expected) cases = [ # Test the base case of evicting fewer than MAX_REPLACEMENT_LIMIT # transactions. ((MAX_REPLACEMENT_LIMIT // num_tx_graphs) - 1, False), # Test hitting the rule 5 eviction limit. (MAX_REPLACEMENT_LIMIT // num_tx_graphs, True), ] for (txs_per_graph, failure_expected) in cases: self.log.debug( f"txs_per_graph: {txs_per_graph}, failure: {failure_expected}") # "Root" utxos of each txn graph that we will attempt to double-spend with # an RBF replacement. root_utxos = [] # For each root UTXO, create a package that contains the spend of that # UTXO and `txs_per_graph` children tx. for graph_num in range(num_tx_graphs): root_utxos.append(wallet.get_utxo()) optin_parent_tx = wallet.send_self_transfer_multi( from_node=normal_node, sequence=BIP125_SEQUENCE_NUMBER, utxos_to_spend=[root_utxos[graph_num]], num_outputs=txs_per_graph, ) assert_equal( True, normal_node.getmempoolentry( optin_parent_tx['txid'])['bip125-replaceable']) new_utxos = optin_parent_tx['new_utxos'] for utxo in new_utxos: # Create spends for each output from the "root" of this graph. child_tx = wallet.send_self_transfer( from_node=normal_node, utxo_to_spend=utxo, ) assert normal_node.getmempoolentry(child_tx['txid']) num_txs_invalidated = len(root_utxos) + (num_tx_graphs * txs_per_graph) if failure_expected: assert num_txs_invalidated > MAX_REPLACEMENT_LIMIT else: assert num_txs_invalidated <= MAX_REPLACEMENT_LIMIT # Now attempt to submit a tx that double-spends all the root tx inputs, which # would invalidate `num_txs_invalidated` transactions. tx_hex = wallet.create_self_transfer_multi( utxos_to_spend=root_utxos, fee_per_output=10_000_000, # absurdly high feerate )["hex"] if failure_expected: assert_raises_rpc_error(-26, "too many potential replacements", normal_node.sendrawtransaction, tx_hex, 0) else: txid = normal_node.sendrawtransaction(tx_hex, 0) assert normal_node.getmempoolentry(txid) # Clear the mempool once finished, and rescan the other nodes' wallet # to account for the spends we've made on `normal_node`. self.generate(normal_node, 1) self.wallet.rescan_utxos() def test_opt_in(self): """Replacing should only work if orig tx opted in""" tx0_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN)) # Create a non-opting in transaction tx1a_utxo = self.wallet.send_self_transfer( from_node=self.nodes[0], utxo_to_spend=tx0_outpoint, sequence=SEQUENCE_FINAL, fee=Decimal("0.1"), )["new_utxo"] # This transaction isn't shown as replaceable assert_equal( self.nodes[0].getmempoolentry( tx1a_utxo["txid"])['bip125-replaceable'], False) # Shouldn't be able to double-spend tx1b_hex = self.wallet.create_self_transfer( utxo_to_spend=tx0_outpoint, sequence=0, fee=Decimal("0.2"), )["hex"] # This will raise an exception assert_raises_rpc_error(-26, "txn-mempool-conflict", self.nodes[0].sendrawtransaction, tx1b_hex, 0) tx1_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN)) # Create a different non-opting in transaction tx2a_utxo = self.wallet.send_self_transfer( from_node=self.nodes[0], utxo_to_spend=tx1_outpoint, sequence=0xfffffffe, fee=Decimal("0.1"), )["new_utxo"] # Still shouldn't be able to double-spend tx2b_hex = self.wallet.create_self_transfer( utxo_to_spend=tx1_outpoint, sequence=0, fee=Decimal("0.2"), )["hex"] # This will raise an exception assert_raises_rpc_error(-26, "txn-mempool-conflict", self.nodes[0].sendrawtransaction, tx2b_hex, 0) # Now create a new transaction that spends from tx1a and tx2a # opt-in on one of the inputs # Transaction should be replaceable on either input tx3a_txid = self.wallet.send_self_transfer_multi( from_node=self.nodes[0], utxos_to_spend=[tx1a_utxo, tx2a_utxo], sequence=[SEQUENCE_FINAL, 0xfffffffd], fee_per_output=int(0.1 * COIN), )["txid"] # This transaction is shown as replaceable assert_equal( self.nodes[0].getmempoolentry(tx3a_txid)['bip125-replaceable'], True) self.wallet.send_self_transfer( from_node=self.nodes[0], utxo_to_spend=tx1a_utxo, sequence=0, fee=Decimal("0.4"), ) # If tx3b was accepted, tx3c won't look like a replacement, # but make sure it is accepted anyway self.wallet.send_self_transfer( from_node=self.nodes[0], utxo_to_spend=tx2a_utxo, sequence=0, fee=Decimal("0.4"), ) def test_prioritised_transactions(self): # Ensure that fee deltas used via prioritisetransaction are # correctly used by replacement logic # 1. Check that feeperkb uses modified fees tx0_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN)) tx1a_txid = self.wallet.send_self_transfer( from_node=self.nodes[0], utxo_to_spend=tx0_outpoint, sequence=0, fee=Decimal("0.1"), )["txid"] # Higher fee, but the actual fee per KB is much lower. tx1b_hex = self.wallet.create_self_transfer_multi( utxos_to_spend=[tx0_outpoint], sequence=0, num_outputs=100, amount_per_output=int(0.00001 * COIN), )["hex"] # Verify tx1b cannot replace tx1a. assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx1b_hex, 0) # Use prioritisetransaction to set tx1a's fee to 0. self.nodes[0].prioritisetransaction(txid=tx1a_txid, fee_delta=int(-0.1 * COIN)) # Now tx1b should be able to replace tx1a tx1b_txid = self.nodes[0].sendrawtransaction(tx1b_hex, 0) assert tx1b_txid in self.nodes[0].getrawmempool() # 2. Check that absolute fee checks use modified fee. tx1_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN)) # tx2a self.wallet.send_self_transfer( from_node=self.nodes[0], utxo_to_spend=tx1_outpoint, sequence=0, fee=Decimal("0.1"), ) # Lower fee, but we'll prioritise it tx2b = self.wallet.create_self_transfer( utxo_to_spend=tx1_outpoint, sequence=0, fee=Decimal("0.09"), ) # Verify tx2b cannot replace tx2a. assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx2b["hex"], 0) # Now prioritise tx2b to have a higher modified fee self.nodes[0].prioritisetransaction(txid=tx2b["txid"], fee_delta=int(0.1 * COIN)) # tx2b should now be accepted tx2b_txid = self.nodes[0].sendrawtransaction(tx2b["hex"], 0) assert tx2b_txid in self.nodes[0].getrawmempool() def test_rpc(self): us0 = self.wallet.get_utxo() ins = [us0] outs = {ADDRESS_BCRT1_UNSPENDABLE: Decimal(1.0000000)} rawtx0 = self.nodes[0].createrawtransaction(ins, outs, 0, True) rawtx1 = self.nodes[0].createrawtransaction(ins, outs, 0, False) json0 = self.nodes[0].decoderawtransaction(rawtx0) json1 = self.nodes[0].decoderawtransaction(rawtx1) assert_equal(json0["vin"][0]["sequence"], 4294967293) assert_equal(json1["vin"][0]["sequence"], 4294967295) if self.is_specified_wallet_compiled(): self.init_wallet(node=0) rawtx2 = self.nodes[0].createrawtransaction([], outs) frawtx2a = self.nodes[0].fundrawtransaction( rawtx2, {"replaceable": True}) frawtx2b = self.nodes[0].fundrawtransaction( rawtx2, {"replaceable": False}) json0 = self.nodes[0].decoderawtransaction(frawtx2a['hex']) json1 = self.nodes[0].decoderawtransaction(frawtx2b['hex']) assert_equal(json0["vin"][0]["sequence"], 4294967293) assert_equal(json1["vin"][0]["sequence"], 4294967294) def test_no_inherited_signaling(self): confirmed_utxo = self.wallet.get_utxo() # Create an explicitly opt-in parent transaction optin_parent_tx = self.wallet.send_self_transfer( from_node=self.nodes[0], utxo_to_spend=confirmed_utxo, sequence=BIP125_SEQUENCE_NUMBER, fee_rate=Decimal('0.01'), ) assert_equal( True, self.nodes[0].getmempoolentry( optin_parent_tx['txid'])['bip125-replaceable']) replacement_parent_tx = self.wallet.create_self_transfer( utxo_to_spend=confirmed_utxo, sequence=BIP125_SEQUENCE_NUMBER, fee_rate=Decimal('0.02'), ) # Test if parent tx can be replaced. res = self.nodes[0].testmempoolaccept( rawtxs=[replacement_parent_tx['hex']])[0] # Parent can be replaced. assert_equal(res['allowed'], True) # Create an opt-out child tx spending the opt-in parent parent_utxo = self.wallet.get_utxo(txid=optin_parent_tx['txid']) optout_child_tx = self.wallet.send_self_transfer( from_node=self.nodes[0], utxo_to_spend=parent_utxo, sequence=SEQUENCE_FINAL, fee_rate=Decimal('0.01'), ) # Reports true due to inheritance assert_equal( True, self.nodes[0].getmempoolentry( optout_child_tx['txid'])['bip125-replaceable']) replacement_child_tx = self.wallet.create_self_transfer( utxo_to_spend=parent_utxo, sequence=SEQUENCE_FINAL, fee_rate=Decimal('0.02'), ) # Broadcast replacement child tx # BIP 125 : # 1. The original transactions signal replaceability explicitly or through inheritance as described in the above # Summary section. # The original transaction (`optout_child_tx`) doesn't signal RBF but its parent (`optin_parent_tx`) does. # The replacement transaction (`replacement_child_tx`) should be able to replace the original transaction. # See CVE-2021-31876 for further explanations. assert_equal( True, self.nodes[0].getmempoolentry( optin_parent_tx['txid'])['bip125-replaceable']) assert_raises_rpc_error(-26, 'txn-mempool-conflict', self.nodes[0].sendrawtransaction, replacement_child_tx["hex"], 0) self.log.info( 'Check that the child tx can still be replaced (via a tx that also replaces the parent)' ) replacement_parent_tx = self.wallet.send_self_transfer( from_node=self.nodes[0], utxo_to_spend=confirmed_utxo, sequence=SEQUENCE_FINAL, fee_rate=Decimal('0.03'), ) # Check that child is removed and update wallet utxo state assert_raises_rpc_error(-5, 'Transaction not in mempool', self.nodes[0].getmempoolentry, optout_child_tx['txid']) self.wallet.get_utxo(txid=optout_child_tx['txid']) def test_replacement_relay_fee(self): tx = self.wallet.send_self_transfer(from_node=self.nodes[0])['tx'] # Higher fee, higher feerate, different txid, but the replacement does not provide a relay # fee conforming to node's `incrementalrelayfee` policy of 1000 sat per KB. assert_equal(self.nodes[0].getmempoolinfo()["incrementalrelayfee"], Decimal("0.00001")) tx.vout[0].nValue -= 1 assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx.serialize().hex()) def test_fullrbf(self): txid = self.wallet.send_self_transfer(from_node=self.nodes[0])['txid'] self.generate(self.nodes[0], 1) confirmed_utxo = self.wallet.get_utxo(txid=txid) self.restart_node(0, extra_args=["-mempoolfullrbf=1"]) # Create an explicitly opt-out transaction optout_tx = self.wallet.send_self_transfer( from_node=self.nodes[0], utxo_to_spend=confirmed_utxo, sequence=SEQUENCE_FINAL, fee_rate=Decimal('0.01'), ) assert_equal( False, self.nodes[0].getmempoolentry( optout_tx['txid'])['bip125-replaceable']) conflicting_tx = self.wallet.create_self_transfer( utxo_to_spend=confirmed_utxo, sequence=SEQUENCE_FINAL, fee_rate=Decimal('0.02'), ) # Send the replacement transaction, conflicting with the optout_tx. self.nodes[0].sendrawtransaction(conflicting_tx['hex'], 0) # Optout_tx is not anymore in the mempool. assert optout_tx['txid'] not in self.nodes[0].getrawmempool()
class RpcCreateMultiSigTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 3 self.supports_cli = False if self.is_bdb_compiled(): self.requires_wallet = True def get_keys(self): self.pub = [] self.priv = [] node0, node1, node2 = self.nodes for _ in range(self.nkeys): k = ECKey() k.generate() self.pub.append(k.get_pubkey().get_bytes().hex()) self.priv.append(bytes_to_wif(k.get_bytes(), k.is_compressed)) if self.is_bdb_compiled(): self.final = node2.getnewaddress() else: self.final = getnewdestination('bech32')[2] def run_test(self): node0, node1, node2 = self.nodes self.wallet = MiniWallet(test_node=node0) if self.is_bdb_compiled(): self.check_addmultisigaddress_errors() self.log.info('Generating blocks ...') self.generate(self.wallet, 149) self.moved = 0 for self.nkeys in [3, 5]: for self.nsigs in [2, 3]: for self.output_type in ["bech32", "p2sh-segwit", "legacy"]: self.get_keys() self.do_multisig() if self.is_bdb_compiled(): self.checkbalances() # Test mixed compressed and uncompressed pubkeys self.log.info( 'Mixed compressed and uncompressed multisigs are not allowed') pk0, pk1, pk2 = [ getnewdestination('bech32')[0].hex() for _ in range(3) ] # decompress pk2 pk_obj = ECPubKey() pk_obj.set(bytes.fromhex(pk2)) pk_obj.compressed = False pk2 = pk_obj.get_bytes().hex() if self.is_bdb_compiled(): node0.createwallet(wallet_name='wmulti0', disable_private_keys=True) wmulti0 = node0.get_wallet_rpc('wmulti0') # Check all permutations of keys because order matters apparently for keys in itertools.permutations([pk0, pk1, pk2]): # Results should be the same as this legacy one legacy_addr = node0.createmultisig(2, keys, 'legacy')['address'] if self.is_bdb_compiled(): result = wmulti0.addmultisigaddress(2, keys, '', 'legacy') assert_equal(legacy_addr, result['address']) assert 'warnings' not in result # Generate addresses with the segwit types. These should all make legacy addresses err_msg = [ "Unable to make chosen address type, please ensure no uncompressed public keys are present." ] for addr_type in ['bech32', 'p2sh-segwit']: result = self.nodes[0].createmultisig(nrequired=2, keys=keys, address_type=addr_type) assert_equal(legacy_addr, result['address']) assert_equal(result['warnings'], err_msg) if self.is_bdb_compiled(): result = wmulti0.addmultisigaddress(nrequired=2, keys=keys, address_type=addr_type) assert_equal(legacy_addr, result['address']) assert_equal(result['warnings'], err_msg) self.log.info( 'Testing sortedmulti descriptors with BIP 67 test vectors') with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data/rpc_bip67.json'), encoding='utf-8') as f: vectors = json.load(f) for t in vectors: key_str = ','.join(t['keys']) desc = descsum_create('sh(sortedmulti(2,{}))'.format(key_str)) assert_equal(self.nodes[0].deriveaddresses(desc)[0], t['address']) sorted_key_str = ','.join(t['sorted_keys']) sorted_key_desc = descsum_create( 'sh(multi(2,{}))'.format(sorted_key_str)) assert_equal(self.nodes[0].deriveaddresses(sorted_key_desc)[0], t['address']) # Check that bech32m is currently not allowed assert_raises_rpc_error( -5, "createmultisig cannot create bech32m multisig addresses", self.nodes[0].createmultisig, 2, self.pub, "bech32m") def check_addmultisigaddress_errors(self): if self.options.descriptors: return self.log.info( 'Check that addmultisigaddress fails when the private keys are missing' ) addresses = [ self.nodes[1].getnewaddress(address_type='legacy') for _ in range(2) ] assert_raises_rpc_error( -5, 'no full public key for address', lambda: self.nodes[0].addmultisigaddress(nrequired=1, keys=addresses)) for a in addresses: # Importing all addresses should not change the result self.nodes[0].importaddress(a) assert_raises_rpc_error( -5, 'no full public key for address', lambda: self.nodes[0].addmultisigaddress(nrequired=1, keys=addresses)) # Bech32m address type is disallowed for legacy wallets pubs = [ self.nodes[1].getaddressinfo(addr)["pubkey"] for addr in addresses ] assert_raises_rpc_error( -5, "Bech32m multisig addresses cannot be created with legacy wallets", self.nodes[0].addmultisigaddress, 2, pubs, "", "bech32m") def checkbalances(self): node0, node1, node2 = self.nodes self.generate(node0, COINBASE_MATURITY) bal0 = node0.getbalance() bal1 = node1.getbalance() bal2 = node2.getbalance() balw = self.wallet.get_balance() height = node0.getblockchaininfo()["blocks"] assert 150 < height < 350 total = 149 * 50 + (height - 149 - 100) * 25 assert bal1 == 0 assert bal2 == self.moved assert_equal(bal0 + bal1 + bal2 + balw, total) def do_multisig(self): node0, node1, node2 = self.nodes if self.is_bdb_compiled(): if 'wmulti' not in node1.listwallets(): try: node1.loadwallet('wmulti') except JSONRPCException as e: path = os.path.join(self.options.tmpdir, "node1", "regtest", "wallets", "wmulti") if e.error[ 'code'] == -18 and "Wallet file verification failed. Failed to load database path '{}'. Path does not exist.".format( path) in e.error['message']: node1.createwallet(wallet_name='wmulti', disable_private_keys=True) else: raise wmulti = node1.get_wallet_rpc('wmulti') # Construct the expected descriptor desc = 'multi({},{})'.format(self.nsigs, ','.join(self.pub)) if self.output_type == 'legacy': desc = 'sh({})'.format(desc) elif self.output_type == 'p2sh-segwit': desc = 'sh(wsh({}))'.format(desc) elif self.output_type == 'bech32': desc = 'wsh({})'.format(desc) desc = descsum_create(desc) msig = node2.createmultisig(self.nsigs, self.pub, self.output_type) assert 'warnings' not in msig madd = msig["address"] mredeem = msig["redeemScript"] assert_equal(desc, msig['descriptor']) if self.output_type == 'bech32': assert madd[0:4] == "bcrt" # actually a bech32 address if self.is_bdb_compiled(): # compare against addmultisigaddress msigw = wmulti.addmultisigaddress(self.nsigs, self.pub, None, self.output_type) maddw = msigw["address"] mredeemw = msigw["redeemScript"] assert_equal(desc, drop_origins(msigw['descriptor'])) # addmultisigiaddress and createmultisig work the same assert maddw == madd assert mredeemw == mredeem wmulti.unloadwallet() spk = bytes.fromhex(node0.validateaddress(madd)["scriptPubKey"]) txid, _ = self.wallet.send_to(from_node=self.nodes[0], scriptPubKey=spk, amount=1300) tx = node0.getrawtransaction(txid, True) vout = [ v["n"] for v in tx["vout"] if madd == v["scriptPubKey"]["address"] ] assert len(vout) == 1 vout = vout[0] scriptPubKey = tx["vout"][vout]["scriptPubKey"]["hex"] value = tx["vout"][vout]["value"] prevtxs = [{ "txid": txid, "vout": vout, "scriptPubKey": scriptPubKey, "redeemScript": mredeem, "amount": value }] self.generate(node0, 1) outval = value - decimal.Decimal("0.00001000") rawtx = node2.createrawtransaction([{ "txid": txid, "vout": vout }], [{ self.final: outval }]) prevtx_err = dict(prevtxs[0]) del prevtx_err["redeemScript"] assert_raises_rpc_error(-8, "Missing redeemScript/witnessScript", node2.signrawtransactionwithkey, rawtx, self.priv[0:self.nsigs - 1], [prevtx_err]) # if witnessScript specified, all ok prevtx_err["witnessScript"] = prevtxs[0]["redeemScript"] node2.signrawtransactionwithkey(rawtx, self.priv[0:self.nsigs - 1], [prevtx_err]) # both specified, also ok prevtx_err["redeemScript"] = prevtxs[0]["redeemScript"] node2.signrawtransactionwithkey(rawtx, self.priv[0:self.nsigs - 1], [prevtx_err]) # redeemScript mismatch to witnessScript prevtx_err["redeemScript"] = "6a" # OP_RETURN assert_raises_rpc_error( -8, "redeemScript does not correspond to witnessScript", node2.signrawtransactionwithkey, rawtx, self.priv[0:self.nsigs - 1], [prevtx_err]) # redeemScript does not match scriptPubKey del prevtx_err["witnessScript"] assert_raises_rpc_error( -8, "redeemScript/witnessScript does not match scriptPubKey", node2.signrawtransactionwithkey, rawtx, self.priv[0:self.nsigs - 1], [prevtx_err]) # witnessScript does not match scriptPubKey prevtx_err["witnessScript"] = prevtx_err["redeemScript"] del prevtx_err["redeemScript"] assert_raises_rpc_error( -8, "redeemScript/witnessScript does not match scriptPubKey", node2.signrawtransactionwithkey, rawtx, self.priv[0:self.nsigs - 1], [prevtx_err]) rawtx2 = node2.signrawtransactionwithkey(rawtx, self.priv[0:self.nsigs - 1], prevtxs) rawtx3 = node2.signrawtransactionwithkey(rawtx2["hex"], [self.priv[-1]], prevtxs) self.moved += outval tx = node0.sendrawtransaction(rawtx3["hex"], 0) blk = self.generate(node0, 1)[0] assert tx in node0.getblock(blk)["tx"] txinfo = node0.getrawtransaction(tx, True, blk) self.log.info("n/m=%d/%d %s size=%d vsize=%d weight=%d" % (self.nsigs, self.nkeys, self.output_type, txinfo["size"], txinfo["vsize"], txinfo["weight"]))