fork_pid_tests.cpp raw
1 // Copyright (c) 2025 The Limenka developers
2 // Distributed under the MIT software license, see the accompanying
3 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
4
5 #include <test/fork_util.h>
6
7 #include <chain.h>
8 #include <consensus/params.h>
9 #include <pow_fork.h>
10 #include <primitives/block.h>
11
12 #include <boost/test/unit_test.hpp>
13
14 BOOST_FIXTURE_TEST_SUITE(fork_pid_tests, ForkTestingSetup)
15
16 // Seed target for a fresh block index: round-trip the fork powLimit through
17 // compact encoding (24-bit mantissa is lossy), so that nBits round-trips exactly.
18 static arith_uint256 SeedTarget(const Consensus::Params& p)
19 {
20 arith_uint256 seed;
21 seed.SetCompact(UintToArith256(p.powLimit).GetCompact());
22 return seed;
23 }
24
25 // Advance one fork block: compute the target (pure) and commit the returned
26 // DAA state onto the block index, mirroring ConnectBlock.
27 static uint32_t Step(CBlockIndex& idx, CBlockHeader& hdr, const Consensus::Params& p)
28 {
29 ForkDAAState daa;
30 uint32_t nbits = CalculateForkTarget(&idx, &hdr, p, &daa);
31 idx.nForkTarget = daa.nForkTarget;
32 idx.nForkAvgError = daa.nForkAvgError;
33 idx.nForkLastBlockTime = daa.nForkLastBlockTime;
34 return nbits;
35 }
36
37 BOOST_AUTO_TEST_CASE(pid_state_zero_on_fresh_index)
38 {
39 CBlockHeader dummy;
40 dummy.nVersion = 0;
41 dummy.nTime = 0;
42 CBlockIndex fresh(dummy);
43 BOOST_CHECK_EQUAL(fresh.nForkTarget, arith_uint256(0));
44 BOOST_CHECK_EQUAL(fresh.nForkAvgError, 0);
45 BOOST_CHECK_EQUAL(fresh.nForkLastBlockTime, 0);
46 }
47
48 BOOST_AUTO_TEST_CASE(pid_init_seeds_from_block_difficulty)
49 {
50 const auto& p = ForkConsensus();
51 CBlockHeader dummy;
52 dummy.nTime = 100;
53 const arith_uint256 seed = SeedTarget(p);
54 dummy.nBits = seed.GetCompact();
55 CBlockIndex idx(dummy);
56 InitForkDAAState(&idx);
57 BOOST_CHECK_EQUAL(idx.nForkTarget, seed);
58 BOOST_CHECK_EQUAL(idx.nForkAvgError, 0);
59 BOOST_CHECK_EQUAL(idx.nForkLastBlockTime, 0);
60 }
61
62 BOOST_AUTO_TEST_CASE(pid_interval_target)
63 {
64 const auto& p = ForkConsensus();
65 BOOST_CHECK_EQUAL(p.nForkIntervalTarget, 617);
66 }
67
68 BOOST_AUTO_TEST_CASE(pid_constants_populated)
69 {
70 const auto& p = ForkConsensus();
71 BOOST_CHECK_GT(p.nForkKp, 0);
72 BOOST_CHECK_GT(p.nForkKiUp, 0);
73 BOOST_CHECK_GT(p.nForkKiDown, 0);
74 }
75
76 BOOST_AUTO_TEST_CASE(pid_calculate_first_block_no_error)
77 {
78 const auto& p = ForkConsensus();
79 const arith_uint256 seed = SeedTarget(p);
80
81 CBlockHeader hdr;
82 hdr.nVersion = 2;
83 hdr.nBits = seed.GetCompact();
84 hdr.nTime = 1000;
85
86 CBlockIndex idx(hdr);
87 InitForkDAAState(&idx);
88
89 // First block: last_time == 0, so actual = interval. error = 0.
90 uint32_t nbits = Step(idx, hdr, p);
91 BOOST_CHECK_GT(nbits, 0u);
92 BOOST_CHECK_EQUAL(idx.nForkLastBlockTime, 1000);
93 // No error on the first block: target unchanged.
94 BOOST_CHECK_EQUAL(idx.nForkTarget, seed);
95 }
96
97 BOOST_AUTO_TEST_CASE(pid_calculate_updates_state)
98 {
99 const auto& p = ForkConsensus();
100 const arith_uint256 seed = SeedTarget(p);
101
102 CBlockHeader hdr;
103 hdr.nVersion = 2;
104 hdr.nBits = seed.GetCompact();
105 hdr.nTime = 1050; // 50s after first (fast: target 600s)
106
107 CBlockIndex idx(hdr);
108 InitForkDAAState(&idx);
109 idx.nForkLastBlockTime = 1000;
110
111 uint32_t nbits = Step(idx, hdr, p);
112 BOOST_CHECK_GT(nbits, 0u);
113
114 // State updated: error = 600-50 = 550 (blocks fast, harden).
115 BOOST_CHECK_GT(idx.nForkAvgError, 0);
116 BOOST_CHECK_EQUAL(idx.nForkLastBlockTime, 1050);
117 // Target hardened (fast blocks -> lower target).
118 BOOST_CHECK_LT(idx.nForkTarget, seed);
119 }
120
121 BOOST_AUTO_TEST_CASE(pid_converges_under_massive_hashpower)
122 {
123 // A massive hashpower increase: blocks arrive every 32s
124 // instead of the 600s target. The PID must push the target DOWN (harder).
125 const auto& p = ForkConsensus();
126 const arith_uint256 seed = SeedTarget(p);
127
128 CBlockHeader hdr;
129 hdr.nVersion = 2;
130 hdr.nBits = seed.GetCompact();
131
132 CBlockIndex idx(hdr);
133 InitForkDAAState(&idx);
134 idx.nForkLastBlockTime = 1000;
135
136 arith_uint256 prev_target = idx.nForkTarget;
137 int64_t sim_time = 1000;
138
139 // Mine 20 blocks at 32s intervals.
140 for (int i = 0; i < 20; i++) {
141 sim_time += 32;
142 hdr.nTime = sim_time;
143 Step(idx, hdr, p);
144 }
145
146 // Target must have decreased (harder) because blocks are too fast.
147 BOOST_CHECK_LT(idx.nForkTarget, prev_target);
148
149 // Target should still be above a reasonable floor (not collapsed).
150 BOOST_CHECK_GT(idx.nForkTarget, seed / 1000);
151
152 // avg_error should be positive (blocks consistently fast).
153 BOOST_CHECK_GT(idx.nForkAvgError, 0);
154 }
155
156 BOOST_AUTO_TEST_CASE(pid_direction_reverses_on_slow_blocks)
157 {
158 // After fast blocks, switch to slow blocks (hashpower leaves).
159 // The PID should switch direction and ease the target.
160 const auto& p = ForkConsensus();
161 const arith_uint256 seed = SeedTarget(p);
162
163 CBlockHeader hdr;
164 hdr.nVersion = 2;
165 hdr.nBits = seed.GetCompact();
166
167 CBlockIndex idx(hdr);
168 InitForkDAAState(&idx);
169 idx.nForkLastBlockTime = 1000;
170
171 int64_t sim_time = 1000;
172
173 // Phase 1: fast blocks (32s)
174 for (int i = 0; i < 10; i++) {
175 sim_time += 32;
176 hdr.nTime = sim_time;
177 Step(idx, hdr, p);
178 }
179 arith_uint256 after_fast = idx.nForkTarget;
180 BOOST_CHECK_LT(after_fast, seed); // harder than the seed
181
182 // Phase 2: slow blocks (2000s — hashpower dropped far below target)
183 for (int i = 0; i < 10; i++) {
184 sim_time += 2000;
185 hdr.nTime = sim_time;
186 Step(idx, hdr, p);
187 }
188 arith_uint256 after_slow = idx.nForkTarget;
189
190 // Target should be EASIER than after the fast phase.
191 BOOST_CHECK_GT(after_slow, after_fast);
192 }
193
194 BOOST_AUTO_TEST_CASE(pid_recompute_matches_stateful)
195 {
196 // A2 regression: the DAA state is a deterministic pure function of the
197 // header chain. Recomputing from zeroed state (restart with an old
198 // index format, or headers-first sync) must produce the identical
199 // target as the stateful computation - the zero-state must not be
200 // mistaken for "first fork block".
201 const auto& p = ForkConsensus();
202 std::vector<std::unique_ptr<CBlockIndex>> chain;
203 chain.reserve(8);
204
205 {
206 auto idx = std::make_unique<CBlockIndex>();
207 idx->nTime = 1700000000;
208 idx->nBits = SeedTarget(p).GetCompact();
209 idx->nHeight = 0;
210 InitForkDAAState(idx.get());
211 chain.push_back(std::move(idx));
212 }
213
214 // Build a chain of 6 fork blocks with varying intervals, storing each
215 // block's DAA state exactly as ConnectBlock does (state of block N is
216 // computed from block N-1's state and N's stamp).
217 int64_t t = 1700000000;
218 for (int i = 1; i < 7; ++i) {
219 t += (i % 2 == 0) ? 620 : 605;
220 auto idx = std::make_unique<CBlockIndex>();
221 idx->nTime = t;
222 idx->nHeight = i;
223 idx->pprev = chain.back().get();
224 CBlockHeader hdr;
225 hdr.nTime = t;
226 ForkDAAState daa;
227 idx->nBits = CalculateForkTarget(idx->pprev, &hdr, p, &daa);
228 idx->nForkTarget = daa.nForkTarget;
229 idx->nForkAvgError = daa.nForkAvgError;
230 idx->nForkLastBlockTime = daa.nForkLastBlockTime;
231 chain.push_back(std::move(idx));
232 }
233
234 // Stateful target for the next block.
235 CBlockHeader next;
236 next.nTime = t + 600;
237 ForkDAAState daa_stateful;
238 const uint32_t nbits_stateful = CalculateForkTarget(chain.back().get(), &next, p, &daa_stateful);
239 const arith_uint256 stateful_tip_state = chain.back()->nForkTarget;
240
241 // Simulate a restart with an old-format index: zero every entry.
242 for (auto& idx : chain) {
243 idx->nForkTarget = arith_uint256(0);
244 idx->nForkAvgError = 0;
245 idx->nForkLastBlockTime = 0;
246 idx->nForkAggregateSeconds = 0;
247 }
248
249 // The recompute path must reproduce the identical target and restore
250 // the state on the entries.
251 ForkDAAState daa;
252 const uint32_t nbits_recomputed = CalculateForkTarget(chain.back().get(), &next, p, &daa);
253 BOOST_CHECK_EQUAL(nbits_recomputed, nbits_stateful);
254 // The walk restored the tip's state to its pre-restart value.
255 BOOST_CHECK_EQUAL(chain.back()->nForkTarget, stateful_tip_state);
256 }
257
258 BOOST_AUTO_TEST_SUITE_END()
259