versionbits_tests.cpp raw

   1  // Copyright (c) 2014-2022 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 <chain.h>
   6  #include <chainparams.h>
   7  #include <consensus/params.h>
   8  #include <test/util/random.h>
   9  #include <test/util/setup_common.h>
  10  #include <util/chaintype.h>
  11  #include <versionbits.h>
  12  
  13  #include <boost/test/unit_test.hpp>
  14  
  15  /* Define a virtual block time, one block per 10 minutes after Nov 14 2014, 0:55:36am */
  16  static int32_t TestTime(int nHeight) { return 1415926536 + 600 * nHeight; }
  17  
  18  static std::string StateName(ThresholdState state)
  19  {
  20      switch (state) {
  21      case ThresholdState::DEFINED:   return "DEFINED";
  22      case ThresholdState::STARTED:   return "STARTED";
  23      case ThresholdState::LOCKED_IN: return "LOCKED_IN";
  24      case ThresholdState::ACTIVE:    return "ACTIVE";
  25      case ThresholdState::FAILED:    return "FAILED";
  26      case ThresholdState::EXPIRED:   return "EXPIRED";
  27      } // no default case, so the compiler can warn about missing cases
  28      return "";
  29  }
  30  
  31  static const Consensus::Params paramsDummy = Consensus::Params();
  32  
  33  class TestConditionChecker : public AbstractThresholdConditionChecker
  34  {
  35  private:
  36      mutable ThresholdConditionCache cache;
  37  
  38  public:
  39      int64_t BeginTime(const Consensus::Params& params) const override { return TestTime(10000); }
  40      int64_t EndTime(const Consensus::Params& params) const override { return TestTime(20000); }
  41      int Period(const Consensus::Params& params) const override { return 1000; }
  42      int Threshold(const Consensus::Params& params) const override { return 900; }
  43      bool Condition(const CBlockIndex* pindex, const Consensus::Params& params) const override { return (pindex->nVersion & 0x100); }
  44  
  45      ThresholdState GetStateFor(const CBlockIndex* pindexPrev) const { return AbstractThresholdConditionChecker::GetStateFor(pindexPrev, paramsDummy, cache); }
  46      int GetStateSinceHeightFor(const CBlockIndex* pindexPrev) const { return AbstractThresholdConditionChecker::GetStateSinceHeightFor(pindexPrev, paramsDummy, cache); }
  47  };
  48  
  49  class TestDelayedActivationConditionChecker : public TestConditionChecker
  50  {
  51  public:
  52      int MinActivationHeight(const Consensus::Params& params) const override { return 15000; }
  53  };
  54  
  55  class TestAlwaysActiveConditionChecker : public TestConditionChecker
  56  {
  57  public:
  58      int64_t BeginTime(const Consensus::Params& params) const override { return Consensus::BIP9Deployment::ALWAYS_ACTIVE; }
  59  };
  60  
  61  class TestNeverActiveConditionChecker : public TestConditionChecker
  62  {
  63  public:
  64      int64_t BeginTime(const Consensus::Params& params) const override { return Consensus::BIP9Deployment::NEVER_ACTIVE; }
  65  };
  66  
  67  #define CHECKERS 6
  68  
  69  class VersionBitsTester
  70  {
  71      FastRandomContext& m_rng;
  72      // A fake blockchain
  73      std::vector<CBlockIndex*> vpblock;
  74  
  75      // 6 independent checkers for the same bit.
  76      // The first one performs all checks, the second only 50%, the third only 25%, etc...
  77      // This is to test whether lack of cached information leads to the same results.
  78      TestConditionChecker checker[CHECKERS];
  79      // Another 6 that assume delayed activation
  80      TestDelayedActivationConditionChecker checker_delayed[CHECKERS];
  81      // Another 6 that assume always active activation
  82      TestAlwaysActiveConditionChecker checker_always[CHECKERS];
  83      // Another 6 that assume never active activation
  84      TestNeverActiveConditionChecker checker_never[CHECKERS];
  85  
  86      // Test counter (to identify failures)
  87      int num{1000};
  88  
  89  public:
  90      VersionBitsTester(FastRandomContext& rng) : m_rng{rng} {}
  91  
  92      VersionBitsTester& Reset() {
  93          // Have each group of tests be counted by the 1000s part, starting at 1000
  94          num = num - (num % 1000) + 1000;
  95  
  96          for (unsigned int i = 0; i < vpblock.size(); i++) {
  97              delete vpblock[i];
  98          }
  99          for (unsigned int  i = 0; i < CHECKERS; i++) {
 100              checker[i] = TestConditionChecker();
 101              checker_delayed[i] = TestDelayedActivationConditionChecker();
 102              checker_always[i] = TestAlwaysActiveConditionChecker();
 103              checker_never[i] = TestNeverActiveConditionChecker();
 104          }
 105          vpblock.clear();
 106          return *this;
 107      }
 108  
 109      ~VersionBitsTester() {
 110           Reset();
 111      }
 112  
 113      VersionBitsTester& Mine(unsigned int height, int32_t nTime, int32_t nVersion) {
 114          while (vpblock.size() < height) {
 115              CBlockIndex* pindex = new CBlockIndex();
 116              pindex->nHeight = vpblock.size();
 117              pindex->pprev = Tip();
 118              pindex->nTime = nTime;
 119              pindex->nVersion = nVersion;
 120              pindex->BuildSkip();
 121              vpblock.push_back(pindex);
 122          }
 123          return *this;
 124      }
 125  
 126      VersionBitsTester& TestStateSinceHeight(int height)
 127      {
 128          return TestStateSinceHeight(height, height);
 129      }
 130  
 131      VersionBitsTester& TestStateSinceHeight(int height, int height_delayed)
 132      {
 133          const CBlockIndex* tip = Tip();
 134          for (int i = 0; i < CHECKERS; i++) {
 135              if (m_rng.randbits(i) == 0) {
 136                  BOOST_CHECK_MESSAGE(checker[i].GetStateSinceHeightFor(tip) == height, strprintf("Test %i for StateSinceHeight", num));
 137                  BOOST_CHECK_MESSAGE(checker_delayed[i].GetStateSinceHeightFor(tip) == height_delayed, strprintf("Test %i for StateSinceHeight (delayed)", num));
 138                  BOOST_CHECK_MESSAGE(checker_always[i].GetStateSinceHeightFor(tip) == 0, strprintf("Test %i for StateSinceHeight (always active)", num));
 139                  BOOST_CHECK_MESSAGE(checker_never[i].GetStateSinceHeightFor(tip) == 0, strprintf("Test %i for StateSinceHeight (never active)", num));
 140              }
 141          }
 142          num++;
 143          return *this;
 144      }
 145  
 146      VersionBitsTester& TestState(ThresholdState exp)
 147      {
 148          return TestState(exp, exp);
 149      }
 150  
 151      VersionBitsTester& TestState(ThresholdState exp, ThresholdState exp_delayed)
 152      {
 153          if (exp != exp_delayed) {
 154              // only expected differences are that delayed stays in locked_in longer
 155              BOOST_CHECK_EQUAL(exp, ThresholdState::ACTIVE);
 156              BOOST_CHECK_EQUAL(exp_delayed, ThresholdState::LOCKED_IN);
 157          }
 158  
 159          const CBlockIndex* pindex = Tip();
 160          for (int i = 0; i < CHECKERS; i++) {
 161              if (m_rng.randbits(i) == 0) {
 162                  ThresholdState got = checker[i].GetStateFor(pindex);
 163                  ThresholdState got_delayed = checker_delayed[i].GetStateFor(pindex);
 164                  ThresholdState got_always = checker_always[i].GetStateFor(pindex);
 165                  ThresholdState got_never = checker_never[i].GetStateFor(pindex);
 166                  // nHeight of the next block. If vpblock is empty, the next (ie first)
 167                  // block should be the genesis block with nHeight == 0.
 168                  int height = pindex == nullptr ? 0 : pindex->nHeight + 1;
 169                  BOOST_CHECK_MESSAGE(got == exp, strprintf("Test %i for %s height %d (got %s)", num, StateName(exp), height, StateName(got)));
 170                  BOOST_CHECK_MESSAGE(got_delayed == exp_delayed, strprintf("Test %i for %s height %d (got %s; delayed case)", num, StateName(exp_delayed), height, StateName(got_delayed)));
 171                  BOOST_CHECK_MESSAGE(got_always == ThresholdState::ACTIVE, strprintf("Test %i for ACTIVE height %d (got %s; always active case)", num, height, StateName(got_always)));
 172                  BOOST_CHECK_MESSAGE(got_never == ThresholdState::FAILED, strprintf("Test %i for FAILED height %d (got %s; never active case)", num, height, StateName(got_never)));
 173              }
 174          }
 175          num++;
 176          return *this;
 177      }
 178  
 179      VersionBitsTester& TestDefined() { return TestState(ThresholdState::DEFINED); }
 180      VersionBitsTester& TestStarted() { return TestState(ThresholdState::STARTED); }
 181      VersionBitsTester& TestLockedIn() { return TestState(ThresholdState::LOCKED_IN); }
 182      VersionBitsTester& TestActive() { return TestState(ThresholdState::ACTIVE); }
 183      VersionBitsTester& TestFailed() { return TestState(ThresholdState::FAILED); }
 184  
 185      // non-delayed should be active; delayed should still be locked in
 186      VersionBitsTester& TestActiveDelayed() { return TestState(ThresholdState::ACTIVE, ThresholdState::LOCKED_IN); }
 187  
 188      CBlockIndex* Tip() { return vpblock.empty() ? nullptr : vpblock.back(); }
 189  };
 190  
 191  BOOST_FIXTURE_TEST_SUITE(versionbits_tests, BasicTestingSetup)
 192  
 193  BOOST_AUTO_TEST_CASE(versionbits_test)
 194  {
 195      for (int i = 0; i < 64; i++) {
 196          // DEFINED -> STARTED after timeout reached -> FAILED
 197          VersionBitsTester(m_rng).TestDefined().TestStateSinceHeight(0)
 198                             .Mine(1, TestTime(1), 0x100).TestDefined().TestStateSinceHeight(0)
 199                             .Mine(11, TestTime(11), 0x100).TestDefined().TestStateSinceHeight(0)
 200                             .Mine(989, TestTime(989), 0x100).TestDefined().TestStateSinceHeight(0)
 201                             .Mine(999, TestTime(20000), 0x100).TestDefined().TestStateSinceHeight(0) // Timeout and start time reached simultaneously
 202                             .Mine(1000, TestTime(20000), 0).TestStarted().TestStateSinceHeight(1000) // Hit started, stop signalling
 203                             .Mine(1999, TestTime(30001), 0).TestStarted().TestStateSinceHeight(1000)
 204                             .Mine(2000, TestTime(30002), 0x100).TestFailed().TestStateSinceHeight(2000) // Hit failed, start signalling again
 205                             .Mine(2001, TestTime(30003), 0x100).TestFailed().TestStateSinceHeight(2000)
 206                             .Mine(2999, TestTime(30004), 0x100).TestFailed().TestStateSinceHeight(2000)
 207                             .Mine(3000, TestTime(30005), 0x100).TestFailed().TestStateSinceHeight(2000)
 208                             .Mine(4000, TestTime(30006), 0x100).TestFailed().TestStateSinceHeight(2000)
 209  
 210          // DEFINED -> STARTED -> FAILED
 211                             .Reset().TestDefined().TestStateSinceHeight(0)
 212                             .Mine(1, TestTime(1), 0).TestDefined().TestStateSinceHeight(0)
 213                             .Mine(1000, TestTime(10000) - 1, 0x100).TestDefined().TestStateSinceHeight(0) // One second more and it would be defined
 214                             .Mine(2000, TestTime(10000), 0x100).TestStarted().TestStateSinceHeight(2000) // So that's what happens the next period
 215                             .Mine(2051, TestTime(10010), 0).TestStarted().TestStateSinceHeight(2000) // 51 old blocks
 216                             .Mine(2950, TestTime(10020), 0x100).TestStarted().TestStateSinceHeight(2000) // 899 new blocks
 217                             .Mine(3000, TestTime(20000), 0).TestFailed().TestStateSinceHeight(3000) // 50 old blocks (so 899 out of the past 1000)
 218                             .Mine(4000, TestTime(20010), 0x100).TestFailed().TestStateSinceHeight(3000)
 219  
 220          // DEFINED -> STARTED -> LOCKEDIN after timeout reached -> ACTIVE
 221                             .Reset().TestDefined().TestStateSinceHeight(0)
 222                             .Mine(1, TestTime(1), 0).TestDefined().TestStateSinceHeight(0)
 223                             .Mine(1000, TestTime(10000) - 1, 0x101).TestDefined().TestStateSinceHeight(0) // One second more and it would be defined
 224                             .Mine(2000, TestTime(10000), 0x101).TestStarted().TestStateSinceHeight(2000) // So that's what happens the next period
 225                             .Mine(2999, TestTime(30000), 0x100).TestStarted().TestStateSinceHeight(2000) // 999 new blocks
 226                             .Mine(3000, TestTime(30000), 0x100).TestLockedIn().TestStateSinceHeight(3000) // 1 new block (so 1000 out of the past 1000 are new)
 227                             .Mine(3999, TestTime(30001), 0).TestLockedIn().TestStateSinceHeight(3000)
 228                             .Mine(4000, TestTime(30002), 0).TestActiveDelayed().TestStateSinceHeight(4000, 3000)
 229                             .Mine(14333, TestTime(30003), 0).TestActiveDelayed().TestStateSinceHeight(4000, 3000)
 230                             .Mine(24000, TestTime(40000), 0).TestActive().TestStateSinceHeight(4000, 15000)
 231  
 232          // DEFINED -> STARTED -> LOCKEDIN before timeout -> ACTIVE
 233                             .Reset().TestDefined()
 234                             .Mine(1, TestTime(1), 0).TestDefined().TestStateSinceHeight(0)
 235                             .Mine(1000, TestTime(10000) - 1, 0x101).TestDefined().TestStateSinceHeight(0) // One second more and it would be defined
 236                             .Mine(2000, TestTime(10000), 0x101).TestStarted().TestStateSinceHeight(2000) // So that's what happens the next period
 237                             .Mine(2050, TestTime(10010), 0x200).TestStarted().TestStateSinceHeight(2000) // 50 old blocks
 238                             .Mine(2950, TestTime(10020), 0x100).TestStarted().TestStateSinceHeight(2000) // 900 new blocks
 239                             .Mine(2999, TestTime(19999), 0x200).TestStarted().TestStateSinceHeight(2000) // 49 old blocks
 240                             .Mine(3000, TestTime(29999), 0x200).TestLockedIn().TestStateSinceHeight(3000) // 1 old block (so 900 out of the past 1000)
 241                             .Mine(3999, TestTime(30001), 0).TestLockedIn().TestStateSinceHeight(3000)
 242                             .Mine(4000, TestTime(30002), 0).TestActiveDelayed().TestStateSinceHeight(4000, 3000) // delayed will not become active until height=15000
 243                             .Mine(14333, TestTime(30003), 0).TestActiveDelayed().TestStateSinceHeight(4000, 3000)
 244                             .Mine(15000, TestTime(40000), 0).TestActive().TestStateSinceHeight(4000, 15000)
 245                             .Mine(24000, TestTime(40000), 0).TestActive().TestStateSinceHeight(4000, 15000)
 246  
 247          // DEFINED multiple periods -> STARTED multiple periods -> FAILED
 248                             .Reset().TestDefined().TestStateSinceHeight(0)
 249                             .Mine(999, TestTime(999), 0).TestDefined().TestStateSinceHeight(0)
 250                             .Mine(1000, TestTime(1000), 0).TestDefined().TestStateSinceHeight(0)
 251                             .Mine(2000, TestTime(2000), 0).TestDefined().TestStateSinceHeight(0)
 252                             .Mine(3000, TestTime(10000), 0).TestStarted().TestStateSinceHeight(3000)
 253                             .Mine(4000, TestTime(10000), 0).TestStarted().TestStateSinceHeight(3000)
 254                             .Mine(5000, TestTime(10000), 0).TestStarted().TestStateSinceHeight(3000)
 255                             .Mine(5999, TestTime(20000), 0).TestStarted().TestStateSinceHeight(3000)
 256                             .Mine(6000, TestTime(20000), 0).TestFailed().TestStateSinceHeight(6000)
 257                             .Mine(7000, TestTime(20000), 0x100).TestFailed().TestStateSinceHeight(6000)
 258                             .Mine(24000, TestTime(20000), 0x100).TestFailed().TestStateSinceHeight(6000) // stay in FAILED no matter how much we signal
 259          ;
 260      }
 261  }
 262  
 263  struct BlockVersionTest : BasicTestingSetup {
 264  /** Check that ComputeBlockVersion will set the appropriate bit correctly */
 265  void check_computeblockversion(VersionBitsCache& versionbitscache, const Consensus::Params& params, Consensus::DeploymentPos dep)
 266  {
 267      // Clear the cache every time
 268      versionbitscache.Clear();
 269  
 270      int64_t bit = params.vDeployments[dep].bit;
 271      int64_t nStartTime = params.vDeployments[dep].nStartTime;
 272      int64_t nTimeout = params.vDeployments[dep].nTimeout;
 273      int min_activation_height = params.vDeployments[dep].min_activation_height;
 274  
 275      // should not be any signalling for first block
 276      BOOST_CHECK_EQUAL(versionbitscache.ComputeBlockVersion(nullptr, params), VERSIONBITS_TOP_BITS);
 277  
 278      // always/never active deployments shouldn't need to be tested further
 279      if (nStartTime == Consensus::BIP9Deployment::ALWAYS_ACTIVE ||
 280          nStartTime == Consensus::BIP9Deployment::NEVER_ACTIVE)
 281      {
 282          BOOST_CHECK_EQUAL(min_activation_height, 0);
 283          BOOST_CHECK_EQUAL(nTimeout, Consensus::BIP9Deployment::NO_TIMEOUT);
 284          return;
 285      }
 286  
 287      BOOST_REQUIRE(nStartTime < nTimeout);
 288      BOOST_REQUIRE(nStartTime >= 0);
 289      BOOST_REQUIRE(nTimeout <= std::numeric_limits<uint32_t>::max() || nTimeout == Consensus::BIP9Deployment::NO_TIMEOUT);
 290      BOOST_REQUIRE(0 <= bit && bit < 32);
 291      // Make sure that no deployment tries to set an invalid bit.
 292      BOOST_REQUIRE(((1 << bit) & VERSIONBITS_TOP_MASK) == 0);
 293      BOOST_REQUIRE(min_activation_height >= 0);
 294      // Check min_activation_height is on a retarget boundary
 295      BOOST_REQUIRE_EQUAL(min_activation_height % params.nMinerConfirmationWindow, 0U);
 296  
 297      const uint32_t bitmask{versionbitscache.Mask(params, dep)};
 298      BOOST_CHECK_EQUAL(bitmask, uint32_t{1} << bit);
 299  
 300      // In the first chain, test that the bit is set by CBV until it has failed.
 301      // In the second chain, test the bit is set by CBV while STARTED and
 302      // LOCKED-IN, and then no longer set while ACTIVE.
 303      VersionBitsTester firstChain{m_rng}, secondChain{m_rng};
 304  
 305      int64_t nTime = nStartTime;
 306  
 307      const CBlockIndex *lastBlock = nullptr;
 308  
 309      // Before MedianTimePast of the chain has crossed nStartTime, the bit
 310      // should not be set.
 311      if (nTime == 0) {
 312          // since CBlockIndex::nTime is uint32_t we can't represent any
 313          // earlier time, so will transition from DEFINED to STARTED at the
 314          // end of the first period by mining blocks at nTime == 0
 315          lastBlock = firstChain.Mine(params.nMinerConfirmationWindow - 1, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
 316          BOOST_CHECK_EQUAL(versionbitscache.ComputeBlockVersion(lastBlock, params) & (1 << bit), 0);
 317          lastBlock = firstChain.Mine(params.nMinerConfirmationWindow, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
 318          BOOST_CHECK((versionbitscache.ComputeBlockVersion(lastBlock, params) & (1 << bit)) != 0);
 319          // then we'll keep mining at nStartTime...
 320      } else {
 321          // use a time 1s earlier than start time to check we stay DEFINED
 322          --nTime;
 323  
 324          // Start generating blocks before nStartTime
 325          lastBlock = firstChain.Mine(params.nMinerConfirmationWindow, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
 326          BOOST_CHECK_EQUAL(versionbitscache.ComputeBlockVersion(lastBlock, params) & (1 << bit), 0);
 327  
 328          // Mine more blocks (4 less than the adjustment period) at the old time, and check that CBV isn't setting the bit yet.
 329          for (uint32_t i = 1; i < params.nMinerConfirmationWindow - 4; i++) {
 330              lastBlock = firstChain.Mine(params.nMinerConfirmationWindow + i, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
 331              BOOST_CHECK_EQUAL(versionbitscache.ComputeBlockVersion(lastBlock, params) & (1 << bit), 0);
 332          }
 333          // Now mine 5 more blocks at the start time -- MTP should not have passed yet, so
 334          // CBV should still not yet set the bit.
 335          nTime = nStartTime;
 336          for (uint32_t i = params.nMinerConfirmationWindow - 4; i <= params.nMinerConfirmationWindow; i++) {
 337              lastBlock = firstChain.Mine(params.nMinerConfirmationWindow + i, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
 338              BOOST_CHECK_EQUAL(versionbitscache.ComputeBlockVersion(lastBlock, params) & (1 << bit), 0);
 339          }
 340          // Next we will advance to the next period and transition to STARTED,
 341      }
 342  
 343      lastBlock = firstChain.Mine(params.nMinerConfirmationWindow * 3, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
 344      // so ComputeBlockVersion should now set the bit,
 345      BOOST_CHECK((versionbitscache.ComputeBlockVersion(lastBlock, params) & (1 << bit)) != 0);
 346      // and should also be using the VERSIONBITS_TOP_BITS.
 347      BOOST_CHECK_EQUAL(versionbitscache.ComputeBlockVersion(lastBlock, params) & VERSIONBITS_TOP_MASK, VERSIONBITS_TOP_BITS);
 348  
 349      // Check that ComputeBlockVersion will set the bit until nTimeout
 350      nTime += 600;
 351      uint32_t blocksToMine = params.nMinerConfirmationWindow * 2; // test blocks for up to 2 time periods
 352      uint32_t nHeight = params.nMinerConfirmationWindow * 3;
 353      // These blocks are all before nTimeout is reached.
 354      while (nTime < nTimeout && blocksToMine > 0) {
 355          lastBlock = firstChain.Mine(nHeight+1, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
 356          BOOST_CHECK((versionbitscache.ComputeBlockVersion(lastBlock, params) & (1 << bit)) != 0);
 357          BOOST_CHECK_EQUAL(versionbitscache.ComputeBlockVersion(lastBlock, params) & VERSIONBITS_TOP_MASK, VERSIONBITS_TOP_BITS);
 358          blocksToMine--;
 359          nTime += 600;
 360          nHeight += 1;
 361      }
 362  
 363      if (nTimeout != Consensus::BIP9Deployment::NO_TIMEOUT) {
 364          // can reach any nTimeout other than NO_TIMEOUT due to earlier BOOST_REQUIRE
 365  
 366          nTime = nTimeout;
 367  
 368          // finish the last period before we start timing out
 369          while (nHeight % params.nMinerConfirmationWindow != 0) {
 370              lastBlock = firstChain.Mine(nHeight+1, nTime - 1, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
 371              BOOST_CHECK((versionbitscache.ComputeBlockVersion(lastBlock, params) & (1 << bit)) != 0);
 372              nHeight += 1;
 373          }
 374  
 375          // FAILED is only triggered at the end of a period, so CBV should be setting
 376          // the bit until the period transition.
 377          for (uint32_t i = 0; i < params.nMinerConfirmationWindow - 1; i++) {
 378              lastBlock = firstChain.Mine(nHeight+1, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
 379              BOOST_CHECK((versionbitscache.ComputeBlockVersion(lastBlock, params) & (1 << bit)) != 0);
 380              nHeight += 1;
 381          }
 382          // The next block should trigger no longer setting the bit.
 383          lastBlock = firstChain.Mine(nHeight+1, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
 384          BOOST_CHECK_EQUAL(versionbitscache.ComputeBlockVersion(lastBlock, params) & (1 << bit), 0);
 385      }
 386  
 387      // On a new chain:
 388      // verify that the bit will be set after lock-in, and then stop being set
 389      // after activation.
 390      nTime = nStartTime;
 391  
 392      // Mine one period worth of blocks, and check that the bit will be on for the
 393      // next period.
 394      lastBlock = secondChain.Mine(params.nMinerConfirmationWindow, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
 395      BOOST_CHECK((versionbitscache.ComputeBlockVersion(lastBlock, params) & (1 << bit)) != 0);
 396  
 397      // Mine another period worth of blocks, signaling the new bit.
 398      lastBlock = secondChain.Mine(params.nMinerConfirmationWindow * 2, nTime, VERSIONBITS_TOP_BITS | (1<<bit)).Tip();
 399      // After one period of setting the bit on each block, it should have locked in.
 400      // We keep setting the bit for one more period though, until activation.
 401      BOOST_CHECK((versionbitscache.ComputeBlockVersion(lastBlock, params) & (1 << bit)) != 0);
 402  
 403      // Now check that we keep mining the block until the end of this period, and
 404      // then stop at the beginning of the next period.
 405      lastBlock = secondChain.Mine((params.nMinerConfirmationWindow * 3) - 1, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
 406      BOOST_CHECK((versionbitscache.ComputeBlockVersion(lastBlock, params) & (1 << bit)) != 0);
 407      lastBlock = secondChain.Mine(params.nMinerConfirmationWindow * 3, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
 408  
 409      if (lastBlock->nHeight + 1 < min_activation_height) {
 410          // check signalling continues while min_activation_height is not reached
 411          lastBlock = secondChain.Mine(min_activation_height - 1, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
 412          BOOST_CHECK((versionbitscache.ComputeBlockVersion(lastBlock, params) & (1 << bit)) != 0);
 413          // then reach min_activation_height, which was already REQUIRE'd to start a new period
 414          lastBlock = secondChain.Mine(min_activation_height, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
 415      }
 416  
 417      // Check that we don't signal after activation
 418      BOOST_CHECK_EQUAL(versionbitscache.ComputeBlockVersion(lastBlock, params) & (1 << bit), 0);
 419  }
 420  }; // struct BlockVersionTest
 421  
 422  BOOST_FIXTURE_TEST_CASE(versionbits_computeblockversion, BlockVersionTest)
 423  {
 424      VersionBitsCache vbcache;
 425  
 426      // check that any deployment on any chain can conceivably reach both
 427      // ACTIVE and FAILED states in roughly the way we expect
 428      for (const auto& chain_type: {ChainType::MAIN, ChainType::TESTNET, ChainType::TESTNET4, ChainType::SIGNET, ChainType::REGTEST}) {
 429          const auto chainParams = CreateChainParams(*m_node.args, chain_type);
 430          uint32_t chain_all_vbits{0};
 431          for (int i = 0; i < (int)Consensus::MAX_VERSION_BITS_DEPLOYMENTS; ++i) {
 432              const auto dep = static_cast<Consensus::DeploymentPos>(i);
 433              // Check that no bits are reused (within the same chain). This is
 434              // disallowed because the transition to FAILED (on timeout) does
 435              // not take precedence over STARTED/LOCKED_IN. So all softforks on
 436              // the same bit might overlap, even when non-overlapping start-end
 437              // times are picked.
 438              const uint32_t dep_mask{vbcache.Mask(chainParams->GetConsensus(), dep)};
 439              BOOST_CHECK(!(chain_all_vbits & dep_mask));
 440              chain_all_vbits |= dep_mask;
 441              check_computeblockversion(vbcache, chainParams->GetConsensus(), dep);
 442          }
 443      }
 444  
 445      {
 446          // Use regtest/testdummy to ensure we always exercise some
 447          // deployment that's not always/never active
 448          ArgsManager args;
 449          args.ForceSetArg("-vbparams", "testdummy:1199145601:1230767999"); // January 1, 2008 - December 31, 2008
 450          const auto chainParams = CreateChainParams(args, ChainType::REGTEST);
 451          check_computeblockversion(vbcache, chainParams->GetConsensus(), Consensus::DEPLOYMENT_TESTDUMMY);
 452      }
 453  
 454      {
 455          // Use regtest/testdummy to ensure we always exercise the
 456          // min_activation_height test, even if we're not using that in a
 457          // live deployment
 458          ArgsManager args;
 459          args.ForceSetArg("-vbparams", "testdummy:1199145601:1230767999:403200"); // January 1, 2008 - December 31, 2008, min act height 403200
 460          const auto chainParams = CreateChainParams(args, ChainType::REGTEST);
 461          check_computeblockversion(vbcache, chainParams->GetConsensus(), Consensus::DEPLOYMENT_TESTDUMMY);
 462      }
 463  }
 464  
 465  /**
 466   * Test condition checker with max_activation_height for mandatory activation deadline.
 467   * When max_activation_height is set, the deployment forces LOCKED_IN one period before
 468   * max_activation_height, even if threshold signaling was not met.
 469   */
 470  class TestMaxActivationHeightConditionChecker : public AbstractThresholdConditionChecker
 471  {
 472  private:
 473      mutable ThresholdConditionCache cache;
 474      int m_max_activation_height;
 475  
 476  public:
 477      explicit TestMaxActivationHeightConditionChecker(int max_height) : m_max_activation_height(max_height) {}
 478  
 479      int64_t BeginTime(const Consensus::Params& params) const override { return 0; } // Start immediately
 480      int64_t EndTime(const Consensus::Params& params) const override { return Consensus::BIP9Deployment::NO_TIMEOUT; }
 481      int Period(const Consensus::Params& params) const override { return 144; }
 482      int Threshold(const Consensus::Params& params) const override { return 108; } // 75%
 483      int MaxActivationHeight(const Consensus::Params& params) const override { return m_max_activation_height; }
 484      bool Condition(const CBlockIndex* pindex, const Consensus::Params& params) const override { return (pindex->nVersion & 0x100); }
 485  
 486      ThresholdState GetStateFor(const CBlockIndex* pindexPrev) const { return AbstractThresholdConditionChecker::GetStateFor(pindexPrev, paramsDummy, cache); }
 487      int GetStateSinceHeightFor(const CBlockIndex* pindexPrev) const { return AbstractThresholdConditionChecker::GetStateSinceHeightFor(pindexPrev, paramsDummy, cache); }
 488      void ClearCache() { cache.clear(); }
 489  };
 490  
 491  BOOST_AUTO_TEST_CASE(versionbits_max_activation_height)
 492  {
 493      // Test that max_activation_height forces LOCKED_IN one period before max_activation_height
 494      // even without sufficient signaling.
 495      //
 496      // Timeline with period=144, max_activation_height=432:
 497      // - Period 0 (0-143): DEFINED
 498      // - Period 1 (144-287): STARTED (no signaling -> normally would stay STARTED)
 499      // - Period 2 (288-431): LOCKED_IN (forced because 288 >= 432 - 144)
 500      // - Period 3 (432+): ACTIVE
 501  
 502      std::vector<CBlockIndex*> blocks;
 503      auto cleanup = [&blocks]() {
 504          for (auto* b : blocks) delete b;
 505          blocks.clear();
 506      };
 507  
 508      // max_activation_height = 432 (period 3 start)
 509      TestMaxActivationHeightConditionChecker checker(432);
 510  
 511      // Helper to create blocks
 512      auto mine_block = [&blocks](int32_t nVersion) -> CBlockIndex* {
 513          CBlockIndex* pindex = new CBlockIndex();
 514          pindex->nHeight = blocks.size();
 515          pindex->pprev = blocks.empty() ? nullptr : blocks.back();
 516          pindex->nTime = 1415926536 + 600 * pindex->nHeight;
 517          pindex->nVersion = nVersion;
 518          pindex->BuildSkip();
 519          blocks.push_back(pindex);
 520          return pindex;
 521      };
 522  
 523      // Mine through period 0 (DEFINED) - 144 blocks (0-143)
 524      for (int i = 0; i < 144; i++) {
 525          mine_block(0); // No signaling
 526      }
 527      BOOST_CHECK_EQUAL(blocks.back()->nHeight, 143);
 528      // At tip 143, next block (144) would be STARTED
 529      BOOST_CHECK(checker.GetStateFor(blocks.back()) == ThresholdState::STARTED);
 530      BOOST_CHECK_EQUAL(checker.GetStateSinceHeightFor(blocks.back()), 144);
 531  
 532      // Mine through period 1 (STARTED) without signaling - blocks 144-287
 533      for (int i = 0; i < 144; i++) {
 534          mine_block(0); // No signaling
 535      }
 536      BOOST_CHECK_EQUAL(blocks.back()->nHeight, 287);
 537      // At tip 287, next block (288) would be LOCKED_IN due to max_activation_height
 538      // 288 >= 432 - 144, so forced LOCKED_IN
 539      BOOST_CHECK(checker.GetStateFor(blocks.back()) == ThresholdState::LOCKED_IN);
 540      BOOST_CHECK_EQUAL(checker.GetStateSinceHeightFor(blocks.back()), 288);
 541  
 542      // Mine through period 2 (LOCKED_IN) - blocks 288-431
 543      for (int i = 0; i < 144; i++) {
 544          mine_block(0);
 545      }
 546      BOOST_CHECK_EQUAL(blocks.back()->nHeight, 431);
 547      // At tip 431, next block (432) would be ACTIVE
 548      BOOST_CHECK(checker.GetStateFor(blocks.back()) == ThresholdState::ACTIVE);
 549      BOOST_CHECK_EQUAL(checker.GetStateSinceHeightFor(blocks.back()), 432);
 550  
 551      // Mine into period 3 (ACTIVE) - blocks 432+
 552      for (int i = 0; i < 10; i++) {
 553          mine_block(0);
 554      }
 555      BOOST_CHECK_EQUAL(blocks.back()->nHeight, 441);
 556      BOOST_CHECK(checker.GetStateFor(blocks.back()) == ThresholdState::ACTIVE);
 557      BOOST_CHECK_EQUAL(checker.GetStateSinceHeightFor(blocks.back()), 432);
 558  
 559      cleanup();
 560  
 561      // Test 2: Verify that signaling still works to activate earlier than max_activation_height
 562      TestMaxActivationHeightConditionChecker checker2(1000); // max_activation_height far in future
 563  
 564      // Period 0: DEFINED
 565      for (int i = 0; i < 144; i++) {
 566          mine_block(0);
 567      }
 568      BOOST_CHECK(checker2.GetStateFor(blocks.back()) == ThresholdState::STARTED);
 569  
 570      // Period 1: Signal 108+ blocks (threshold)
 571      for (int i = 0; i < 108; i++) {
 572          mine_block(0x100); // Signal
 573      }
 574      for (int i = 0; i < 36; i++) {
 575          mine_block(0); // No signal
 576      }
 577      BOOST_CHECK_EQUAL(blocks.back()->nHeight, 287);
 578      // Should be LOCKED_IN via signaling, not via max_activation_height
 579      BOOST_CHECK(checker2.GetStateFor(blocks.back()) == ThresholdState::LOCKED_IN);
 580      BOOST_CHECK_EQUAL(checker2.GetStateSinceHeightFor(blocks.back()), 288);
 581  
 582      // Period 2: LOCKED_IN -> ACTIVE
 583      for (int i = 0; i < 144; i++) {
 584          mine_block(0);
 585      }
 586      BOOST_CHECK(checker2.GetStateFor(blocks.back()) == ThresholdState::ACTIVE);
 587      BOOST_CHECK_EQUAL(checker2.GetStateSinceHeightFor(blocks.back()), 432);
 588  
 589      cleanup();
 590  }
 591  
 592  BOOST_AUTO_TEST_CASE(versionbits_max_activation_height_boundary)
 593  {
 594      // Test edge case: verify exact boundary where LOCKED_IN is forced
 595      // With period=144 and max_activation_height=432:
 596      // - At height 287, next block is 288, which is >= 432-144=288, so LOCKED_IN
 597      // - At height 286, next block is 287, which is < 288, so would stay STARTED
 598  
 599      std::vector<CBlockIndex*> blocks;
 600      auto cleanup = [&blocks]() {
 601          for (auto* b : blocks) delete b;
 602          blocks.clear();
 603      };
 604  
 605      TestMaxActivationHeightConditionChecker checker(432);
 606  
 607      auto mine_block = [&blocks](int32_t nVersion) -> CBlockIndex* {
 608          CBlockIndex* pindex = new CBlockIndex();
 609          pindex->nHeight = blocks.size();
 610          pindex->pprev = blocks.empty() ? nullptr : blocks.back();
 611          pindex->nTime = 1415926536 + 600 * pindex->nHeight;
 612          pindex->nVersion = nVersion;
 613          pindex->BuildSkip();
 614          blocks.push_back(pindex);
 615          return pindex;
 616      };
 617  
 618      // Mine to height 143 (end of period 0)
 619      for (int i = 0; i < 144; i++) {
 620          mine_block(0);
 621      }
 622      BOOST_CHECK_EQUAL(blocks.back()->nHeight, 143);
 623      // State for block 144 is STARTED
 624      BOOST_CHECK(checker.GetStateFor(blocks.back()) == ThresholdState::STARTED);
 625  
 626      // Mine period 1 without signaling (blocks 144-287)
 627      // But stop at block 286 first to check boundary
 628      for (int i = 0; i < 143; i++) {
 629          mine_block(0);
 630      }
 631      BOOST_CHECK_EQUAL(blocks.back()->nHeight, 286);
 632      // At tip 286, next block 287 is still in STARTED period
 633      // State is still STARTED
 634      BOOST_CHECK(checker.GetStateFor(blocks.back()) == ThresholdState::STARTED);
 635  
 636      // Mine block 287 (last block of period 1)
 637      mine_block(0);
 638      BOOST_CHECK_EQUAL(blocks.back()->nHeight, 287);
 639      // At tip 287, state for next block (288) is computed
 640      // 288 >= 432 - 144 = 288, so LOCKED_IN
 641      BOOST_CHECK(checker.GetStateFor(blocks.back()) == ThresholdState::LOCKED_IN);
 642      BOOST_CHECK_EQUAL(checker.GetStateSinceHeightFor(blocks.back()), 288);
 643  
 644      cleanup();
 645  }
 646  
 647  BOOST_FIXTURE_TEST_CASE(versionbits_active_duration, BasicTestingSetup)
 648  {
 649      // Test active_duration parameter via -vbparams
 650      // Format: deployment:start:timeout:min_activation_height:max_activation_height:active_duration
 651      //
 652      // This tests that the parameter is parsed correctly. The actual expiry logic
 653      // is tested in DeploymentActiveAt/DeploymentActiveAfter which use active_duration.
 654  
 655      {
 656          ArgsManager args;
 657          // start=0, timeout=never, min_height=0, max_height=INT_MAX (disabled), active_duration=144
 658          args.ForceSetArg("-vbparams", "testdummy:0:999999999999:0:2147483647:144");
 659          const auto chainParams = CreateChainParams(args, ChainType::REGTEST);
 660          const auto& deployment = chainParams->GetConsensus().vDeployments[Consensus::DEPLOYMENT_TESTDUMMY];
 661  
 662          BOOST_CHECK_EQUAL(deployment.nStartTime, 0);
 663          BOOST_CHECK_EQUAL(deployment.nTimeout, 999999999999);
 664          BOOST_CHECK_EQUAL(deployment.min_activation_height, 0);
 665          BOOST_CHECK_EQUAL(deployment.max_activation_height, std::numeric_limits<int>::max());
 666          BOOST_CHECK_EQUAL(deployment.active_duration, 144);
 667      }
 668  
 669      {
 670          ArgsManager args;
 671          // Test with max_activation_height set
 672          // start=0, timeout=NO_TIMEOUT, min_height=288, max_height=432, active_duration=1008 (144*7)
 673          // NO_TIMEOUT = INT64_MAX = 9223372036854775807
 674          args.ForceSetArg("-vbparams", "testdummy:0:9223372036854775807:288:432:1008");
 675          const auto chainParams = CreateChainParams(args, ChainType::REGTEST);
 676          const auto& deployment = chainParams->GetConsensus().vDeployments[Consensus::DEPLOYMENT_TESTDUMMY];
 677  
 678          BOOST_CHECK_EQUAL(deployment.min_activation_height, 288);
 679          BOOST_CHECK_EQUAL(deployment.max_activation_height, 432);
 680          BOOST_CHECK_EQUAL(deployment.active_duration, 1008);
 681      }
 682  
 683      {
 684          ArgsManager args;
 685          // Test permanent deployment (active_duration = INT_MAX)
 686          args.ForceSetArg("-vbparams", "testdummy:0:999999999999:0:2147483647:2147483647");
 687          const auto chainParams = CreateChainParams(args, ChainType::REGTEST);
 688          const auto& deployment = chainParams->GetConsensus().vDeployments[Consensus::DEPLOYMENT_TESTDUMMY];
 689  
 690          BOOST_CHECK_EQUAL(deployment.active_duration, std::numeric_limits<int>::max());
 691      }
 692  }
 693  
 694  BOOST_FIXTURE_TEST_CASE(versionbits_max_activation_height_parsing, BasicTestingSetup)
 695  {
 696      // Test max_activation_height parameter via -vbparams
 697  
 698      {
 699          ArgsManager args;
 700          // Test with max_activation_height=432 (mandatory activation deadline)
 701          // NO_TIMEOUT = INT64_MAX = 9223372036854775807
 702          args.ForceSetArg("-vbparams", "testdummy:0:9223372036854775807:0:432:2147483647");
 703          const auto chainParams = CreateChainParams(args, ChainType::REGTEST);
 704          const auto& deployment = chainParams->GetConsensus().vDeployments[Consensus::DEPLOYMENT_TESTDUMMY];
 705  
 706          BOOST_CHECK_EQUAL(deployment.max_activation_height, 432);
 707          // active_duration should be permanent when not specified differently
 708          BOOST_CHECK_EQUAL(deployment.active_duration, std::numeric_limits<int>::max());
 709      }
 710  
 711      {
 712          ArgsManager args;
 713          // Test combined: max_activation_height + active_duration (RDTS)
 714          // NO_TIMEOUT = INT64_MAX = 9223372036854775807
 715          args.ForceSetArg("-vbparams", "testdummy:0:9223372036854775807:288:576:144");
 716          const auto chainParams = CreateChainParams(args, ChainType::REGTEST);
 717          const auto& deployment = chainParams->GetConsensus().vDeployments[Consensus::DEPLOYMENT_TESTDUMMY];
 718  
 719          BOOST_CHECK_EQUAL(deployment.min_activation_height, 288);
 720          BOOST_CHECK_EQUAL(deployment.max_activation_height, 576);
 721          BOOST_CHECK_EQUAL(deployment.active_duration, 144);
 722      }
 723  }
 724  
 725  /**
 726   * Test condition checker for temporary deployments with active_duration.
 727   * After active_duration blocks past activation, the state transitions to EXPIRED.
 728   */
 729  class TestTemporaryDeploymentConditionChecker : public AbstractThresholdConditionChecker
 730  {
 731  private:
 732      mutable ThresholdConditionCache cache;
 733      int m_active_duration;
 734  
 735  public:
 736      explicit TestTemporaryDeploymentConditionChecker(int active_duration) : m_active_duration(active_duration) {}
 737  
 738      int64_t BeginTime(const Consensus::Params& params) const override { return 0; } // Start immediately
 739      int64_t EndTime(const Consensus::Params& params) const override { return Consensus::BIP9Deployment::NO_TIMEOUT; }
 740      int Period(const Consensus::Params& params) const override { return 144; }
 741      int Threshold(const Consensus::Params& params) const override { return 108; } // 75%
 742      int ActiveDuration(const Consensus::Params& params) const override { return m_active_duration; }
 743      bool Condition(const CBlockIndex* pindex, const Consensus::Params& params) const override { return (pindex->nVersion & 0x100); }
 744  
 745      ThresholdState GetStateFor(const CBlockIndex* pindexPrev) const { return AbstractThresholdConditionChecker::GetStateFor(pindexPrev, paramsDummy, cache); }
 746      int GetStateSinceHeightFor(const CBlockIndex* pindexPrev) const { return AbstractThresholdConditionChecker::GetStateSinceHeightFor(pindexPrev, paramsDummy, cache); }
 747      void ClearCache() { cache.clear(); }
 748  };
 749  
 750  BOOST_AUTO_TEST_CASE(versionbits_expired_state)
 751  {
 752      // Test that a temporary deployment transitions from ACTIVE to EXPIRED
 753      // after active_duration blocks past activation.
 754      //
 755      // Timeline with period=144, active_duration=288:
 756      // - Period 0 (0-143): DEFINED
 757      // - Period 1 (144-287): STARTED, signal enough to lock in
 758      // - Period 2 (288-431): LOCKED_IN
 759      // - Period 3 (432-575): ACTIVE (activation_height=432)
 760      // - Period 4 (576-719): ACTIVE (blocks in this period are ACTIVE)
 761      // - At pindexPrev=719: EXPIRED for block 720+ (720 >= 432 + 288)
 762  
 763      std::vector<CBlockIndex*> blocks;
 764      auto cleanup = [&blocks]() {
 765          for (auto* b : blocks) delete b;
 766          blocks.clear();
 767      };
 768  
 769      TestTemporaryDeploymentConditionChecker checker(288);
 770  
 771      auto mine_block = [&blocks](int32_t nVersion) -> CBlockIndex* {
 772          CBlockIndex* pindex = new CBlockIndex();
 773          pindex->nHeight = blocks.size();
 774          pindex->pprev = blocks.empty() ? nullptr : blocks.back();
 775          pindex->nTime = 1415926536 + 600 * pindex->nHeight;
 776          pindex->nVersion = nVersion;
 777          pindex->BuildSkip();
 778          blocks.push_back(pindex);
 779          return pindex;
 780      };
 781  
 782      // Period 0: DEFINED (blocks 0-143)
 783      for (int i = 0; i < 144; i++) {
 784          mine_block(0);
 785      }
 786      BOOST_CHECK(checker.GetStateFor(blocks.back()) == ThresholdState::STARTED);
 787  
 788      // Period 1: STARTED, signal all blocks to lock in (blocks 144-287)
 789      for (int i = 0; i < 144; i++) {
 790          mine_block(0x100); // Signal
 791      }
 792      BOOST_CHECK(checker.GetStateFor(blocks.back()) == ThresholdState::LOCKED_IN);
 793      BOOST_CHECK_EQUAL(checker.GetStateSinceHeightFor(blocks.back()), 288);
 794  
 795      // Period 2: LOCKED_IN (blocks 288-431)
 796      for (int i = 0; i < 144; i++) {
 797          mine_block(0);
 798      }
 799      BOOST_CHECK(checker.GetStateFor(blocks.back()) == ThresholdState::ACTIVE);
 800      BOOST_CHECK_EQUAL(checker.GetStateSinceHeightFor(blocks.back()), 432);
 801  
 802      // Period 3: ACTIVE (blocks 432-575), activation_height = 432
 803      for (int i = 0; i < 144; i++) {
 804          mine_block(0);
 805      }
 806      BOOST_CHECK(checker.GetStateFor(blocks.back()) == ThresholdState::ACTIVE);
 807      BOOST_CHECK_EQUAL(checker.GetStateSinceHeightFor(blocks.back()), 432);
 808  
 809      // Period 4 (blocks 576-719): blocks in this period are ACTIVE,
 810      // but at pindexPrev=719 the state for block 720+ is EXPIRED (720 >= 432 + 288)
 811      for (int i = 0; i < 144; i++) {
 812          mine_block(0);
 813      }
 814      BOOST_CHECK(checker.GetStateFor(blocks.back()) == ThresholdState::EXPIRED);
 815      BOOST_CHECK_EQUAL(checker.GetStateSinceHeightFor(blocks.back()), 720);
 816  
 817      // Verify EXPIRED is terminal
 818      for (int i = 0; i < 144; i++) {
 819          mine_block(0x100); // Signal shouldn't matter
 820      }
 821      BOOST_CHECK(checker.GetStateFor(blocks.back()) == ThresholdState::EXPIRED);
 822      BOOST_CHECK_EQUAL(checker.GetStateSinceHeightFor(blocks.back()), 720);
 823  
 824      cleanup();
 825  }
 826  
 827  BOOST_AUTO_TEST_CASE(versionbits_expired_unaligned_duration_rejected)
 828  {
 829      // Test that active_duration that is NOT a multiple of the period is rejected.
 830      ArgsManager args;
 831      args.ForceSetArg("-vbparams", "testdummy:0:9223372036854775807:0:432:200");
 832      BOOST_CHECK_THROW(CreateChainParams(args, ChainType::REGTEST), std::runtime_error);
 833  }
 834  
 835  BOOST_AUTO_TEST_CASE(versionbits_expired_minimum_duration)
 836  {
 837      // Test active_duration = 1 period (144). Minimum useful duration.
 838      // Activation at 432, so 432+144=576. At pindexPrev=575, state for 576+:
 839      // 576 >= 576 -> EXPIRED. Only one period of ACTIVE.
 840  
 841      std::vector<CBlockIndex*> blocks;
 842      auto cleanup = [&blocks]() {
 843          for (auto* b : blocks) delete b;
 844          blocks.clear();
 845      };
 846  
 847      TestTemporaryDeploymentConditionChecker checker(144);
 848  
 849      auto mine_block = [&blocks](int32_t nVersion) -> CBlockIndex* {
 850          CBlockIndex* pindex = new CBlockIndex();
 851          pindex->nHeight = blocks.size();
 852          pindex->pprev = blocks.empty() ? nullptr : blocks.back();
 853          pindex->nTime = 1415926536 + 600 * pindex->nHeight;
 854          pindex->nVersion = nVersion;
 855          pindex->BuildSkip();
 856          blocks.push_back(pindex);
 857          return pindex;
 858      };
 859  
 860      // Period 0: DEFINED (0-143)
 861      for (int i = 0; i < 144; i++) mine_block(0);
 862      BOOST_CHECK(checker.GetStateFor(blocks.back()) == ThresholdState::STARTED);
 863  
 864      // Period 1: Signal (144-287)
 865      for (int i = 0; i < 144; i++) mine_block(0x100);
 866      BOOST_CHECK(checker.GetStateFor(blocks.back()) == ThresholdState::LOCKED_IN);
 867  
 868      // Period 2: LOCKED_IN (288-431)
 869      for (int i = 0; i < 144; i++) mine_block(0);
 870      BOOST_CHECK(checker.GetStateFor(blocks.back()) == ThresholdState::ACTIVE);
 871      BOOST_CHECK_EQUAL(checker.GetStateSinceHeightFor(blocks.back()), 432);
 872  
 873      // Period 3 (432-575): ACTIVE for this period, but state for 576+:
 874      // 576 >= 432 + 144 = 576 -> EXPIRED immediately at next boundary
 875      for (int i = 0; i < 144; i++) mine_block(0);
 876      BOOST_CHECK(checker.GetStateFor(blocks.back()) == ThresholdState::EXPIRED);
 877      BOOST_CHECK_EQUAL(checker.GetStateSinceHeightFor(blocks.back()), 576);
 878  
 879      cleanup();
 880  }
 881  
 882  BOOST_AUTO_TEST_CASE(versionbits_expired_cold_cache)
 883  {
 884      // Test that clearing the cache and re-querying correctly recovers
 885      // the EXPIRED state. This exercises the activation_height recovery
 886      // path in GetStateFor where it calls GetStateSinceHeightFor to find
 887      // when ACTIVE started.
 888  
 889      std::vector<CBlockIndex*> blocks;
 890      auto cleanup = [&blocks]() {
 891          for (auto* b : blocks) delete b;
 892          blocks.clear();
 893      };
 894  
 895      TestTemporaryDeploymentConditionChecker checker(288);
 896  
 897      auto mine_block = [&blocks](int32_t nVersion) -> CBlockIndex* {
 898          CBlockIndex* pindex = new CBlockIndex();
 899          pindex->nHeight = blocks.size();
 900          pindex->pprev = blocks.empty() ? nullptr : blocks.back();
 901          pindex->nTime = 1415926536 + 600 * pindex->nHeight;
 902          pindex->nVersion = nVersion;
 903          pindex->BuildSkip();
 904          blocks.push_back(pindex);
 905          return pindex;
 906      };
 907  
 908      // Build chain through to EXPIRED (same as basic test)
 909      // Period 0: DEFINED (0-143)
 910      for (int i = 0; i < 144; i++) mine_block(0);
 911      // Period 1: Signal (144-287)
 912      for (int i = 0; i < 144; i++) mine_block(0x100);
 913      // Period 2: LOCKED_IN (288-431)
 914      for (int i = 0; i < 144; i++) mine_block(0);
 915      // Period 3: ACTIVE (432-575)
 916      for (int i = 0; i < 144; i++) mine_block(0);
 917      // Period 4: (576-719) -> EXPIRED at 720
 918      for (int i = 0; i < 144; i++) mine_block(0);
 919  
 920      // Verify EXPIRED with warm cache
 921      BOOST_CHECK(checker.GetStateFor(blocks.back()) == ThresholdState::EXPIRED);
 922      BOOST_CHECK_EQUAL(checker.GetStateSinceHeightFor(blocks.back()), 720);
 923  
 924      // Clear cache completely — simulates node restart
 925      checker.ClearCache();
 926  
 927      // Re-query: must walk from genesis, recover activation_height, compute EXPIRED
 928      BOOST_CHECK(checker.GetStateFor(blocks.back()) == ThresholdState::EXPIRED);
 929      BOOST_CHECK_EQUAL(checker.GetStateSinceHeightFor(blocks.back()), 720);
 930  
 931      // Also verify intermediate states are correct after cache rebuild
 932      checker.ClearCache();
 933      // Query at end of period 3 (pindexPrev=575) — should be ACTIVE
 934      BOOST_CHECK(checker.GetStateFor(blocks[575]) == ThresholdState::ACTIVE);
 935      BOOST_CHECK_EQUAL(checker.GetStateSinceHeightFor(blocks[575]), 432);
 936  
 937      // Now query at end of period 4 with partially-populated cache
 938      // (period 3 is cached as ACTIVE from the query above)
 939      BOOST_CHECK(checker.GetStateFor(blocks.back()) == ThresholdState::EXPIRED);
 940      BOOST_CHECK_EQUAL(checker.GetStateSinceHeightFor(blocks.back()), 720);
 941  
 942      cleanup();
 943  }
 944  
 945  BOOST_AUTO_TEST_CASE(versionbits_expired_with_max_activation_height)
 946  {
 947      // Test interaction: deployment with BOTH max_activation_height AND active_duration.
 948      // max_activation_height forces LOCKED_IN, then active_duration causes EXPIRED.
 949      //
 950      // period=144, max_activation_height=432, active_duration=288
 951      // - Period 0 (0-143): DEFINED
 952      // - Period 1 (144-287): STARTED (no signaling, but forced LOCKED_IN at boundary
 953      //   because 288 >= 432 - 144)
 954      // - Period 2 (288-431): LOCKED_IN
 955      // - Period 3 (432-575): ACTIVE (activation_height=432)
 956      // - Period 4 (576-719): ACTIVE
 957      // - At pindexPrev=719: EXPIRED (720 >= 432 + 288)
 958  
 959      std::vector<CBlockIndex*> blocks;
 960      auto cleanup = [&blocks]() {
 961          for (auto* b : blocks) delete b;
 962          blocks.clear();
 963      };
 964  
 965      // Need a checker that has both max_activation_height AND active_duration
 966      class TestCombinedChecker : public AbstractThresholdConditionChecker
 967      {
 968      private:
 969          mutable ThresholdConditionCache cache;
 970      public:
 971          int64_t BeginTime(const Consensus::Params& params) const override { return 0; }
 972          int64_t EndTime(const Consensus::Params& params) const override { return Consensus::BIP9Deployment::NO_TIMEOUT; }
 973          int Period(const Consensus::Params& params) const override { return 144; }
 974          int Threshold(const Consensus::Params& params) const override { return 108; }
 975          int MaxActivationHeight(const Consensus::Params& params) const override { return 432; }
 976          int ActiveDuration(const Consensus::Params& params) const override { return 288; }
 977          bool Condition(const CBlockIndex* pindex, const Consensus::Params& params) const override { return (pindex->nVersion & 0x100); }
 978          ThresholdState GetStateFor(const CBlockIndex* pindexPrev) const { return AbstractThresholdConditionChecker::GetStateFor(pindexPrev, paramsDummy, cache); }
 979          int GetStateSinceHeightFor(const CBlockIndex* pindexPrev) const { return AbstractThresholdConditionChecker::GetStateSinceHeightFor(pindexPrev, paramsDummy, cache); }
 980      };
 981  
 982      TestCombinedChecker checker;
 983  
 984      auto mine_block = [&blocks](int32_t nVersion) -> CBlockIndex* {
 985          CBlockIndex* pindex = new CBlockIndex();
 986          pindex->nHeight = blocks.size();
 987          pindex->pprev = blocks.empty() ? nullptr : blocks.back();
 988          pindex->nTime = 1415926536 + 600 * pindex->nHeight;
 989          pindex->nVersion = nVersion;
 990          pindex->BuildSkip();
 991          blocks.push_back(pindex);
 992          return pindex;
 993      };
 994  
 995      // Period 0: DEFINED (0-143), no signaling
 996      for (int i = 0; i < 144; i++) mine_block(0);
 997      BOOST_CHECK(checker.GetStateFor(blocks.back()) == ThresholdState::STARTED);
 998  
 999      // Period 1: STARTED (144-287), no signaling — forced LOCKED_IN by max_activation_height
1000      for (int i = 0; i < 144; i++) mine_block(0);
1001      BOOST_CHECK(checker.GetStateFor(blocks.back()) == ThresholdState::LOCKED_IN);
1002      BOOST_CHECK_EQUAL(checker.GetStateSinceHeightFor(blocks.back()), 288);
1003  
1004      // Period 2: LOCKED_IN (288-431)
1005      for (int i = 0; i < 144; i++) mine_block(0);
1006      BOOST_CHECK(checker.GetStateFor(blocks.back()) == ThresholdState::ACTIVE);
1007      BOOST_CHECK_EQUAL(checker.GetStateSinceHeightFor(blocks.back()), 432);
1008  
1009      // Period 3: ACTIVE (432-575)
1010      for (int i = 0; i < 144; i++) mine_block(0);
1011      BOOST_CHECK(checker.GetStateFor(blocks.back()) == ThresholdState::ACTIVE);
1012  
1013      // Period 4: (576-719) -> EXPIRED at 720
1014      for (int i = 0; i < 144; i++) mine_block(0);
1015      BOOST_CHECK(checker.GetStateFor(blocks.back()) == ThresholdState::EXPIRED);
1016      BOOST_CHECK_EQUAL(checker.GetStateSinceHeightFor(blocks.back()), 720);
1017  
1018      cleanup();
1019  }
1020  
1021  BOOST_AUTO_TEST_CASE(versionbits_expired_zero_duration)
1022  {
1023      // Test active_duration = 0. This means the deployment expires immediately
1024      // after activation — zero blocks of enforcement.
1025      // Activation at 432, so 432 + 0 = 432. At pindexPrev=431 (end of LOCKED_IN),
1026      // state for 432+: LOCKED_IN transitions to ACTIVE, sets activation_height=432.
1027      // But that's computed for this period boundary. The ACTIVE->EXPIRED check
1028      // happens on the NEXT iteration of the walk-forward.
1029      // At pindexPrev=575 (end of period 3): 576 >= 432 + 0 = 432 -> EXPIRED.
1030      // So active_duration=0 gives exactly one period of ACTIVE, same as
1031      // active_duration=144 with period=144 (since transitions are per-period).
1032  
1033      std::vector<CBlockIndex*> blocks;
1034      auto cleanup = [&blocks]() {
1035          for (auto* b : blocks) delete b;
1036          blocks.clear();
1037      };
1038  
1039      TestTemporaryDeploymentConditionChecker checker(0);
1040  
1041      auto mine_block = [&blocks](int32_t nVersion) -> CBlockIndex* {
1042          CBlockIndex* pindex = new CBlockIndex();
1043          pindex->nHeight = blocks.size();
1044          pindex->pprev = blocks.empty() ? nullptr : blocks.back();
1045          pindex->nTime = 1415926536 + 600 * pindex->nHeight;
1046          pindex->nVersion = nVersion;
1047          pindex->BuildSkip();
1048          blocks.push_back(pindex);
1049          return pindex;
1050      };
1051  
1052      // Period 0: DEFINED (0-143)
1053      for (int i = 0; i < 144; i++) mine_block(0);
1054      // Period 1: Signal (144-287)
1055      for (int i = 0; i < 144; i++) mine_block(0x100);
1056      BOOST_CHECK(checker.GetStateFor(blocks.back()) == ThresholdState::LOCKED_IN);
1057  
1058      // Period 2: LOCKED_IN (288-431) -> ACTIVE at 432
1059      for (int i = 0; i < 144; i++) mine_block(0);
1060      BOOST_CHECK(checker.GetStateFor(blocks.back()) == ThresholdState::ACTIVE);
1061      BOOST_CHECK_EQUAL(checker.GetStateSinceHeightFor(blocks.back()), 432);
1062  
1063      // Period 3 (432-575): ACTIVE, but 576 >= 432+0 -> EXPIRED at next boundary
1064      for (int i = 0; i < 144; i++) mine_block(0);
1065      BOOST_CHECK(checker.GetStateFor(blocks.back()) == ThresholdState::EXPIRED);
1066      BOOST_CHECK_EQUAL(checker.GetStateSinceHeightFor(blocks.back()), 576);
1067  
1068      cleanup();
1069  }
1070  
1071  BOOST_AUTO_TEST_CASE(versionbits_no_signaling_after_expired)
1072  {
1073      // Test that ComputeBlockVersion does NOT set the deployment bit after EXPIRED.
1074      // Uses regtest with testdummy deployment configured with active_duration.
1075      //
1076      // ComputeBlockVersion only signals for STARTED and LOCKED_IN states.
1077      // After EXPIRED, the bit should not be set.
1078  
1079      ArgsManager args;
1080      // testdummy: start=0, timeout=never, min_height=0, max_height=INT_MAX, active_duration=144
1081      args.ForceSetArg("-vbparams", "testdummy:0:9223372036854775807:0:2147483647:144");
1082      const auto chainParams = CreateChainParams(args, ChainType::REGTEST);
1083      const auto& params = chainParams->GetConsensus();
1084  
1085      VersionBitsCache vbcache;
1086      const auto dep = Consensus::DEPLOYMENT_TESTDUMMY;
1087      const uint32_t bitmask = vbcache.Mask(params, dep);
1088      const int period = params.nMinerConfirmationWindow; // 144 for regtest
1089  
1090      std::vector<CBlockIndex*> blocks;
1091      auto cleanup = [&blocks]() {
1092          for (auto* b : blocks) delete b;
1093          blocks.clear();
1094      };
1095  
1096      auto mine_block = [&blocks](int32_t nVersion) -> CBlockIndex* {
1097          CBlockIndex* pindex = new CBlockIndex();
1098          pindex->nHeight = blocks.size();
1099          pindex->pprev = blocks.empty() ? nullptr : blocks.back();
1100          pindex->nTime = 1415926536 + 600 * pindex->nHeight;
1101          pindex->nVersion = nVersion;
1102          pindex->BuildSkip();
1103          blocks.push_back(pindex);
1104          return pindex;
1105      };
1106  
1107      // Period 0: DEFINED (0-143) — bit should not be set
1108      for (int i = 0; i < period; i++) mine_block(VERSIONBITS_TOP_BITS);
1109      BOOST_CHECK_EQUAL(vbcache.ComputeBlockVersion(blocks.back(), params) & bitmask, bitmask); // STARTED, bit set
1110  
1111      // Period 1: STARTED — signal to lock in (144-287), bit should be set
1112      for (int i = 0; i < period; i++) mine_block(VERSIONBITS_TOP_BITS | bitmask);
1113      BOOST_CHECK(vbcache.State(blocks.back(), params, dep) == ThresholdState::LOCKED_IN);
1114      BOOST_CHECK_EQUAL(vbcache.ComputeBlockVersion(blocks.back(), params) & bitmask, bitmask); // LOCKED_IN, bit set
1115  
1116      // Period 2: LOCKED_IN (288-431), bit should be set
1117      for (int i = 0; i < period; i++) mine_block(VERSIONBITS_TOP_BITS | bitmask);
1118      BOOST_CHECK(vbcache.State(blocks.back(), params, dep) == ThresholdState::ACTIVE);
1119      // ACTIVE: bit should NOT be set
1120      BOOST_CHECK_EQUAL(vbcache.ComputeBlockVersion(blocks.back(), params) & bitmask, 0u);
1121  
1122      // Period 3: ACTIVE (432-575)
1123      for (int i = 0; i < period; i++) mine_block(VERSIONBITS_TOP_BITS);
1124      BOOST_CHECK(vbcache.State(blocks.back(), params, dep) == ThresholdState::EXPIRED);
1125      // EXPIRED: bit should NOT be set
1126      BOOST_CHECK_EQUAL(vbcache.ComputeBlockVersion(blocks.back(), params) & bitmask, 0u);
1127  
1128      // Period 4: EXPIRED (576+) — verify bit stays off
1129      for (int i = 0; i < period; i++) mine_block(VERSIONBITS_TOP_BITS);
1130      BOOST_CHECK(vbcache.State(blocks.back(), params, dep) == ThresholdState::EXPIRED);
1131      BOOST_CHECK_EQUAL(vbcache.ComputeBlockVersion(blocks.back(), params) & bitmask, 0u);
1132  
1133      cleanup();
1134  }
1135  
1136  BOOST_AUTO_TEST_SUITE_END()
1137