def run_test(self): wallet = MiniWallet(self.nodes[0]) # Start with a 200 block chain assert_equal(self.nodes[0].getblockcount(), 200) self.log.info("Add 4 coinbase utxos to the miniwallet") # Block 76 contains the first spendable coinbase txs. first_block = 76 wallet.rescan_utxos() # Three scenarios for re-orging coinbase spends in the memory pool: # 1. Direct coinbase spend : spend_1 # 2. Indirect (coinbase spend in chain, child in mempool) : spend_2 and spend_2_1 # 3. Indirect (coinbase and child both in chain) : spend_3 and spend_3_1 # Use invalidateblock to make all of the above coinbase spends invalid (immature coinbase), # and make sure the mempool code behaves correctly. b = [ self.nodes[0].getblockhash(n) for n in range(first_block, first_block + 4) ] coinbase_txids = [self.nodes[0].getblock(h)['tx'][0] for h in b] utxo_1 = wallet.get_utxo(txid=coinbase_txids[1]) utxo_2 = wallet.get_utxo(txid=coinbase_txids[2]) utxo_3 = wallet.get_utxo(txid=coinbase_txids[3]) self.log.info( "Create three transactions spending from coinbase utxos: spend_1, spend_2, spend_3" ) spend_1 = wallet.create_self_transfer(from_node=self.nodes[0], utxo_to_spend=utxo_1) spend_2 = wallet.create_self_transfer(from_node=self.nodes[0], utxo_to_spend=utxo_2) spend_3 = wallet.create_self_transfer(from_node=self.nodes[0], utxo_to_spend=utxo_3) self.log.info( "Create another transaction which is time-locked to two blocks in the future" ) utxo = wallet.get_utxo(txid=coinbase_txids[0]) timelock_tx = wallet.create_self_transfer( from_node=self.nodes[0], utxo_to_spend=utxo, mempool_valid=False, locktime=self.nodes[0].getblockcount() + 2)['hex'] self.log.info( "Check that the time-locked transaction is too immature to spend") assert_raises_rpc_error(-26, "non-final", self.nodes[0].sendrawtransaction, timelock_tx) self.log.info("Broadcast and mine spend_2 and spend_3") wallet.sendrawtransaction(from_node=self.nodes[0], tx_hex=spend_2['hex']) wallet.sendrawtransaction(from_node=self.nodes[0], tx_hex=spend_3['hex']) self.log.info("Generate a block") self.generate(self.nodes[0], 1) self.log.info( "Check that time-locked transaction is still too immature to spend" ) assert_raises_rpc_error(-26, 'non-final', self.nodes[0].sendrawtransaction, timelock_tx) self.log.info("Create spend_2_1 and spend_3_1") spend_2_utxo = wallet.get_utxo(txid=spend_2['txid']) spend_2_1 = wallet.create_self_transfer(from_node=self.nodes[0], utxo_to_spend=spend_2_utxo) spend_3_utxo = wallet.get_utxo(txid=spend_3['txid']) spend_3_1 = wallet.create_self_transfer(from_node=self.nodes[0], utxo_to_spend=spend_3_utxo) self.log.info("Broadcast and mine spend_3_1") spend_3_1_id = self.nodes[0].sendrawtransaction(spend_3_1['hex']) self.log.info("Generate a block") last_block = self.generate(self.nodes[0], 1) # generate() implicitly syncs blocks, so that peer 1 gets the block before timelock_tx # Otherwise, peer 1 would put the timelock_tx in m_recent_rejects self.log.info("The time-locked transaction can now be spent") timelock_tx_id = self.nodes[0].sendrawtransaction(timelock_tx) self.log.info("Add spend_1 and spend_2_1 to the mempool") spend_1_id = self.nodes[0].sendrawtransaction(spend_1['hex']) spend_2_1_id = self.nodes[0].sendrawtransaction(spend_2_1['hex']) assert_equal(set(self.nodes[0].getrawmempool()), {spend_1_id, spend_2_1_id, timelock_tx_id}) self.sync_all() self.log.info("invalidate the last block") for node in self.nodes: node.invalidateblock(last_block[0]) self.log.info( "The time-locked transaction is now too immature and has been removed from the mempool" ) self.log.info( "spend_3_1 has been re-orged out of the chain and is back in the mempool" ) assert_equal(set(self.nodes[0].getrawmempool()), {spend_1_id, spend_2_1_id, spend_3_1_id}) self.log.info( "Use invalidateblock to re-org back and make all those coinbase spends immature/invalid" ) b = self.nodes[0].getblockhash(first_block + 100) for node in self.nodes: node.invalidateblock(b) self.log.info("Check that the mempool is empty") assert_equal(set(self.nodes[0].getrawmempool()), set()) self.sync_all()
class MempoolAcceptanceTest(UmkoinTestFramework): def set_test_params(self): self.num_nodes = 1 self.extra_args = [[ '-txindex','-permitbaremultisig=0', ]] * self.num_nodes self.supports_cli = False def check_mempool_result(self, result_expected, *args, **kwargs): """Wrapper to check result of testmempoolaccept on node_0's mempool""" result_test = self.nodes[0].testmempoolaccept(*args, **kwargs) for r in result_test: r.pop('wtxid') # Skip check for now assert_equal(result_expected, result_test) assert_equal(self.nodes[0].getmempoolinfo()['size'], self.mempool_size) # Must not change mempool state def run_test(self): node = self.nodes[0] self.wallet = MiniWallet(node) self.wallet.rescan_utxos() self.log.info('Start with empty mempool, and 200 blocks') self.mempool_size = 0 assert_equal(node.getblockcount(), 200) assert_equal(node.getmempoolinfo()['size'], self.mempool_size) self.log.info('Should not accept garbage to testmempoolaccept') assert_raises_rpc_error(-3, 'Expected type array, got string', lambda: node.testmempoolaccept(rawtxs='ff00baar')) assert_raises_rpc_error(-8, 'Array must contain between 1 and 25 transactions.', lambda: node.testmempoolaccept(rawtxs=['ff22']*26)) assert_raises_rpc_error(-8, 'Array must contain between 1 and 25 transactions.', lambda: node.testmempoolaccept(rawtxs=[])) assert_raises_rpc_error(-22, 'TX decode failed', lambda: node.testmempoolaccept(rawtxs=['ff00baar'])) self.log.info('A transaction already in the blockchain') tx = self.wallet.create_self_transfer()['tx'] # Pick a random coin(base) to spend tx.vout.append(deepcopy(tx.vout[0])) tx.vout[0].nValue = int(0.3 * COIN) tx.vout[1].nValue = int(49 * COIN) raw_tx_in_block = tx.serialize().hex() txid_in_block = self.wallet.sendrawtransaction(from_node=node, tx_hex=raw_tx_in_block, maxfeerate=0) self.generate(node, 1) self.mempool_size = 0 self.check_mempool_result( result_expected=[{'txid': txid_in_block, 'allowed': False, 'reject-reason': 'txn-already-known'}], rawtxs=[raw_tx_in_block], ) self.log.info('A transaction not in the mempool') fee = Decimal('0.000007') utxo_to_spend = self.wallet.get_utxo(txid=txid_in_block) # use 0.3 UMK UTXO tx = self.wallet.create_self_transfer(utxo_to_spend=utxo_to_spend, sequence=BIP125_SEQUENCE_NUMBER)['tx'] tx.vout[0].nValue = int((Decimal('0.3') - fee) * COIN) raw_tx_0 = tx.serialize().hex() txid_0 = tx.rehash() self.check_mempool_result( result_expected=[{'txid': txid_0, 'allowed': True, 'vsize': tx.get_vsize(), 'fees': {'base': fee}}], rawtxs=[raw_tx_0], ) self.log.info('A final transaction not in the mempool') output_amount = Decimal('0.025') tx = self.wallet.create_self_transfer( sequence=SEQUENCE_FINAL, locktime=node.getblockcount() + 2000, # Can be anything )['tx'] tx.vout[0].nValue = int(output_amount * COIN) raw_tx_final = tx.serialize().hex() tx = tx_from_hex(raw_tx_final) fee_expected = Decimal('50.0') - output_amount self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': True, 'vsize': tx.get_vsize(), 'fees': {'base': fee_expected}}], rawtxs=[tx.serialize().hex()], maxfeerate=0, ) node.sendrawtransaction(hexstring=raw_tx_final, maxfeerate=0) self.mempool_size += 1 self.log.info('A transaction in the mempool') node.sendrawtransaction(hexstring=raw_tx_0) self.mempool_size += 1 self.check_mempool_result( result_expected=[{'txid': txid_0, 'allowed': False, 'reject-reason': 'txn-already-in-mempool'}], rawtxs=[raw_tx_0], ) self.log.info('A transaction that replaces a mempool transaction') tx = tx_from_hex(raw_tx_0) tx.vout[0].nValue -= int(fee * COIN) # Double the fee tx.vin[0].nSequence = BIP125_SEQUENCE_NUMBER + 1 # Now, opt out of RBF raw_tx_0 = tx.serialize().hex() txid_0 = tx.rehash() self.check_mempool_result( result_expected=[{'txid': txid_0, 'allowed': True, 'vsize': tx.get_vsize(), 'fees': {'base': (2 * fee)}}], rawtxs=[raw_tx_0], ) self.log.info('A transaction that conflicts with an unconfirmed tx') # Send the transaction that replaces the mempool transaction and opts out of replaceability node.sendrawtransaction(hexstring=tx.serialize().hex(), maxfeerate=0) # take original raw_tx_0 tx = tx_from_hex(raw_tx_0) tx.vout[0].nValue -= int(4 * fee * COIN) # Set more fee self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'txn-mempool-conflict'}], rawtxs=[tx.serialize().hex()], maxfeerate=0, ) self.log.info('A transaction with missing inputs, that never existed') tx = tx_from_hex(raw_tx_0) tx.vin[0].prevout = COutPoint(hash=int('ff' * 32, 16), n=14) self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'missing-inputs'}], rawtxs=[tx.serialize().hex()], ) self.log.info('A transaction with missing inputs, that existed once in the past') tx = tx_from_hex(raw_tx_0) tx.vin[0].prevout.n = 1 # Set vout to 1, to spend the other outpoint (49 coins) of the in-chain-tx we want to double spend raw_tx_1 = tx.serialize().hex() txid_1 = node.sendrawtransaction(hexstring=raw_tx_1, maxfeerate=0) # Now spend both to "clearly hide" the outputs, ie. remove the coins from the utxo set by spending them tx = self.wallet.create_self_transfer()['tx'] tx.vin.append(deepcopy(tx.vin[0])) tx.wit.vtxinwit.append(deepcopy(tx.wit.vtxinwit[0])) tx.vin[0].prevout = COutPoint(hash=int(txid_0, 16), n=0) tx.vin[1].prevout = COutPoint(hash=int(txid_1, 16), n=0) tx.vout[0].nValue = int(0.1 * COIN) raw_tx_spend_both = tx.serialize().hex() txid_spend_both = self.wallet.sendrawtransaction(from_node=node, tx_hex=raw_tx_spend_both, maxfeerate=0) self.generate(node, 1) self.mempool_size = 0 # Now see if we can add the coins back to the utxo set by sending the exact txs again self.check_mempool_result( result_expected=[{'txid': txid_0, 'allowed': False, 'reject-reason': 'missing-inputs'}], rawtxs=[raw_tx_0], ) self.check_mempool_result( result_expected=[{'txid': txid_1, 'allowed': False, 'reject-reason': 'missing-inputs'}], rawtxs=[raw_tx_1], ) self.log.info('Create a "reference" tx for later use') utxo_to_spend = self.wallet.get_utxo(txid=txid_spend_both) tx = self.wallet.create_self_transfer(utxo_to_spend=utxo_to_spend, sequence=SEQUENCE_FINAL)['tx'] tx.vout[0].nValue = int(0.05 * COIN) raw_tx_reference = tx.serialize().hex() # Reference tx should be valid on itself self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': True, 'vsize': tx.get_vsize(), 'fees': { 'base': Decimal('0.1') - Decimal('0.05')}}], rawtxs=[tx.serialize().hex()], maxfeerate=0, ) self.log.info('A transaction with no outputs') tx = tx_from_hex(raw_tx_reference) tx.vout = [] self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'bad-txns-vout-empty'}], rawtxs=[tx.serialize().hex()], ) self.log.info('A really large transaction') tx = tx_from_hex(raw_tx_reference) tx.vin = [tx.vin[0]] * math.ceil(MAX_BLOCK_WEIGHT // 4 / len(tx.vin[0].serialize())) self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'bad-txns-oversize'}], rawtxs=[tx.serialize().hex()], ) self.log.info('A transaction with negative output value') tx = tx_from_hex(raw_tx_reference) tx.vout[0].nValue *= -1 self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'bad-txns-vout-negative'}], rawtxs=[tx.serialize().hex()], ) # The following two validations prevent overflow of the output amounts (see CVE-2010-5139). self.log.info('A transaction with too large output value') tx = tx_from_hex(raw_tx_reference) tx.vout[0].nValue = MAX_MONEY + 1 self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'bad-txns-vout-toolarge'}], rawtxs=[tx.serialize().hex()], ) self.log.info('A transaction with too large sum of output values') tx = tx_from_hex(raw_tx_reference) tx.vout = [tx.vout[0]] * 2 tx.vout[0].nValue = MAX_MONEY self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'bad-txns-txouttotal-toolarge'}], rawtxs=[tx.serialize().hex()], ) self.log.info('A transaction with duplicate inputs') tx = tx_from_hex(raw_tx_reference) tx.vin = [tx.vin[0]] * 2 self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'bad-txns-inputs-duplicate'}], rawtxs=[tx.serialize().hex()], ) self.log.info('A non-coinbase transaction with coinbase-like outpoint') tx = tx_from_hex(raw_tx_reference) tx.vin.append(CTxIn(COutPoint(hash=0, n=0xffffffff))) self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'bad-txns-prevout-null'}], rawtxs=[tx.serialize().hex()], ) self.log.info('A coinbase transaction') # Pick the input of the first tx we created, so it has to be a coinbase tx raw_tx_coinbase_spent = node.getrawtransaction(txid=node.decoderawtransaction(hexstring=raw_tx_in_block)['vin'][0]['txid']) tx = tx_from_hex(raw_tx_coinbase_spent) self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'coinbase'}], rawtxs=[tx.serialize().hex()], ) self.log.info('Some nonstandard transactions') tx = tx_from_hex(raw_tx_reference) tx.nVersion = 3 # A version currently non-standard self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'version'}], rawtxs=[tx.serialize().hex()], ) tx = tx_from_hex(raw_tx_reference) tx.vout[0].scriptPubKey = CScript([OP_0]) # Some non-standard script self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'scriptpubkey'}], rawtxs=[tx.serialize().hex()], ) tx = tx_from_hex(raw_tx_reference) key = ECKey() key.generate() pubkey = key.get_pubkey().get_bytes() tx.vout[0].scriptPubKey = keys_to_multisig_script([pubkey] * 3, k=2) # Some bare multisig script (2-of-3) self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'bare-multisig'}], rawtxs=[tx.serialize().hex()], ) tx = tx_from_hex(raw_tx_reference) tx.vin[0].scriptSig = CScript([OP_HASH160]) # Some not-pushonly scriptSig self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'scriptsig-not-pushonly'}], rawtxs=[tx.serialize().hex()], ) tx = tx_from_hex(raw_tx_reference) tx.vin[0].scriptSig = CScript([b'a' * 1648]) # Some too large scriptSig (>1650 bytes) self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'scriptsig-size'}], rawtxs=[tx.serialize().hex()], ) tx = tx_from_hex(raw_tx_reference) output_p2sh_burn = CTxOut(nValue=540, scriptPubKey=script_to_p2sh_script(b'burn')) num_scripts = 100000 // len(output_p2sh_burn.serialize()) # Use enough outputs to make the tx too large for our policy tx.vout = [output_p2sh_burn] * num_scripts self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'tx-size'}], rawtxs=[tx.serialize().hex()], ) tx = tx_from_hex(raw_tx_reference) tx.vout[0] = output_p2sh_burn tx.vout[0].nValue -= 1 # Make output smaller, such that it is dust for our policy self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'dust'}], rawtxs=[tx.serialize().hex()], ) tx = tx_from_hex(raw_tx_reference) tx.vout[0].scriptPubKey = CScript([OP_RETURN, b'\xff']) tx.vout = [tx.vout[0]] * 2 self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'multi-op-return'}], rawtxs=[tx.serialize().hex()], ) self.log.info('A timelocked transaction') tx = tx_from_hex(raw_tx_reference) tx.vin[0].nSequence -= 1 # Should be non-max, so locktime is not ignored tx.nLockTime = node.getblockcount() + 1 self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'non-final'}], rawtxs=[tx.serialize().hex()], ) self.log.info('A transaction that is locked by BIP68 sequence logic') tx = tx_from_hex(raw_tx_reference) tx.vin[0].nSequence = 2 # We could include it in the second block mined from now, but not the very next one self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'non-BIP68-final'}], rawtxs=[tx.serialize().hex()], maxfeerate=0, )
class MempoolPackageLimitsTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 self.setup_clean_chain = True def run_test(self): self.wallet = MiniWallet(self.nodes[0]) # Add enough mature utxos to the wallet so that all txs spend confirmed coins. self.generate(self.wallet, 35) self.generate(self.nodes[0], COINBASE_MATURITY) self.test_chain_limits() self.test_desc_count_limits() self.test_desc_count_limits_2() self.test_anc_count_limits() self.test_anc_count_limits_2() self.test_anc_count_limits_bushy() # The node will accept our (nonstandard) extra large OP_RETURN outputs self.restart_node(0, extra_args=["-acceptnonstdtxn=1"]) self.test_anc_size_limits() self.test_desc_size_limits() def test_chain_limits_helper(self, mempool_count, package_count): node = self.nodes[0] assert_equal(0, node.getmempoolinfo()["size"]) chain_hex = [] chaintip_utxo = self.wallet.send_self_transfer_chain( from_node=node, chain_length=mempool_count) # in-package transactions for _ in range(package_count): tx = self.wallet.create_self_transfer(utxo_to_spend=chaintip_utxo) chaintip_utxo = tx["new_utxo"] chain_hex.append(tx["hex"]) testres_too_long = node.testmempoolaccept(rawtxs=chain_hex) for txres in testres_too_long: assert_equal(txres["package-error"], "package-mempool-limits") # Clear mempool and check that the package passes now self.generate(node, 1) assert all([ res["allowed"] for res in node.testmempoolaccept(rawtxs=chain_hex) ]) def test_chain_limits(self): """Create chains from mempool and package transactions that are longer than 25, but only if both in-mempool and in-package transactions are considered together. This checks that both mempool and in-package transactions are taken into account when calculating ancestors/descendant limits. """ self.log.info( "Check that in-package ancestors count for mempool ancestor limits" ) # 24 transactions in the mempool and 2 in the package. The parent in the package has # 24 in-mempool ancestors and 1 in-package descendant. The child has 0 direct parents # in the mempool, but 25 in-mempool and in-package ancestors in total. self.test_chain_limits_helper(24, 2) # 2 transactions in the mempool and 24 in the package. self.test_chain_limits_helper(2, 24) # 13 transactions in the mempool and 13 in the package. self.test_chain_limits_helper(13, 13) def test_desc_count_limits(self): """Create an 'A' shaped package with 24 transactions in the mempool and 2 in the package: M1 ^ ^ M2a M2b . . . . . . M12a ^ ^ M13b ^ ^ Pa Pb The top ancestor in the package exceeds descendant limits but only if the in-mempool and in-package descendants are all considered together (24 including in-mempool descendants and 26 including both package transactions). """ node = self.nodes[0] assert_equal(0, node.getmempoolinfo()["size"]) self.log.info( "Check that in-mempool and in-package descendants are calculated properly in packages" ) # Top parent in mempool, M1 m1_utxos = self.wallet.send_self_transfer_multi( from_node=node, num_outputs=2)['new_utxos'] package_hex = [] # Chain A (M2a... M12a) chain_a_tip_utxo = self.wallet.send_self_transfer_chain( from_node=node, chain_length=11, utxo_to_spend=m1_utxos[0]) # Pa pa_hex = self.wallet.create_self_transfer( utxo_to_spend=chain_a_tip_utxo)["hex"] package_hex.append(pa_hex) # Chain B (M2b... M13b) chain_b_tip_utxo = self.wallet.send_self_transfer_chain( from_node=node, chain_length=12, utxo_to_spend=m1_utxos[1]) # Pb pb_hex = self.wallet.create_self_transfer( utxo_to_spend=chain_b_tip_utxo)["hex"] package_hex.append(pb_hex) assert_equal(24, node.getmempoolinfo()["size"]) assert_equal(2, len(package_hex)) testres_too_long = node.testmempoolaccept(rawtxs=package_hex) for txres in testres_too_long: assert_equal(txres["package-error"], "package-mempool-limits") # Clear mempool and check that the package passes now self.generate(node, 1) assert all([ res["allowed"] for res in node.testmempoolaccept(rawtxs=package_hex) ]) def test_desc_count_limits_2(self): """Create a Package with 24 transaction in mempool and 2 transaction in package: M1 ^ ^ M2 ^ . ^ . ^ . ^ M24 ^ ^ P1 ^ P2 P1 has M1 as a mempool ancestor, P2 has no in-mempool ancestors, but when combined P2 has M1 as an ancestor and M1 exceeds descendant_limits(23 in-mempool descendants + 2 in-package descendants, a total of 26 including itself). """ node = self.nodes[0] package_hex = [] # M1 m1_utxos = self.wallet.send_self_transfer_multi( from_node=node, num_outputs=2)['new_utxos'] # Chain M2...M24 self.wallet.send_self_transfer_chain(from_node=node, chain_length=23, utxo_to_spend=m1_utxos[0]) # P1 p1_tx = self.wallet.create_self_transfer(utxo_to_spend=m1_utxos[1]) package_hex.append(p1_tx["hex"]) # P2 p2_tx = self.wallet.create_self_transfer( utxo_to_spend=p1_tx["new_utxo"]) package_hex.append(p2_tx["hex"]) assert_equal(24, node.getmempoolinfo()["size"]) assert_equal(2, len(package_hex)) testres = node.testmempoolaccept(rawtxs=package_hex) assert_equal(len(testres), len(package_hex)) for txres in testres: assert_equal(txres["package-error"], "package-mempool-limits") # Clear mempool and check that the package passes now self.generate(node, 1) assert all([ res["allowed"] for res in node.testmempoolaccept(rawtxs=package_hex) ]) def test_anc_count_limits(self): """Create a 'V' shaped chain with 24 transactions in the mempool and 3 in the package: M1a M1b ^ ^ M2a M2b . . . . . . M12a M12b ^ ^ Pa Pb ^ ^ Pc The lowest descendant, Pc, exceeds ancestor limits, but only if the in-mempool and in-package ancestors are all considered together. """ node = self.nodes[0] assert_equal(0, node.getmempoolinfo()["size"]) package_hex = [] pc_parent_utxos = [] self.log.info( "Check that in-mempool and in-package ancestors are calculated properly in packages" ) # Two chains of 13 transactions each for _ in range(2): chain_tip_utxo = self.wallet.send_self_transfer_chain( from_node=node, chain_length=12) # Save the 13th transaction for the package tx = self.wallet.create_self_transfer(utxo_to_spend=chain_tip_utxo) package_hex.append(tx["hex"]) pc_parent_utxos.append(tx["new_utxo"]) # Child Pc pc_hex = self.wallet.create_self_transfer_multi( utxos_to_spend=pc_parent_utxos)["hex"] package_hex.append(pc_hex) assert_equal(24, node.getmempoolinfo()["size"]) assert_equal(3, len(package_hex)) testres_too_long = node.testmempoolaccept(rawtxs=package_hex) for txres in testres_too_long: assert_equal(txres["package-error"], "package-mempool-limits") # Clear mempool and check that the package passes now self.generate(node, 1) assert all([ res["allowed"] for res in node.testmempoolaccept(rawtxs=package_hex) ]) def test_anc_count_limits_2(self): """Create a 'Y' shaped chain with 24 transactions in the mempool and 2 in the package: M1a M1b ^ ^ M2a M2b . . . . . . M12a M12b ^ ^ Pc ^ Pd The lowest descendant, Pd, exceeds ancestor limits, but only if the in-mempool and in-package ancestors are all considered together. """ node = self.nodes[0] assert_equal(0, node.getmempoolinfo()["size"]) pc_parent_utxos = [] self.log.info( "Check that in-mempool and in-package ancestors are calculated properly in packages" ) # Two chains of 12 transactions each for _ in range(2): chaintip_utxo = self.wallet.send_self_transfer_chain( from_node=node, chain_length=12) # last 2 transactions will be the parents of Pc pc_parent_utxos.append(chaintip_utxo) # Child Pc pc_tx = self.wallet.create_self_transfer_multi( utxos_to_spend=pc_parent_utxos) # Child Pd pd_tx = self.wallet.create_self_transfer( utxo_to_spend=pc_tx["new_utxos"][0]) assert_equal(24, node.getmempoolinfo()["size"]) testres_too_long = node.testmempoolaccept( rawtxs=[pc_tx["hex"], pd_tx["hex"]]) for txres in testres_too_long: assert_equal(txres["package-error"], "package-mempool-limits") # Clear mempool and check that the package passes now self.generate(node, 1) assert all([ res["allowed"] for res in node.testmempoolaccept( rawtxs=[pc_tx["hex"], pd_tx["hex"]]) ]) def test_anc_count_limits_bushy(self): """Create a tree with 20 transactions in the mempool and 6 in the package: M1...M4 M5...M8 M9...M12 M13...M16 M17...M20 ^ ^ ^ ^ ^ (each with 4 parents) P0 P1 P2 P3 P4 ^ ^ ^ ^ ^ (5 parents) PC Where M(4i+1)...M+(4i+4) are the parents of Pi and P0, P1, P2, P3, and P4 are the parents of PC. P0... P4 individually only have 4 parents each, and PC has no in-mempool parents. But combined, PC has 25 in-mempool and in-package parents. """ node = self.nodes[0] assert_equal(0, node.getmempoolinfo()["size"]) package_hex = [] pc_parent_utxos = [] for _ in range(5): # Make package transactions P0 ... P4 pc_grandparent_utxos = [] for _ in range(4): # Make mempool transactions M(4i+1)...M(4i+4) pc_grandparent_utxos.append( self.wallet.send_self_transfer(from_node=node)["new_utxo"]) # Package transaction Pi pi_tx = self.wallet.create_self_transfer_multi( utxos_to_spend=pc_grandparent_utxos) package_hex.append(pi_tx["hex"]) pc_parent_utxos.append(pi_tx["new_utxos"][0]) # Package transaction PC pc_hex = self.wallet.create_self_transfer_multi( utxos_to_spend=pc_parent_utxos)["hex"] package_hex.append(pc_hex) assert_equal(20, node.getmempoolinfo()["size"]) assert_equal(6, len(package_hex)) testres = node.testmempoolaccept(rawtxs=package_hex) for txres in testres: assert_equal(txres["package-error"], "package-mempool-limits") # Clear mempool and check that the package passes now self.generate(node, 1) assert all([ res["allowed"] for res in node.testmempoolaccept(rawtxs=package_hex) ]) def test_anc_size_limits(self): """Test Case with 2 independent transactions in the mempool and a parent + child in the package, where the package parent is the child of both mempool transactions (30KvB each): A B ^ ^ C ^ D The lowest descendant, D, exceeds ancestor size limits, but only if the in-mempool and in-package ancestors are all considered together. """ node = self.nodes[0] assert_equal(0, node.getmempoolinfo()["size"]) parent_utxos = [] target_weight = WITNESS_SCALE_FACTOR * 1000 * 30 # 30KvB high_fee = Decimal("0.003") # 10 sats/vB self.log.info( "Check that in-mempool and in-package ancestor size limits are calculated properly in packages" ) # Mempool transactions A and B for _ in range(2): bulked_tx = self.wallet.create_self_transfer( target_weight=target_weight) self.wallet.sendrawtransaction(from_node=node, tx_hex=bulked_tx["hex"]) parent_utxos.append(bulked_tx["new_utxo"]) # Package transaction C pc_tx = self.wallet.create_self_transfer_multi( utxos_to_spend=parent_utxos, fee_per_output=int(high_fee * COIN), target_weight=target_weight) # Package transaction D pd_tx = self.wallet.create_self_transfer( utxo_to_spend=pc_tx["new_utxos"][0], target_weight=target_weight) assert_equal(2, node.getmempoolinfo()["size"]) testres_too_heavy = node.testmempoolaccept( rawtxs=[pc_tx["hex"], pd_tx["hex"]]) for txres in testres_too_heavy: assert_equal(txres["package-error"], "package-mempool-limits") # Clear mempool and check that the package passes now self.generate(node, 1) assert all([ res["allowed"] for res in node.testmempoolaccept( rawtxs=[pc_tx["hex"], pd_tx["hex"]]) ]) def test_desc_size_limits(self): """Create 3 mempool transactions and 2 package transactions (25KvB each): Ma ^ ^ Mb Mc ^ ^ Pd Pe The top ancestor in the package exceeds descendant size limits but only if the in-mempool and in-package descendants are all considered together. """ node = self.nodes[0] assert_equal(0, node.getmempoolinfo()["size"]) target_weight = 21 * 1000 * WITNESS_SCALE_FACTOR high_fee = Decimal("0.0021") # 10 sats/vB self.log.info( "Check that in-mempool and in-package descendant sizes are calculated properly in packages" ) # Top parent in mempool, Ma ma_tx = self.wallet.create_self_transfer_multi( num_outputs=2, fee_per_output=int(high_fee / 2 * COIN), target_weight=target_weight) self.wallet.sendrawtransaction(from_node=node, tx_hex=ma_tx["hex"]) package_hex = [] for j in range(2): # Two legs (left and right) # Mempool transaction (Mb and Mc) mempool_tx = self.wallet.create_self_transfer( utxo_to_spend=ma_tx["new_utxos"][j], target_weight=target_weight) self.wallet.sendrawtransaction(from_node=node, tx_hex=mempool_tx["hex"]) # Package transaction (Pd and Pe) package_tx = self.wallet.create_self_transfer( utxo_to_spend=mempool_tx["new_utxo"], target_weight=target_weight) package_hex.append(package_tx["hex"]) assert_equal(3, node.getmempoolinfo()["size"]) assert_equal(2, len(package_hex)) testres_too_heavy = node.testmempoolaccept(rawtxs=package_hex) for txres in testres_too_heavy: assert_equal(txres["package-error"], "package-mempool-limits") # Clear mempool and check that the package passes now self.generate(node, 1) assert all([ res["allowed"] for res in node.testmempoolaccept(rawtxs=package_hex) ])
class ReplaceByFeeTest(BitcoinTestFramework): 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 skip_test_if_missing_module(self): self.skip_if_no_wallet() 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 Assumes that MiniWallet has enough funds to cover the amount and the fixed fee (from it's internal utxos, the one with the largest value is taken). confirmed - txouts created will be confirmed in the blockchain; unconfirmed otherwise. """ # MiniWallet only supports sweeping utxos to its own internal scriptPubKey, so in # order to create an output with arbitrary amount/scriptPubKey, we have to add it # manually after calling the create_self_transfer method. The MiniWallet output's # nValue has to be adapted accordingly (amount and fee deduction). To keep things # simple, we use a fixed fee of 1000 Satoshis here. fee = 1000 tx = self.wallet.create_self_transfer(from_node=node, fee_rate=0, mempool_valid=False)['tx'] assert_greater_than(tx.vout[0].nValue, amount + fee) tx.vout[0].nValue -= (amount + fee) # change output -> MiniWallet tx.vout.append(CTxOut( amount, scriptPubKey)) # desired output -> to be returned txid = self.wallet.sendrawtransaction(from_node=node, tx_hex=tx.serialize().hex()) # 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), 1) 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.nodes[0].listunspent()[0] ins = [us0] outs = {self.nodes[0].getnewaddress(): 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) 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())