|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# Copyright (c) 2023 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 | +"""Test network topology aware pinning""" |
| 7 | + |
| 8 | +import time |
| 9 | + |
| 10 | +from test_framework.key import ( |
| 11 | + ECKey |
| 12 | +) |
| 13 | + |
| 14 | +from test_framework.messages import ( |
| 15 | + CTransaction, |
| 16 | + CTxIn, |
| 17 | + CTxInWitness, |
| 18 | + CTxOut, |
| 19 | + COutPoint, |
| 20 | + sha256, |
| 21 | + COIN, |
| 22 | + tx_from_hex, |
| 23 | +) |
| 24 | + |
| 25 | +from test_framework.util import ( |
| 26 | + assert_equal, |
| 27 | + assert_raises_rpc_error |
| 28 | +) |
| 29 | + |
| 30 | +from test_framework.script import ( |
| 31 | + CScript, |
| 32 | + hash160, |
| 33 | + OP_IF, |
| 34 | + OP_HASH160, |
| 35 | + OP_EQUAL, |
| 36 | + OP_ELSE, |
| 37 | + OP_ENDIF, |
| 38 | + OP_CHECKSIG, |
| 39 | + OP_SWAP, |
| 40 | + OP_SIZE, |
| 41 | + OP_NOTIF, |
| 42 | + OP_DROP, |
| 43 | + OP_CHECKMULTISIG, |
| 44 | + OP_EQUALVERIFY, |
| 45 | + OP_0, |
| 46 | + OP_2, |
| 47 | + OP_TRUE, |
| 48 | + SegwitV0SignatureHash, |
| 49 | + SIGHASH_ALL, |
| 50 | + SIGHASH_SINGLE, |
| 51 | + SIGHASH_ANYONECANPAY, |
| 52 | +) |
| 53 | + |
| 54 | +from test_framework.test_framework import BitcoinTestFramework |
| 55 | + |
| 56 | +from test_framework.wallet import MiniWallet |
| 57 | + |
| 58 | +def get_funding_redeemscript(funder_pubkey, fundee_pubkey): |
| 59 | + return CScript([OP_2, funder_pubkey.get_bytes(), fundee_pubkey.get_bytes(), OP_2, OP_CHECKMULTISIG]) |
| 60 | + |
| 61 | +def generate_funding_chan(wallet, coin, funder_pubkey, fundee_pubkey): |
| 62 | + witness_script = get_funding_redeemscript(funder_pubkey, fundee_pubkey) |
| 63 | + witness_program = sha256(witness_script) |
| 64 | + script_pubkey = CScript([OP_0, witness_program]) |
| 65 | + |
| 66 | + funding_tx = CTransaction() |
| 67 | + funding_tx.vin.append(CTxIn(COutPoint(int(coin['txid'], 16), coin['vout']), b"")) |
| 68 | + funding_tx.vout.append(CTxOut(int(49.99998 * COIN), script_pubkey)) |
| 69 | + funding_tx.rehash() |
| 70 | + |
| 71 | + wallet.sign_tx(funding_tx) |
| 72 | + return funding_tx |
| 73 | + |
| 74 | +def generate_asymmetric_commitment_tx(funding_txid, funding_vout, alice_seckey, bob_seckey, input_amount, input_script): |
| 75 | + |
| 76 | + alice_pubkey = alice_seckey.get_pubkey() |
| 77 | + alice_anchor_output_script = CScript([alice_pubkey.get_bytes(), OP_CHECKSIG]) |
| 78 | + alice_anchor_output_scriptpubkey = CScript([OP_0, sha256(alice_anchor_output_script)]) |
| 79 | + |
| 80 | + bob_pubkey = bob_seckey.get_pubkey() |
| 81 | + bob_anchor_output_script = CScript([bob_pubkey.get_bytes(), OP_CHECKSIG]) |
| 82 | + bob_anchor_output_scriptpubkey = CScript([OP_0, sha256(bob_anchor_output_script)]) |
| 83 | + |
| 84 | + alice_commitment_fee = 250 * 10 |
| 85 | + alice_commitment_tx = CTransaction() |
| 86 | + |
| 87 | + alice_commitment_tx.vin.append(CTxIn(COutPoint(int(funding_txid, 16), funding_vout), b"", 0x0)) |
| 88 | + alice_commitment_tx.vout.append(CTxOut(int(input_amount / 2) - alice_commitment_fee, alice_anchor_output_scriptpubkey)) |
| 89 | + alice_commitment_tx.vout.append(CTxOut(int(input_amount / 2), bob_anchor_output_scriptpubkey)) |
| 90 | + |
| 91 | + sig_hash = SegwitV0SignatureHash(input_script, alice_commitment_tx, 0, SIGHASH_ALL, int(input_amount)) |
| 92 | + funder_sig = alice_seckey.sign_ecdsa(sig_hash) + b'\x01' |
| 93 | + fundee_sig = bob_seckey.sign_ecdsa(sig_hash) + b'\x01' |
| 94 | + |
| 95 | + alice_commitment_tx.wit.vtxinwit.append(CTxInWitness()) |
| 96 | + alice_commitment_tx.wit.vtxinwit[0].scriptWitness.stack = [b'', funder_sig, fundee_sig, input_script] |
| 97 | + alice_commitment_tx.rehash() |
| 98 | + |
| 99 | + bob_commitment_fee = 250 * 10 - 1 #For the asymmetry |
| 100 | + bob_commitment_tx = CTransaction() |
| 101 | + |
| 102 | + bob_commitment_tx.vin.append(CTxIn(COutPoint(int(funding_txid, 16), funding_vout), b"", 0x0)) |
| 103 | + bob_commitment_tx.vout.append(CTxOut(int(input_amount / 2) - bob_commitment_fee, alice_anchor_output_scriptpubkey)) |
| 104 | + bob_commitment_tx.vout.append(CTxOut(int(input_amount / 2), bob_anchor_output_scriptpubkey)) |
| 105 | + |
| 106 | + sig_hash = SegwitV0SignatureHash(input_script, bob_commitment_tx, 0, SIGHASH_ALL, int(input_amount)) |
| 107 | + funder_sig = alice_seckey.sign_ecdsa(sig_hash) + b'\x01' |
| 108 | + fundee_sig = bob_seckey.sign_ecdsa(sig_hash) + b'\x01' |
| 109 | + |
| 110 | + bob_commitment_tx.wit.vtxinwit.append(CTxInWitness()) |
| 111 | + bob_commitment_tx.wit.vtxinwit[0].scriptWitness.stack = [b'', funder_sig, fundee_sig, input_script] |
| 112 | + bob_commitment_tx.rehash() |
| 113 | + |
| 114 | + return (alice_commitment_tx, bob_commitment_tx) |
| 115 | + |
| 116 | +def generate_cpfp_txn(alice_commitment_txid, on_alice_anchor_vout, bob_commitment_txid, on_bob_anchor_vout, alice_seckey, bob_seckey, input_amount_one, input_amount_two): |
| 117 | + |
| 118 | + alice_pubkey = alice_seckey.get_pubkey() |
| 119 | + alice_anchor_output_script = CScript([alice_pubkey.get_bytes(), OP_CHECKSIG]) |
| 120 | + |
| 121 | + alice_cpfp_on_alice_fee = 250 * 10 |
| 122 | + alice_cpfp_on_alice_tx = CTransaction() |
| 123 | + |
| 124 | + alice_cpfp_on_alice_tx.vin.append(CTxIn(COutPoint(int(alice_commitment_txid, 16), on_alice_anchor_vout), b"", 0x0)) |
| 125 | + alice_cpfp_on_alice_tx.vout.append(CTxOut(int(input_amount_one - alice_cpfp_on_alice_fee), alice_anchor_output_script)) |
| 126 | + |
| 127 | + sig_hash = SegwitV0SignatureHash(alice_anchor_output_script, alice_cpfp_on_alice_tx, 0, SIGHASH_ALL, int(input_amount_one)) |
| 128 | + anchor_sig = alice_seckey.sign_ecdsa(sig_hash) + b'\x01' |
| 129 | + |
| 130 | + alice_cpfp_on_alice_tx.wit.vtxinwit.append(CTxInWitness()) |
| 131 | + alice_cpfp_on_alice_tx.wit.vtxinwit[0].scriptWitness.stack = [anchor_sig, alice_anchor_output_script] |
| 132 | + alice_cpfp_on_alice_tx.rehash() |
| 133 | + |
| 134 | + alice_cpfp_on_bob_fee = 250 * 10 |
| 135 | + alice_cpfp_on_bob_tx = CTransaction() |
| 136 | + |
| 137 | + alice_cpfp_on_bob_tx.vin.append(CTxIn(COutPoint(int(bob_commitment_txid, 16), on_bob_anchor_vout), b"", 0x0)) |
| 138 | + alice_cpfp_on_bob_tx.vout.append(CTxOut(int(input_amount_two - alice_cpfp_on_bob_fee), alice_anchor_output_script)) |
| 139 | + |
| 140 | + sig_hash = SegwitV0SignatureHash(alice_anchor_output_script, alice_cpfp_on_bob_tx, 0, SIGHASH_ALL, int(input_amount_two)) |
| 141 | + anchor_sig = alice_seckey.sign_ecdsa(sig_hash) + b'\x01' |
| 142 | + |
| 143 | + alice_cpfp_on_bob_tx.wit.vtxinwit.append(CTxInWitness()) |
| 144 | + alice_cpfp_on_bob_tx.wit.vtxinwit[0].scriptWitness.stack = [anchor_sig, alice_anchor_output_script] |
| 145 | + alice_cpfp_on_bob_tx.rehash() |
| 146 | + |
| 147 | + return (alice_cpfp_on_alice_tx, alice_cpfp_on_bob_tx) |
| 148 | + |
| 149 | +class NetworkTopologyAwarePinning(BitcoinTestFramework): |
| 150 | + |
| 151 | + def set_test_params(self): |
| 152 | + self.num_nodes = 2 |
| 153 | + |
| 154 | + def test_network_topology_aware_pinning(self): |
| 155 | + alice = self.nodes[0] |
| 156 | + alice_seckey = ECKey() |
| 157 | + alice_seckey.generate(True) |
| 158 | + |
| 159 | + bob = self.nodes[1] |
| 160 | + bob_seckey = ECKey() |
| 161 | + bob_seckey.generate(True) |
| 162 | + |
| 163 | + self.generate(alice, 501) |
| 164 | + |
| 165 | + self.sync_all() |
| 166 | + |
| 167 | + self.connect_nodes(0, 1) |
| 168 | + |
| 169 | + coin_1 = self.wallet.get_utxo() |
| 170 | + |
| 171 | + wallet = self.wallet |
| 172 | + |
| 173 | + # Generate funding transaction opening channel between Alice and Bob. |
| 174 | + ab_funding_tx = generate_funding_chan(wallet, coin_1, alice_seckey.get_pubkey(), bob_seckey.get_pubkey()) |
| 175 | + |
| 176 | + # Propagate and confirm funding transaction. |
| 177 | + ab_funding_txid = alice.sendrawtransaction(hexstring=ab_funding_tx.serialize().hex(), maxfeerate=0) |
| 178 | + |
| 179 | + self.sync_all() |
| 180 | + |
| 181 | + assert ab_funding_txid in alice.getrawmempool() |
| 182 | + assert ab_funding_txid in bob.getrawmempool() |
| 183 | + |
| 184 | + # We mine one block the Alice - Bob channel is opened. |
| 185 | + self.generate(alice, 1) |
| 186 | + assert_equal(len(alice.getrawmempool()), 0) |
| 187 | + assert_equal(len(bob.getrawmempool()), 0) |
| 188 | + |
| 189 | + |
| 190 | + # Generate commitment transaction with 2 anchor output |
| 191 | + funding_redeemscript = get_funding_redeemscript(alice_seckey.get_pubkey(), bob_seckey.get_pubkey()) |
| 192 | + input_amount = 49.99998 * COIN |
| 193 | + (alice_commitment_tx, bob_commitment_tx) = generate_asymmetric_commitment_tx(ab_funding_txid, 0, alice_seckey, bob_seckey, input_amount, funding_redeemscript) |
| 194 | + |
| 195 | + self.disconnect_nodes(0, 1) |
| 196 | + |
| 197 | + alice_commitment_txid = alice.sendrawtransaction(hexstring=alice_commitment_tx.serialize().hex(), maxfeerate=0) |
| 198 | + |
| 199 | + assert alice_commitment_txid in alice.getrawmempool() |
| 200 | + assert not alice_commitment_txid in bob.getrawmempool() |
| 201 | + |
| 202 | + input_amount_two = bob_commitment_tx.vout[0].nValue |
| 203 | + |
| 204 | + bob_commitment_txid = bob.sendrawtransaction(hexstring=bob_commitment_tx.serialize().hex(), maxfeerate=0) |
| 205 | + assert bob_commitment_txid in bob.getrawmempool() |
| 206 | + assert not bob_commitment_txid in alice.getrawmempool() |
| 207 | + |
| 208 | + self.connect_nodes(0, 1) |
| 209 | + |
| 210 | + assert alice_commitment_txid in alice.getrawmempool() |
| 211 | + assert bob_commitment_txid in bob.getrawmempool() |
| 212 | + |
| 213 | + input_amount_one = (input_amount / 2) - (250 * 10) |
| 214 | + (alice_cpfp_on_alice, alice_cpfp_on_bob) = generate_cpfp_txn(alice_commitment_txid, 0, bob_commitment_txid, 0, alice_seckey, bob_seckey, input_amount_one, input_amount_two) |
| 215 | + |
| 216 | + alice_cpfp_on_alice_txid = alice.sendrawtransaction(hexstring=alice_cpfp_on_alice.serialize().hex(), maxfeerate=0) |
| 217 | + |
| 218 | + assert alice_cpfp_on_alice_txid in alice.getrawmempool() |
| 219 | + assert not alice_cpfp_on_alice_txid in bob.getrawmempool() |
| 220 | + |
| 221 | + self.log.info("Alice CFPP {} on Alice commitment tx {} propagates inside Alice mempool".format(alice_cpfp_on_alice_txid, alice_commitment_txid)) |
| 222 | + self.log.info("Alice CFPP {} on Alice commitment tx {} does not propagate inside Bob mempool".format(alice_cpfp_on_alice_txid, alice_commitment_txid)) |
| 223 | + |
| 224 | + assert bob.testmempoolaccept([alice_cpfp_on_bob.serialize().hex()])[0]["allowed"] |
| 225 | + |
| 226 | + self.log.info("Alice CFPP {} on Bob commitment tx {} does propagate on top of Bob commitment tx".format(alice_cpfp_on_bob.hash, bob_commitment_txid)) |
| 227 | + |
| 228 | + assert_raises_rpc_error(-25, "bad-txns-inputs-missingorspent", alice.sendrawtransaction, alice_cpfp_on_bob.serialize().hex(), 0) |
| 229 | + |
| 230 | + self.log.info("Alice CPFP {} on Bob commitment tx {} does not propagate from Alice mempool".format(alice_cpfp_on_bob.hash, bob_commitment_txid)) |
| 231 | + |
| 232 | + coin_2 = self.wallet.get_utxo() |
| 233 | + |
| 234 | + random_tx = generate_funding_chan(wallet, coin_2, alice_seckey.get_pubkey(), bob_seckey.get_pubkey()) |
| 235 | + |
| 236 | + random_txid = alice.sendrawtransaction(hexstring=random_tx.serialize().hex(), maxfeerate=0) |
| 237 | + bob.sendrawtransaction(hexstring=random_tx.serialize().hex(), maxfeerate=0) |
| 238 | + |
| 239 | + assert random_txid in alice.getrawmempool() |
| 240 | + assert random_txid in bob.getrawmempool() |
| 241 | + |
| 242 | + assert_equal(len(alice.getrawmempool()), 3) # alice_commitment_tx + alice_cpfp_on_alice_tx + random_txid |
| 243 | + assert_equal(len(bob.getrawmempool()), 2) # bob_commitment_tx + random_txid |
| 244 | + |
| 245 | + self.log.info("By partitioning network mempools with asymmetric valid commitment transactions, high-CPFP can be jammed") |
| 246 | + |
| 247 | + def run_test(self): |
| 248 | + self.wallet = MiniWallet(self.nodes[0]) |
| 249 | + |
| 250 | + self.test_network_topology_aware_pinning() |
| 251 | + |
| 252 | +if __name__ == '__main__': |
| 253 | + NetworkTopologyAwarePinning().main() |
0 commit comments