forked from syscoin/syscoin
-
Notifications
You must be signed in to change notification settings - Fork 0
/
feature_llmqchainlocks.py
executable file
·254 lines (226 loc) · 11.8 KB
/
feature_llmqchainlocks.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
#!/usr/bin/env python3
# Copyright (c) 2015-2020 The Dash Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
import time
import struct
from test_framework.test_framework import DashTestFramework
from test_framework.messages import CInv, hash256, msg_clsig, msg_inv, ser_string, uint256_from_str
from test_framework.p2p import (
P2PInterface,
)
from test_framework.blocktools import (
create_block,
create_coinbase,
)
'''
feature_llmqchainlocks.py
Checks LLMQs based ChainLocks
'''
class TestP2PConn(P2PInterface):
def __init__(self):
super().__init__()
self.clsigs = {}
def send_clsig(self, clsig):
clhash = uint256_from_str(hash256(clsig.serialize()))
self.clsigs[clhash] = clsig
inv = msg_inv([CInv(20, clhash)])
self.send_message(inv)
def on_getdata(self, message):
for inv in message.inv:
if inv.hash in self.clsigs:
self.send_message(self.clsigs[inv.hash])
class LLMQChainLocksTest(DashTestFramework):
def set_test_params(self):
self.set_dash_test_params(4, 3, fast_dip3_enforcement=True)
def skip_test_if_missing_module(self):
self.skip_if_no_wallet()
def run_test(self):
# Connect all nodes to node1 so that we always have the whole network connected
# Otherwise only masternode connections will be established between nodes, which won't propagate TXs/blocks
# Usually node0 is the one that does this, but in this test we isolate it multiple times
for i in range(len(self.nodes)):
if i != 1:
self.connect_nodes(i, 1)
self.generate(self.nodes[0], 10)
self.sync_blocks(self.nodes, timeout=60*5)
self.nodes[0].spork("SPORK_17_QUORUM_DKG_ENABLED", 0)
self.nodes[0].spork("SPORK_19_CHAINLOCKS_ENABLED", 4070908800)
self.wait_for_sporks_same()
self.log.info("Mining 4 quorums")
for i in range(4):
self.mine_quorum()
self.nodes[0].spork("SPORK_19_CHAINLOCKS_ENABLED", 0)
self.wait_for_sporks_same()
self.log.info("Mine single block, wait for chainlock")
self.generate(self.nodes[0], 1)
self.wait_for_chainlocked_block_all_nodes(self.nodes[0].getbestblockhash())
self.log.info("Mine many blocks, wait for chainlock")
self.generate(self.nodes[0], 20)
# We need more time here due to 20 blocks being generated at once
self.wait_for_chainlocked_block_all_nodes(self.nodes[0].getbestblockhash(), timeout=30)
self.log.info("Assert that all blocks up until the tip are chainlocked")
for h in range(1, self.nodes[0].getblockcount()):
block = self.nodes[0].getblock(self.nodes[0].getblockhash(h))
assert(block['chainlock'])
self.log.info("Isolate node, mine on another, and reconnect")
self.isolate_node(self.nodes[0])
node0_mining_addr = self.nodes[0].getnewaddress()
node0_tip = self.nodes[0].getbestblockhash()
self.generatetoaddress(self.nodes[1], 5, node0_mining_addr, sync_fun=self.no_op)
self.wait_for_chainlocked_block(self.nodes[1], self.nodes[1].getbestblockhash())
assert(self.nodes[0].getbestblockhash() == node0_tip)
self.reconnect_isolated_node(self.nodes[0], 1)
self.generatetoaddress(self.nodes[1], 1, node0_mining_addr, sync_fun=self.no_op)
self.wait_for_chainlocked_block(self.nodes[0], self.nodes[1].getbestblockhash())
self.log.info("Isolate node, mine on both parts of the network, and reconnect")
self.isolate_node(self.nodes[0])
bad_tip = self.generate(self.nodes[0], 5, sync_fun=self.no_op)[-1]
self.generatetoaddress(self.nodes[1], 1, node0_mining_addr, sync_fun=self.no_op)
good_tip = self.nodes[1].getbestblockhash()
self.wait_for_chainlocked_block(self.nodes[1], good_tip)
assert(not self.nodes[0].getblock(self.nodes[0].getbestblockhash())["chainlock"])
self.reconnect_isolated_node(self.nodes[0], 1)
self.generatetoaddress(self.nodes[1], 1, node0_mining_addr, sync_fun=self.no_op)
self.wait_for_chainlocked_block(self.nodes[0], self.nodes[1].getbestblockhash())
assert(self.nodes[0].getblock(self.nodes[0].getbestblockhash())["previousblockhash"] == good_tip)
assert(self.nodes[1].getblock(self.nodes[1].getbestblockhash())["previousblockhash"] == good_tip)
self.log.info("The tip mined while this node was isolated should be marked conflicting now")
found = False
for tip in self.nodes[0].getchaintips():
if tip["hash"] == bad_tip:
assert(tip["status"] == "conflicting")
found = True
break
assert(found)
self.log.info("Keep node connected and let it try to reorg the chain")
good_tip = self.nodes[0].getbestblockhash()
self.log.info("Restart it so that it forgets all the chainlock messages from the past")
self.stop_node(0)
self.start_node(0)
self.connect_nodes(0, 1)
assert(self.nodes[0].getbestblockhash() == good_tip)
self.nodes[0].invalidateblock(good_tip)
self.log.info("Now try to reorg the chain")
self.generate(self.nodes[0], 2, sync_fun=self.no_op)
time.sleep(6)
assert(self.nodes[1].getbestblockhash() == good_tip)
bad_tip = self.generate(self.nodes[0], 2, sync_fun=self.no_op)[-1]
time.sleep(6)
assert(self.nodes[0].getbestblockhash() == bad_tip)
assert(self.nodes[1].getbestblockhash() == good_tip)
self.log.info("Now let the node which is on the wrong chain reorg back to the locked chain")
self.nodes[0].reconsiderblock(good_tip)
assert(self.nodes[0].getbestblockhash() != good_tip)
good_fork = good_tip
good_tip = self.generatetoaddress(self.nodes[1], 1, node0_mining_addr, sync_fun=self.no_op)[-1] # this should mark bad_tip as conflicting
self.wait_for_chainlocked_block(self.nodes[0], good_tip)
assert(self.nodes[0].getbestblockhash() == good_tip)
found = False
for tip in self.nodes[0].getchaintips():
if tip["hash"] == bad_tip:
assert(tip["status"] == "conflicting")
found = True
break
assert(found)
self.log.info("Should switch to the best non-conflicting tip (not to the most work chain) on restart")
assert(int(self.nodes[0].getblock(bad_tip)["chainwork"], 16) > int(self.nodes[1].getblock(good_tip)["chainwork"], 16))
self.stop_node(0)
self.start_node(0)
self.nodes[0].invalidateblock(good_fork)
self.stop_node(0)
self.start_node(0)
time.sleep(1)
assert(self.nodes[0].getbestblockhash() == good_tip)
txs = []
for i in range(3):
txs.append(self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1))
txs += self.create_chained_txs(self.nodes[0], 1)
self.log.info("Assert that after block generation these TXs are included")
node0_tip = self.generate(self.nodes[0], 1, sync_fun=self.no_op)[-1]
for txid in txs:
tx = self.nodes[0].getrawtransaction(txid, True)
assert("confirmations" in tx)
time.sleep(1)
node0_tip_block = self.nodes[0].getblock(node0_tip)
assert(not node0_tip_block["chainlock"])
assert(node0_tip_block["previousblockhash"] == good_tip)
self.generate(self.nodes[0], 1, sync_fun=self.no_op)
self.log.info("Assert that TXs got included now")
for txid in txs:
tx = self.nodes[0].getrawtransaction(txid, True)
assert("confirmations" in tx and tx["confirmations"] > 0)
# Enable network on first node again, which will cause the blocks to propagate
# for the mined TXs, which will then allow the network to create a CLSIG
self.log.info("Re-enable network on first node and wait for chainlock")
self.reconnect_isolated_node(self.nodes[0], 1)
self.log.info("Send fake future clsigs and see if this breaks ChainLocks")
for i in range(len(self.nodes)):
if i != 0:
self.connect_nodes(i, 0)
SIGN_HEIGHT_OFFSET = 8
p2p_node = self.nodes[0].add_p2p_connection(TestP2PConn())
p2p_node.wait_for_verack()
self.wait_for_chainlocked_block_all_nodes(self.nodes[0].getbestblockhash(), timeout=30)
self.log.info("Should accept fake clsig but other quorums should sign the actual block on the same height and override the malicious one")
fake_clsig1, fake_block_hash1 = self.create_fake_clsig(1)
p2p_node.send_clsig(fake_clsig1)
self.bump_mocktime(5, nodes=self.nodes)
time.sleep(5)
for node in self.nodes:
self.wait_for_most_recent_chainlock(node, fake_block_hash1, timeout=5)
tip = self.generate(self.nodes[0], 1, sync_fun=self.no_op)[-1]
self.sync_blocks()
self.bump_mocktime(5, nodes=self.nodes)
self.wait_for_chainlocked_block_all_nodes(tip, timeout=15)
self.log.info("Shouldn't accept fake clsig for 'tip + SIGN_HEIGHT_OFFSET + 1' block height")
fake_clsig2, fake_block_hash2 = self.create_fake_clsig(SIGN_HEIGHT_OFFSET + 1)
p2p_node.send_clsig(fake_clsig2)
self.bump_mocktime(7, nodes=self.nodes)
time.sleep(5)
for node in self.nodes:
assert(self.nodes[0].getchainlocks()["recent_chainlock"]["blockhash"] == tip)
assert(self.nodes[0].getchainlocks()["active_chainlock"]["blockhash"] == tip)
self.log.info("Should accept fake clsig for 'tip + SIGN_HEIGHT_OFFSET' but new clsigs should still be formed")
fake_clsig3, fake_block_hash3 = self.create_fake_clsig(SIGN_HEIGHT_OFFSET)
p2p_node.send_clsig(fake_clsig3)
self.bump_mocktime(7, nodes=self.nodes)
time.sleep(5)
for node in self.nodes:
self.wait_for_most_recent_chainlock(node, fake_block_hash3, timeout=15)
tip = self.generate(self.nodes[0], 1, sync_fun=self.no_op)[-1]
self.bump_mocktime(7, nodes=self.nodes)
self.wait_for_chainlocked_block_all_nodes(tip, timeout=15)
self.nodes[0].disconnect_p2ps()
def create_fake_clsig(self, height_offset):
# create a fake block height_offset blocks ahead of the tip
height = self.nodes[0].getblockcount() + height_offset
fake_block = create_block(0xff, create_coinbase(height))
# create a fake clsig for that block
quorum_hash = self.nodes[0].quorum_list(1)["llmq_test"][0]
request_id_buf = ser_string(b"clsig") + struct.pack("<I", height)
request_id_buf += bytes.fromhex(quorum_hash)[::-1]
request_id = hash256(request_id_buf)[::-1].hex()
quorum_hash = self.nodes[0].quorum_list(1)["llmq_test"][0]
for mn in self.mninfo:
mn.node.quorum_sign(100, request_id, fake_block.hash, quorum_hash)
rec_sig = self.get_recovered_sig(request_id, fake_block.hash)
fake_clsig = msg_clsig(height, fake_block.sha256, bytes.fromhex(rec_sig['sig']), [1,0,0,0])
return fake_clsig, fake_block.hash
def create_chained_txs(self, node, amount):
txid = node.sendtoaddress(node.getnewaddress(), amount)
tx = node.getrawtransaction(txid, True)
inputs = []
valueIn = 0
for txout in tx["vout"]:
inputs.append({"txid": txid, "vout": txout["n"]})
valueIn += txout["value"]
outputs = {
node.getnewaddress(): round(float(valueIn) - 0.0001, 6)
}
rawtx = node.createrawtransaction(inputs, outputs)
rawtx = node.signrawtransactionwithwallet(rawtx)
rawtxid = node.sendrawtransaction(rawtx["hex"])
return [txid, rawtxid]
if __name__ == '__main__':
LLMQChainLocksTest().main()