Skip to content

Commit 0acd8c7

Browse files
committed
Implement tests for ChainLock vs InstantSend lock conflict resolution
1 parent 0c1450e commit 0acd8c7

File tree

2 files changed

+339
-0
lines changed

2 files changed

+339
-0
lines changed

qa/pull-tester/rpc-tests.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
'llmq-signing.py', # NOTE: needs dash_hash to pass
4848
'llmq-chainlocks.py', # NOTE: needs dash_hash to pass
4949
'llmq-simplepose.py', # NOTE: needs dash_hash to pass
50+
'llmq-is-cl-conflicts.py', # NOTE: needs dash_hash to pass
5051
'dip4-coinbasemerkleroots.py', # NOTE: needs dash_hash to pass
5152
# vv Tests less than 60s vv
5253
'sendheaders.py', # NOTE: needs dash_hash to pass
Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2015-2018 The Dash 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+
from test_framework.blocktools import get_masternode_payment, create_coinbase, create_block
6+
from test_framework.mininode import *
7+
from test_framework.test_framework import DashTestFramework
8+
from test_framework.util import *
9+
from time import *
10+
11+
'''
12+
llmq-is-cl-conflicts.py
13+
14+
Checks conflict handling between ChainLocks and InstantSend
15+
16+
'''
17+
18+
class TestNode(SingleNodeConnCB):
19+
def __init__(self):
20+
SingleNodeConnCB.__init__(self)
21+
self.clsigs = {}
22+
self.islocks = {}
23+
24+
def send_clsig(self, clsig):
25+
hash = uint256_from_str(hash256(clsig.serialize()))
26+
self.clsigs[hash] = clsig
27+
28+
inv = msg_inv([CInv(29, hash)])
29+
self.send_message(inv)
30+
31+
def send_islock(self, islock):
32+
hash = uint256_from_str(hash256(islock.serialize()))
33+
self.islocks[hash] = islock
34+
35+
inv = msg_inv([CInv(30, hash)])
36+
self.send_message(inv)
37+
38+
def on_getdata(self, conn, message):
39+
for inv in message.inv:
40+
if inv.hash in self.clsigs:
41+
self.send_message(self.clsigs[inv.hash])
42+
if inv.hash in self.islocks:
43+
self.send_message(self.islocks[inv.hash])
44+
45+
46+
class LLMQ_IS_CL_Conflicts(DashTestFramework):
47+
def __init__(self):
48+
super().__init__(6, 5, [], fast_dip3_enforcement=True)
49+
#disable_mocktime()
50+
51+
def run_test(self):
52+
53+
while self.nodes[0].getblockchaininfo()["bip9_softforks"]["dip0008"]["status"] != "active":
54+
self.nodes[0].generate(10)
55+
sync_blocks(self.nodes, timeout=60*5)
56+
57+
self.test_node = TestNode()
58+
self.test_node.add_connection(NodeConn('127.0.0.1', p2p_port(0), self.nodes[0], self.test_node))
59+
NetworkThread().start() # Start up network handling in another thread
60+
self.test_node.wait_for_verack()
61+
62+
self.nodes[0].spork("SPORK_17_QUORUM_DKG_ENABLED", 0)
63+
self.nodes[0].spork("SPORK_19_CHAINLOCKS_ENABLED", 0)
64+
self.nodes[0].spork("SPORK_20_INSTANTSEND_LLMQ_BASED", 0)
65+
self.wait_for_sporks_same()
66+
67+
self.mine_quorum()
68+
69+
# mine single block, wait for chainlock
70+
self.nodes[0].generate(1)
71+
self.wait_for_chainlock_tip_all_nodes()
72+
73+
self.test_chainlock_overrides_islock(False)
74+
self.test_chainlock_overrides_islock(True)
75+
self.test_islock_overrides_nonchainlock()
76+
77+
def test_chainlock_overrides_islock(self, test_block_conflict):
78+
# create three raw TXs, they will conflict with each other
79+
rawtx1 = self.create_raw_tx(self.nodes[0], self.nodes[0], 1, 1, 100)['hex']
80+
rawtx2 = self.create_raw_tx(self.nodes[0], self.nodes[0], 1, 1, 100)['hex']
81+
rawtx3 = self.create_raw_tx(self.nodes[0], self.nodes[0], 1, 1, 100)['hex']
82+
rawtx1_obj = FromHex(CTransaction(), rawtx1)
83+
rawtx2_obj = FromHex(CTransaction(), rawtx2)
84+
rawtx3_obj = FromHex(CTransaction(), rawtx3)
85+
86+
rawtx1_txid = self.nodes[0].sendrawtransaction(rawtx1)
87+
rawtx2_txid = encode(hash256(hex_str_to_bytes(rawtx2))[::-1], 'hex_codec').decode('ascii')
88+
rawtx3_txid = encode(hash256(hex_str_to_bytes(rawtx3))[::-1], 'hex_codec').decode('ascii')
89+
90+
# Create a chained TX on top of tx1
91+
inputs = []
92+
n = 0
93+
for out in rawtx1_obj.vout:
94+
if out.nValue == 100000000:
95+
inputs.append({"txid": rawtx1_txid, "vout": n})
96+
n += 1
97+
rawtx4 = self.nodes[0].createrawtransaction(inputs, {self.nodes[0].getnewaddress(): 0.999})
98+
rawtx4 = self.nodes[0].signrawtransaction(rawtx4)['hex']
99+
rawtx4_txid = self.nodes[0].sendrawtransaction(rawtx4)
100+
101+
for node in self.nodes:
102+
self.wait_for_instantlock(rawtx1_txid, node)
103+
self.wait_for_instantlock(rawtx4_txid, node)
104+
105+
block = self.create_block(self.nodes[0], [rawtx2_obj])
106+
if test_block_conflict:
107+
submit_result = self.nodes[0].submitblock(ToHex(block))
108+
assert(submit_result == "conflict-tx-lock")
109+
110+
cl = self.create_chainlock(self.nodes[0].getblockcount() + 1, block.sha256)
111+
self.test_node.send_clsig(cl)
112+
113+
# Give the CLSIG some time to propagate. We unfortunately can't check propagation here as "getblock/getblockheader"
114+
# is required to check for CLSIGs, but this requires the block header to be propagated already
115+
sleep(1)
116+
117+
# The block should get accepted now, and at the same time prune the conflicting ISLOCKs
118+
submit_result = self.nodes[1].submitblock(ToHex(block))
119+
if test_block_conflict:
120+
assert(submit_result == "duplicate")
121+
else:
122+
assert(submit_result is None)
123+
124+
for node in self.nodes:
125+
self.wait_for_chainlock(node, "%064x" % block.sha256)
126+
127+
# Create a chained TX on top of tx2
128+
inputs = []
129+
n = 0
130+
for out in rawtx2_obj.vout:
131+
if out.nValue == 100000000:
132+
inputs.append({"txid": rawtx2_txid, "vout": n})
133+
n += 1
134+
rawtx5 = self.nodes[0].createrawtransaction(inputs, {self.nodes[0].getnewaddress(): 0.999})
135+
rawtx5 = self.nodes[0].signrawtransaction(rawtx5)['hex']
136+
rawtx5_txid = self.nodes[0].sendrawtransaction(rawtx5)
137+
for node in self.nodes:
138+
self.wait_for_instantlock(rawtx5_txid, node)
139+
140+
# Lets verify that the ISLOCKs got pruned
141+
for node in self.nodes:
142+
assert_raises_jsonrpc(-5, "No such mempool or blockchain transaction", node.getrawtransaction, rawtx1_txid, True)
143+
assert_raises_jsonrpc(-5, "No such mempool or blockchain transaction", node.getrawtransaction, rawtx4_txid, True)
144+
rawtx = node.getrawtransaction(rawtx2_txid, True)
145+
assert(rawtx['chainlock'])
146+
assert(rawtx['instantlock'])
147+
assert(not rawtx['instantlock_internal'])
148+
149+
def test_islock_overrides_nonchainlock(self):
150+
# create two raw TXs, they will conflict with each other
151+
rawtx1 = self.create_raw_tx(self.nodes[0], self.nodes[0], 1, 1, 100)['hex']
152+
rawtx2 = self.create_raw_tx(self.nodes[0], self.nodes[0], 1, 1, 100)['hex']
153+
154+
rawtx1_txid = encode(hash256(hex_str_to_bytes(rawtx1))[::-1], 'hex_codec').decode('ascii')
155+
rawtx2_txid = encode(hash256(hex_str_to_bytes(rawtx2))[::-1], 'hex_codec').decode('ascii')
156+
157+
# Create an ISLOCK but don't broadcast it yet
158+
islock = self.create_islock(rawtx2)
159+
160+
# Stop enough MNs so that ChainLocks don't work anymore
161+
for i in range(3):
162+
self.stop_node(len(self.nodes) - 1)
163+
self.nodes.pop(len(self.nodes) - 1)
164+
self.mninfo.pop(len(self.mninfo) - 1)
165+
166+
# Send tx1, which will later conflict with the ISLOCK
167+
self.nodes[0].sendrawtransaction(rawtx1)
168+
169+
# fast forward 11 minutes, so that the TX is considered safe and included in the next block
170+
set_mocktime(get_mocktime() + int(60 * 11))
171+
set_node_times(self.nodes, get_mocktime())
172+
173+
# Mine the conflicting TX into a block
174+
good_tip = self.nodes[0].getbestblockhash()
175+
self.nodes[0].generate(2)
176+
self.sync_all()
177+
178+
# Assert that the conflicting tx got mined and the locked TX is not valid
179+
assert(self.nodes[0].getrawtransaction(rawtx1_txid, True)['confirmations'] > 0)
180+
assert_raises_jsonrpc(-25, "Missing inputs", self.nodes[0].sendrawtransaction, rawtx2)
181+
182+
# Send the ISLOCK, which should result in the last 2 blocks to be invalidated, even though the nodes don't know
183+
# the locked transaction yet
184+
self.test_node.send_islock(islock)
185+
sleep(5)
186+
187+
assert(self.nodes[0].getbestblockhash() == good_tip)
188+
assert(self.nodes[1].getbestblockhash() == good_tip)
189+
190+
# Send the actual transaction and mine it
191+
self.nodes[0].sendrawtransaction(rawtx2)
192+
self.nodes[0].generate(1)
193+
self.sync_all()
194+
195+
assert(self.nodes[0].getrawtransaction(rawtx2_txid, True)['confirmations'] > 0)
196+
assert(self.nodes[1].getrawtransaction(rawtx2_txid, True)['confirmations'] > 0)
197+
assert(self.nodes[0].getrawtransaction(rawtx2_txid, True)['instantlock'])
198+
assert(self.nodes[1].getrawtransaction(rawtx2_txid, True)['instantlock'])
199+
assert(self.nodes[0].getbestblockhash() != good_tip)
200+
assert(self.nodes[1].getbestblockhash() != good_tip)
201+
202+
def wait_for_chainlock_tip_all_nodes(self):
203+
for node in self.nodes:
204+
tip = node.getbestblockhash()
205+
self.wait_for_chainlock(node, tip)
206+
207+
def wait_for_chainlock_tip(self, node):
208+
tip = node.getbestblockhash()
209+
self.wait_for_chainlock(node, tip)
210+
211+
def wait_for_chainlock(self, node, block_hash):
212+
t = time()
213+
while time() - t < 15:
214+
try:
215+
block = node.getblockheader(block_hash)
216+
if block["confirmations"] > 0 and block["chainlock"]:
217+
return
218+
except:
219+
# block might not be on the node yet
220+
pass
221+
sleep(0.1)
222+
raise AssertionError("wait_for_chainlock timed out")
223+
224+
def create_block(self, node, vtx=[]):
225+
bt = node.getblocktemplate()
226+
height = bt['height']
227+
tip_hash = bt['previousblockhash']
228+
229+
coinbasevalue = bt['coinbasevalue']
230+
miner_address = node.getnewaddress()
231+
mn_payee = bt['masternode'][0]['payee']
232+
233+
# calculate fees that the block template included (we'll have to remove it from the coinbase as we won't
234+
# include the template's transactions
235+
bt_fees = 0
236+
for tx in bt['transactions']:
237+
bt_fees += tx['fee']
238+
239+
new_fees = 0
240+
for tx in vtx:
241+
in_value = 0
242+
out_value = 0
243+
for txin in tx.vin:
244+
txout = node.gettxout("%064x" % txin.prevout.hash, txin.prevout.n, False)
245+
in_value += int(txout['value'] * COIN)
246+
for txout in tx.vout:
247+
out_value += txout.nValue
248+
new_fees += in_value - out_value
249+
250+
# fix fees
251+
coinbasevalue -= bt_fees
252+
coinbasevalue += new_fees
253+
254+
mn_amount = get_masternode_payment(height, coinbasevalue)
255+
miner_amount = coinbasevalue - mn_amount
256+
257+
outputs = {miner_address: str(Decimal(miner_amount) / COIN)}
258+
if mn_amount > 0:
259+
outputs[mn_payee] = str(Decimal(mn_amount) / COIN)
260+
261+
coinbase = FromHex(CTransaction(), node.createrawtransaction([], outputs))
262+
coinbase.vin = create_coinbase(height).vin
263+
264+
# We can't really use this one as it would result in invalid merkle roots for masternode lists
265+
if len(bt['coinbase_payload']) != 0:
266+
cbtx = FromHex(CCbTx(version=1), bt['coinbase_payload'])
267+
coinbase.nVersion = 3
268+
coinbase.nType = 5 # CbTx
269+
coinbase.vExtraPayload = cbtx.serialize()
270+
271+
coinbase.calc_sha256()
272+
273+
block = create_block(int(tip_hash, 16), coinbase, nTime=bt['curtime'])
274+
block.vtx += vtx
275+
276+
# Add quorum commitments from template
277+
for tx in bt['transactions']:
278+
tx2 = FromHex(CTransaction(), tx['data'])
279+
if tx2.nType == 6:
280+
block.vtx.append(tx2)
281+
282+
block.hashMerkleRoot = block.calc_merkle_root()
283+
block.solve()
284+
return block
285+
286+
def create_chainlock(self, height, blockHash):
287+
request_id = "%064x" % uint256_from_str(hash256(ser_string(b"clsig") + struct.pack("<I", height)))
288+
message_hash = "%064x" % blockHash
289+
290+
for mn in self.mninfo:
291+
mn.node.quorum('sign', 100, request_id, message_hash)
292+
293+
recSig = None
294+
295+
t = time()
296+
while time() - t < 10:
297+
try:
298+
recSig = self.nodes[0].quorum('getrecsig', 100, request_id, message_hash)
299+
break
300+
except:
301+
sleep(0.1)
302+
assert(recSig is not None)
303+
304+
clsig = msg_clsig(height, blockHash, hex_str_to_bytes(recSig['sig']))
305+
return clsig
306+
307+
def create_islock(self, hextx):
308+
tx = FromHex(CTransaction(), hextx)
309+
tx.rehash()
310+
311+
request_id_buf = ser_string(b"islock") + ser_compact_size(len(tx.vin))
312+
inputs = []
313+
for txin in tx.vin:
314+
request_id_buf += txin.prevout.serialize()
315+
inputs.append(txin.prevout)
316+
request_id = "%064x" % uint256_from_str(hash256(request_id_buf))
317+
message_hash = "%064x" % tx.sha256
318+
319+
for mn in self.mninfo:
320+
mn.node.quorum('sign', 100, request_id, message_hash)
321+
322+
recSig = None
323+
324+
t = time()
325+
while time() - t < 10:
326+
try:
327+
recSig = self.nodes[0].quorum('getrecsig', 100, request_id, message_hash)
328+
break
329+
except:
330+
sleep(0.1)
331+
assert(recSig is not None)
332+
333+
islock = msg_islock(inputs, tx.sha256, hex_str_to_bytes(recSig['sig']))
334+
return islock
335+
336+
337+
if __name__ == '__main__':
338+
LLMQ_IS_CL_Conflicts().main()

0 commit comments

Comments
 (0)