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