mining_template_verification.py raw
1 #!/usr/bin/env python3
2 # Copyright (c) 2024-present 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 """Test getblocktemplate RPC in proposal mode
6
7 Generate several blocks and test them against the getblocktemplate RPC.
8 """
9
10 from concurrent.futures import ThreadPoolExecutor
11
12 import copy
13
14 from test_framework.blocktools import (
15 create_block,
16 add_witness_commitment,
17 NORMAL_GBT_REQUEST_PARAMS,
18 )
19
20 from test_framework.test_framework import BitcoinTestFramework
21 from test_framework.util import (
22 assert_equal,
23 assert_raises_rpc_error,
24 )
25
26 from test_framework.messages import (
27 BLOCK_HEADER_SIZE,
28 uint256_from_compact,
29 )
30
31 from test_framework.wallet import (
32 MiniWallet,
33 )
34
35 def assert_template(node, block, expect, *, rehash=True, submit=True, solve=True, expect_submit=None):
36 if rehash:
37 block.hashMerkleRoot = block.calc_merkle_root()
38
39 rsp = node.getblocktemplate(template_request={
40 'data': block.serialize().hex(),
41 'mode': 'proposal',
42 **NORMAL_GBT_REQUEST_PARAMS,
43 })
44 assert_equal(rsp, expect)
45 # Only attempt to submit invalid templates
46 if submit and expect is not None:
47 # submitblock runs checks in a different order, so may not return
48 # the same error
49 if expect_submit is None:
50 expect_submit = expect
51 if solve:
52 block.solve()
53 assert_equal(node.submitblock(block.serialize().hex()), expect_submit)
54
55 class MiningTemplateVerificationTest(BitcoinTestFramework):
56
57 def set_test_params(self):
58 self.num_nodes = 1
59
60 def valid_block_test(self, node, block):
61 self.log.info("Valid block")
62 assert_template(node, block, None)
63
64 def cb_missing_test(self, node, block):
65 self.log.info("Bad input hash for coinbase transaction")
66 bad_block = copy.deepcopy(block)
67 bad_block.vtx[0].vin[0].prevout.hash += 1
68 assert_template(node, bad_block, 'bad-cb-missing')
69
70 def block_without_transactions_test(self, node, block):
71 self.log.info("Block with no transactions")
72
73 no_tx_block = copy.deepcopy(block)
74 no_tx_block.vtx.clear()
75 no_tx_block.hashMerkleRoot = 0
76 no_tx_block.solve()
77 assert_template(node, no_tx_block, 'bad-blk-length', rehash=False)
78
79 def truncated_final_transaction_test(self, node, block):
80 self.log.info("Truncated final transaction")
81 assert_raises_rpc_error(-22, "Block decode failed", node.getblocktemplate,
82 template_request={
83 "data": block.serialize()[:-1].hex(),
84 "mode": "proposal",
85 **NORMAL_GBT_REQUEST_PARAMS,
86 }
87 )
88
89 def duplicate_transaction_test(self, node, block):
90 self.log.info("Duplicate transaction")
91 bad_block = copy.deepcopy(block)
92 bad_block.vtx.append(bad_block.vtx[0])
93 assert_template(node, bad_block, 'bad-txns-duplicate')
94
95 def thin_air_spending_test(self, node, block):
96 self.log.info("Transaction that spends from thin air")
97 bad_block = copy.deepcopy(block)
98 bad_tx = copy.deepcopy(bad_block.vtx[0])
99 bad_tx.vin[0].prevout.hash = 255
100 bad_block.vtx.append(bad_tx)
101 assert_template(node, bad_block, 'bad-txns-inputs-missingorspent')
102
103 def non_final_transaction_test(self, node, block):
104 self.log.info("Non-final transaction")
105 bad_block = copy.deepcopy(block)
106 bad_block.vtx[0].nLockTime = 2**32 - 1
107 assert_template(node, bad_block, 'bad-txns-nonfinal')
108
109 def bad_tx_count_test(self, node, block):
110 self.log.info("Bad tx count")
111 # The tx count is immediately after the block header
112 bad_block_sn = bytearray(block.serialize())
113 assert_equal(bad_block_sn[BLOCK_HEADER_SIZE], 1)
114 bad_block_sn[BLOCK_HEADER_SIZE] += 1
115 assert_raises_rpc_error(-22, "Block decode failed", node.getblocktemplate, {
116 'data': bad_block_sn.hex(),
117 'mode': 'proposal',
118 **NORMAL_GBT_REQUEST_PARAMS,
119 })
120
121 def nbits_test(self, node, block):
122 self.log.info("Extremely high nBits")
123 bad_block = copy.deepcopy(block)
124 bad_block.nBits = 469762303 # impossible in the real world
125 assert_template(node, bad_block, "bad-diffbits", solve=False, expect_submit="high-hash")
126
127 self.log.info("Lowering nBits should make the block invalid")
128 bad_block = copy.deepcopy(block)
129 bad_block.nBits -= 1
130 assert_template(node, bad_block, "bad-diffbits")
131
132 def merkle_root_test(self, node, block):
133 self.log.info("Bad merkle root")
134 bad_block = copy.deepcopy(block)
135 bad_block.hashMerkleRoot += 1
136 assert_template(node, bad_block, 'bad-txnmrklroot', rehash=False)
137
138 def bad_timestamp_test(self, node, block):
139 self.log.info("Bad timestamps")
140 bad_block = copy.deepcopy(block)
141 bad_block.nTime = 2**32 - 1
142 assert_template(node, bad_block, 'time-too-new')
143 bad_block.nTime = 0
144 assert_template(node, bad_block, 'time-too-old')
145
146 def current_tip_test(self, node, block):
147 self.log.info("Block must build on the current tip")
148 bad_block = copy.deepcopy(block)
149 bad_block.hashPrevBlock = 123
150 bad_block.solve()
151
152 assert_template(node, bad_block, "inconclusive-not-best-prevblk", expect_submit="prev-blk-not-found")
153
154 def pow_test(self, node, block):
155 '''Modifies block with the generated PoW'''
156 self.log.info("Generate a block")
157 target = uint256_from_compact(block.nBits)
158 # Ensure that it doesn't meet the target by coincidence
159 while block.hash_int <= target:
160 block.nNonce += 1
161 self.log.debug("Found a nonce")
162
163 self.log.info("A block template doesn't need PoW")
164 assert_template(node, block, None)
165
166 self.log.info("Add proof of work")
167 block.solve()
168 assert_template(node, block, None)
169
170 def submit_test(self, node, block_0_height, block):
171 self.log.info("getblocktemplate call in previous tests did not submit the block")
172 assert_equal(node.getblockcount(), block_0_height + 1)
173
174 self.log.info("Submitting this block should succeed")
175 assert_equal(node.submitblock(block.serialize().hex()), None)
176 node.waitforblockheight(2)
177
178 def transaction_test(self, node, block_0_height, tx):
179 self.log.info("make block template with a transaction")
180
181 block_1 = node.getblock(node.getblockhash(block_0_height + 1))
182 block_2_hash = node.getblockhash(block_0_height + 2)
183
184 block_3 = create_block(
185 int(block_2_hash, 16),
186 height=block_0_height + 3,
187 ntime=block_1["mediantime"] + 1,
188 txlist=[tx["hex"]],
189 )
190 assert_equal(len(block_3.vtx), 2)
191 add_witness_commitment(block_3)
192 block_3.solve()
193 assert_template(node, block_3, None)
194
195 self.log.info("checking block validity did not update the UTXO set")
196 # Call again to ensure the UTXO set wasn't updated
197 assert_template(node, block_3, None)
198
199 def overspending_transaction_test(self, node, block_0_height, tx):
200 self.log.info("Add an transaction that spends too much")
201
202 block_1 = node.getblock(node.getblockhash(block_0_height + 1))
203 block_2_hash = node.getblockhash(block_0_height + 2)
204
205 bad_tx = copy.deepcopy(tx)
206 bad_tx["tx"].vout[0].nValue = 10000000000
207 bad_tx_hex = bad_tx["tx"].serialize().hex()
208 assert_equal(
209 node.testmempoolaccept([bad_tx_hex])[0]["reject-reason"],
210 "bad-txns-in-belowout",
211 )
212 block_3 = create_block(
213 int(block_2_hash, 16),
214 height=block_0_height + 3,
215 ntime=block_1["mediantime"] + 1,
216 txlist=[bad_tx_hex],
217 )
218 assert_equal(len(block_3.vtx), 2)
219 add_witness_commitment(block_3)
220 block_3.solve()
221
222 assert_template(node, block_3, "bad-txns-in-belowout")
223
224 def spend_twice_test(self, node, block_0_height, tx):
225 block_1 = node.getblock(node.getblockhash(block_0_height + 1))
226 block_2_hash = node.getblockhash(block_0_height + 2)
227
228 self.log.info("Can't spend coins twice")
229 tx_hex = tx["tx"].serialize().hex()
230 tx_2 = copy.deepcopy(tx)
231 tx_2_hex = tx_2["tx"].serialize().hex()
232 # Nothing wrong with these transactions individually
233 assert_equal(node.testmempoolaccept([tx_hex])[0]["allowed"], True)
234 assert_equal(node.testmempoolaccept([tx_2_hex])[0]["allowed"], True)
235 # But can't be combined
236 assert_equal(
237 node.testmempoolaccept([tx_hex, tx_2_hex])[0]["package-error"],
238 "package-contains-duplicates",
239 )
240 block_3 = create_block(
241 int(block_2_hash, 16),
242 height=block_0_height + 3,
243 ntime=block_1["mediantime"] + 1,
244 txlist=[tx_hex, tx_2_hex],
245 )
246 assert_equal(len(block_3.vtx), 3)
247 add_witness_commitment(block_3)
248
249 assert_template(node, block_3, "bad-txns-inputs-missingorspent", submit=False)
250
251 return block_3
252
253 def parallel_test(self, node, block_3):
254 # Ensure that getblocktemplate can be called concurrently by many threads.
255 self.log.info("Check blocks in parallel")
256 check_50_blocks = lambda n: [
257 assert_template(n, block_3, "bad-txns-inputs-missingorspent", submit=False)
258 for _ in range(50)
259 ]
260 rpcs = [node.cli for _ in range(6)]
261 with ThreadPoolExecutor(max_workers=len(rpcs)) as threads:
262 list(threads.map(check_50_blocks, rpcs))
263
264 def run_test(self):
265 node = self.nodes[0]
266
267 block_0_height = node.getblockcount()
268 self.generate(node, sync_fun=self.no_op, nblocks=1)
269 block_1 = node.getblock(node.getbestblockhash())
270 block_2 = create_block(
271 int(block_1["hash"], 16),
272 height=block_0_height + 2,
273 ntime=block_1["mediantime"] + 1,
274 )
275
276 self.valid_block_test(node, block_2)
277 self.cb_missing_test(node, block_2)
278 self.block_without_transactions_test(node, block_2)
279 self.truncated_final_transaction_test(node, block_2)
280 self.duplicate_transaction_test(node, block_2)
281 self.thin_air_spending_test(node, block_2)
282 self.non_final_transaction_test(node, block_2)
283 self.bad_tx_count_test(node, block_2)
284 self.nbits_test(node, block_2)
285 self.merkle_root_test(node, block_2)
286 self.bad_timestamp_test(node, block_2)
287 self.current_tip_test(node, block_2)
288 # This sets the PoW for the next test
289 self.pow_test(node, block_2)
290 self.submit_test(node, block_0_height, block_2)
291
292 self.log.info("Generate a transaction")
293 tx = MiniWallet(node).create_self_transfer()
294
295 self.transaction_test(node, block_0_height, tx)
296 self.overspending_transaction_test(node, block_0_height, tx)
297 block_3 = self.spend_twice_test(node, block_0_height, tx)
298 self.parallel_test(node, block_3)
299
300 if __name__ == "__main__":
301 MiningTemplateVerificationTest(__file__).main()
302