|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# Copyright (c) 2022 The Bitcoin Core developers |
| 3 | +# Distributed under the MIT software license, see the accompanying |
| 4 | +# file COPYING or http://www.opensource.org/licenses/mit-license.php. |
| 5 | + |
| 6 | +import copy |
| 7 | +from decimal import Decimal |
| 8 | + |
| 9 | +from test_framework.messages import ( |
| 10 | + COIN, |
| 11 | + CTxInWitness, |
| 12 | + CTxOut, |
| 13 | + MAX_BIP125_RBF_SEQUENCE, |
| 14 | +) |
| 15 | +from test_framework.script import ( |
| 16 | + CScript, |
| 17 | + OP_TRUE, |
| 18 | +) |
| 19 | +from test_framework.test_framework import BitcoinTestFramework |
| 20 | +from test_framework.util import ( |
| 21 | + assert_equal, |
| 22 | + assert_raises_rpc_error, |
| 23 | +) |
| 24 | +from test_framework.wallet import ( |
| 25 | + DEFAULT_FEE, |
| 26 | + MiniWallet, |
| 27 | +) |
| 28 | + |
| 29 | +class EphemeralAnchorTest(BitcoinTestFramework): |
| 30 | + def set_test_params(self): |
| 31 | + self.num_nodes = 1 |
| 32 | + self.setup_clean_chain = True |
| 33 | + |
| 34 | + def assert_mempool_contents(self, expected=None, unexpected=None): |
| 35 | + """Assert that all transactions in expected are in the mempool, |
| 36 | + and all transactions in unexpected are not in the mempool. |
| 37 | + """ |
| 38 | + if not expected: |
| 39 | + expected = [] |
| 40 | + if not unexpected: |
| 41 | + unexpected = [] |
| 42 | + assert set(unexpected).isdisjoint(expected) |
| 43 | + mempool = self.nodes[0].getrawmempool(verbose=False) |
| 44 | + for tx in expected: |
| 45 | + assert tx.rehash() in mempool |
| 46 | + for tx in unexpected: |
| 47 | + assert tx.rehash() not in mempool |
| 48 | + |
| 49 | + def insert_additional_outputs(self, parent_result, additional_outputs): |
| 50 | + # Modify transaction as needed to add ephemeral anchor |
| 51 | + parent_tx = parent_result["tx"] |
| 52 | + additional_sum = 0 |
| 53 | + for additional_output in additional_outputs: |
| 54 | + parent_tx.vout.append(additional_output) |
| 55 | + additional_sum += additional_output.nValue |
| 56 | + |
| 57 | + # Steal value from destination and recompute fields |
| 58 | + parent_tx.vout[0].nValue -= additional_sum |
| 59 | + parent_result["txid"] = parent_tx.rehash() |
| 60 | + parent_result["wtxid"] = parent_tx.getwtxid() |
| 61 | + parent_result["hex"] = parent_tx.serialize().hex() |
| 62 | + parent_result["new_utxo"] = {**parent_result["new_utxo"], "txid": parent_result["txid"], "value": Decimal(parent_tx.vout[0].nValue)/COIN} |
| 63 | + |
| 64 | + |
| 65 | + def spend_ephemeral_anchor_witness(self, child_result, child_inputs): |
| 66 | + child_tx = child_result["tx"] |
| 67 | + child_tx.wit.vtxinwit = [copy.deepcopy(child_tx.wit.vtxinwit[0]) if "anchor" not in x else CTxInWitness() for x in child_inputs] |
| 68 | + child_result["hex"] = child_tx.serialize().hex() |
| 69 | + |
| 70 | + |
| 71 | + def create_simple_package(self, parent_coin, parent_fee=0, child_fee=DEFAULT_FEE, spend_anchor=1, additional_outputs=None): |
| 72 | + """Create a 1 parent 1 child package using the coin passed in as the parent's input. The |
| 73 | + parent has 1 output, used to fund 1 child transaction. |
| 74 | + All transactions signal BIP125 replaceability, but nSequence changes based on self.ctr. This |
| 75 | + prevents identical txids between packages when the parents spend the same coin and have the |
| 76 | + same fee (i.e. 0sat). |
| 77 | +
|
| 78 | + returns tuple (hex serialized txns, CTransaction objects) |
| 79 | + """ |
| 80 | + |
| 81 | + if additional_outputs is None: |
| 82 | + additional_outputs=[CTxOut(0, CScript([OP_TRUE]))] |
| 83 | + |
| 84 | + child_inputs = [] |
| 85 | + self.ctr += 1 |
| 86 | + # Use fee_rate=0 because create_self_transfer will use the default fee_rate value otherwise. |
| 87 | + # Passing in fee>0 overrides fee_rate, so this still works for non-zero parent_fee. |
| 88 | + parent_result = self.wallet.create_self_transfer( |
| 89 | + fee_rate=0, |
| 90 | + fee=parent_fee, |
| 91 | + utxo_to_spend=parent_coin, |
| 92 | + sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr, |
| 93 | + ) |
| 94 | + |
| 95 | + self.insert_additional_outputs(parent_result, additional_outputs) |
| 96 | + |
| 97 | + # Add inputs to child, depending on spend arg |
| 98 | + child_inputs.append(parent_result["new_utxo"]) |
| 99 | + if spend_anchor: |
| 100 | + for vout, output in enumerate(additional_outputs): |
| 101 | + child_inputs.append({**parent_result["new_utxo"], 'vout': 1+vout, 'value': Decimal(output.nValue)/COIN, 'anchor': True}) |
| 102 | + |
| 103 | + |
| 104 | + child_result = self.wallet.create_self_transfer_multi( |
| 105 | + utxos_to_spend=child_inputs, |
| 106 | + num_outputs=1, |
| 107 | + fee_per_output=int(child_fee * COIN), |
| 108 | + sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr, |
| 109 | + ) |
| 110 | + |
| 111 | + if spend_anchor: |
| 112 | + self.spend_ephemeral_anchor_witness(child_result, child_inputs) |
| 113 | + |
| 114 | + package_hex = [parent_result["hex"], child_result["hex"]] |
| 115 | + package_txns = [parent_result["tx"], child_result["tx"]] |
| 116 | + return package_hex, package_txns |
| 117 | + |
| 118 | + def run_test(self): |
| 119 | + # Counter used to count the number of times we constructed packages. Since we're constructing parent transactions with the same |
| 120 | + # coins (to create conflicts), and giving them the same fee (i.e. 0, since their respective children are paying), we might |
| 121 | + # accidentally just create the exact same transaction again. To prevent this, set nSequences to MAX_BIP125_RBF_SEQUENCE - self.ctr. |
| 122 | + self.ctr = 0 |
| 123 | + |
| 124 | + self.log.info("Generate blocks to create UTXOs") |
| 125 | + node = self.nodes[0] |
| 126 | + self.wallet = MiniWallet(node) |
| 127 | + self.generate(self.wallet, 160) |
| 128 | + self.coins = self.wallet.get_utxos(mark_as_spent=False) |
| 129 | + # Mature coinbase transactions |
| 130 | + self.generate(self.wallet, 100) |
| 131 | + self.address = self.wallet.get_address() |
| 132 | + |
| 133 | + self.test_fee_having_parent() |
| 134 | + self.test_multianchor() |
| 135 | + self.test_nonzero_anchor() |
| 136 | + self.test_prioritise_parent() |
| 137 | + |
| 138 | + def test_fee_having_parent(self): |
| 139 | + self.log.info("Test that a transaction with ephemeral anchor may not have base fee") |
| 140 | + node = self.nodes[0] |
| 141 | + # Reuse the same coins so that the transactions conflict with one another. |
| 142 | + parent_coin = self.coins[-1] |
| 143 | + del self.coins[-1] |
| 144 | + |
| 145 | + package_hex0, package_txns0 = self.create_simple_package(parent_coin=parent_coin, parent_fee=1, child_fee=DEFAULT_FEE) |
| 146 | + assert_raises_rpc_error(-26, "invalid-ephemeral-fee", node.submitpackage, package_hex0) |
| 147 | + assert_equal(node.getrawmempool(), []) |
| 148 | + |
| 149 | + # But works with no parent fee |
| 150 | + package_hex1, package_txns1 = self.create_simple_package(parent_coin=parent_coin, parent_fee=0, child_fee=DEFAULT_FEE) |
| 151 | + node.submitpackage(package_hex1) |
| 152 | + self.assert_mempool_contents(expected=package_txns1, unexpected=[]) |
| 153 | + |
| 154 | + self.generate(node, 1) |
| 155 | + |
| 156 | + def test_multianchor(self): |
| 157 | + self.log.info("Test that a transaction with multiple ephemeral anchors is nonstandard") |
| 158 | + node = self.nodes[0] |
| 159 | + # Reuse the same coins so that the transactions conflict with one another. |
| 160 | + parent_coin = self.coins[-1] |
| 161 | + del self.coins[-1] |
| 162 | + |
| 163 | + package_hex0, package_txns0 = self.create_simple_package(parent_coin=parent_coin, parent_fee=0, child_fee=DEFAULT_FEE, additional_outputs=[CTxOut(0, CScript([OP_TRUE]))] * 2) |
| 164 | + assert_raises_rpc_error(-26, "too-many-ephemeral-anchors", node.submitpackage, package_hex0) |
| 165 | + assert_equal(node.getrawmempool(), []) |
| 166 | + |
| 167 | + self.generate(node, 1) |
| 168 | + |
| 169 | + def test_nonzero_anchor(self): |
| 170 | + def inner_test_anchor_value(output_value): |
| 171 | + node = self.nodes[0] |
| 172 | + # Reuse the same coins so that the transactions conflict with one another. |
| 173 | + parent_coin = self.coins[-1] |
| 174 | + del self.coins[-1] |
| 175 | + |
| 176 | + package_hex0, package_txns0 = self.create_simple_package(parent_coin=parent_coin, parent_fee=0, child_fee=DEFAULT_FEE, additional_outputs=[CTxOut(output_value, CScript([OP_TRUE]))]) |
| 177 | + node.submitpackage(package_hex0) |
| 178 | + self.assert_mempool_contents(expected=package_txns0, unexpected=[]) |
| 179 | + |
| 180 | + self.generate(node, 1) |
| 181 | + |
| 182 | + self.log.info("Test that a transaction with ephemeral anchor may have any otherwise legal satoshi value") |
| 183 | + for i in range(5): |
| 184 | + inner_test_anchor_value(int(i*COIN/4)) |
| 185 | + |
| 186 | + def test_prioritise_parent(self): |
| 187 | + self.log.info("Test that prioritizing a parent transaction with ephemeral anchor doesn't cause mempool rejection due to non-0 parent fee") |
| 188 | + node = self.nodes[0] |
| 189 | + # Reuse the same coins so that the transactions conflict with one another. |
| 190 | + parent_coin = self.coins[-1] |
| 191 | + del self.coins[-1] |
| 192 | + |
| 193 | + # De-prioritising to 0-fee doesn't matter; it's just the base fee that matters |
| 194 | + package_hex0, package_txns0 = self.create_simple_package(parent_coin=parent_coin, parent_fee=1, child_fee=DEFAULT_FEE) |
| 195 | + parent_txid = node.decoderawtransaction(package_hex0[0])['txid'] |
| 196 | + node.prioritisetransaction(txid=parent_txid, dummy=0, fee_delta=COIN) |
| 197 | + assert_raises_rpc_error(-26, "invalid-ephemeral-fee", node.submitpackage, package_hex0) |
| 198 | + assert_equal(node.getrawmempool(), []) |
| 199 | + |
| 200 | + # Also doesn't make it invalid if applied to the parent |
| 201 | + package_hex1, package_txns1 = self.create_simple_package(parent_coin=parent_coin, parent_fee=0, child_fee=DEFAULT_FEE) |
| 202 | + parent_txid = node.decoderawtransaction(package_hex1[0])['txid'] |
| 203 | + node.prioritisetransaction(txid=parent_txid, dummy=0, fee_delta=COIN) |
| 204 | + node.submitpackage(package_hex1) |
| 205 | + self.assert_mempool_contents(expected=package_txns1, unexpected=[]) |
| 206 | + |
| 207 | + self.generate(node, 1) |
| 208 | + |
| 209 | + |
| 210 | +if __name__ == "__main__": |
| 211 | + EphemeralAnchorTest().main() |
0 commit comments