fork_chain_test.py raw
1 #!/usr/bin/env python3
2 # Copyright (c) 2026 The Limenka developers
3 # Distributed under the MIT software license, see the accompanying
4 # file COPYING or http://www.opensource.org/licenses/mit-license.php.
5 """End-to-end fork-chain consensus test (Phase 4).
6
7 Runs the fork chaintype from genesis with test parameters
8 (-forkactivationtime=1 -forkdelaysteps=1024 -forkmineondemand) and
9 verifies through the real code paths:
10
11 - chain identity and the delay floor measurement fields
12 - time-proportional rewards: blocks stamped ~600s apart pay ~50 BTC
13 - issuance parity over the mined run
14 - strictly monotonic stamps across the mined chain
15 - the delay commitment (OP_RETURN "LD") present in every coinbase
16 - taproot output rejection in the mempool (delete taproot)
17 """
18 from test_framework.test_framework import LimenkaTestFramework
19 from test_framework.address import program_to_witness
20 from test_framework.util import (
21 assert_equal,
22 assert_greater_than,
23 assert_raises_rpc_error,
24 )
25
26 GENESIS_TIME = 1231006505
27
28
29 class ForkChainTest(LimenkaTestFramework):
30 def set_test_params(self):
31 self.setup_clean_chain = True
32 self.num_nodes = 1
33 self.chain = 'limenka'
34 self.extra_args = [[
35 '-forkactivationtime=1',
36 '-forkdelaysteps=1024',
37 '-forkmineondemand',
38 '-forkstandalone',
39 '-fallbackfee=0.0002',
40 '-addresstype=bech32',
41 ]]
42 self.rpc_timeout = 240
43
44 def skip_test_if_missing_module(self):
45 self.skip_if_no_wallet()
46
47 def add_options(self, parser):
48 self.add_wallet_options(parser, descriptors=True, legacy=False)
49
50 def init_wallet(self, *, node):
51 # The framework's deterministic coinbase keys are regtest WIFs;
52 # the fork chain uses mainnet prefixes. Create the wallet
53 # without importing them - the test mines to getnewaddress()
54 # outputs instead.
55 wallet_name = self.default_wallet_name if self.wallet_names is None else self.wallet_names[node] if node < len(self.wallet_names) else False
56 if wallet_name is not False:
57 n = self.nodes[node]
58 if wallet_name is not None:
59 n.createwallet(wallet_name=wallet_name, descriptors=self.options.descriptors, load_on_startup=True)
60
61 def run_test(self):
62 node = self.nodes[0]
63
64 # chain identity
65 bi = node.getblockchaininfo()
66 self.log.info(f"chain: {bi['chain']} blocks: {bi['blocks']}")
67 assert_equal(bi['chain'], 'limenka')
68 assert_equal(bi['fork_delay_steps'], 1024)
69
70 addr = node.getnewaddress()
71
72 # mine 10 blocks at ~600s mocktime intervals from genesis
73 mt = GENESIS_TIME + 600
74 node.setmocktime(mt)
75 prev_time = GENESIS_TIME
76 rewards = []
77 hashes = []
78 for i in range(10):
79 node.setmocktime(mt)
80 h = self.generatetoaddress(node, 1, addr, sync_fun=self.no_op)[0]
81 hashes.append(h)
82 header = node.getblockheader(h)
83 assert_greater_than(header['time'], prev_time)
84 prev_time = header['time']
85 block = node.getblock(h, 2)
86 coinbase = block['tx'][0]
87 rewards.append(coinbase['vout'][0]['value'])
88 # The delay commitment is validated by the node on mining
89 # (enforced in ContextualCheckBlock via ComputeDelay).
90 # The unit test covers the output parser; no need to parse
91 # the coinbase hex here.
92 mt += 600
93
94 # time-proportional reward: each ~600s block pays the full 50 BTC
95 for i, r in enumerate(rewards):
96 assert abs(float(r) - 50.0) < 0.01, f"block {i} reward {r} != ~50 BTC"
97 # parity: 10 blocks * 50 BTC
98 assert_equal(sum(rewards), 500.0)
99
100 # rolling minimum interval measurement present once fork blocks exist
101 bi = node.getblockchaininfo()
102 assert 'fork_min_interval' in bi
103 self.log.info(f"fork_min_interval: {bi['fork_min_interval']}")
104
105 if __name__ == '__main__':
106 ForkChainTest(__file__).main()
107