Skip to content

Commit 84e12b8

Browse files
committed
Add NTA pinning
1 parent 6337b97 commit 84e12b8

File tree

1 file changed

+253
-0
lines changed

1 file changed

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

0 commit comments

Comments
 (0)