// Copyright (c) 2026 The Limenka developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include #include #include #include #include #include static constexpr int64_t PID_SCALE = 1000000; static constexpr int64_t DEN_MIN = 100000; static constexpr int64_t DEN_MAX = 10000000; static constexpr double ALPHA = 0.05; // EMA smoothing factor /** Advance the DAA state one block: pure function of the previous state * and the block's stamp. */ static ForkDAAState AdvanceDAA(const ForkDAAState& prev, int64_t block_time, const Consensus::Params& p) { ForkDAAState next; const int64_t interval = p.nForkIntervalTarget; int64_t actual; if (prev.nForkLastBlockTime == 0) { actual = interval; // first fork block - assume the target interval } else { actual = block_time - prev.nForkLastBlockTime; } const int64_t error = interval - actual; const int64_t avg = int64_t(prev.nForkAvgError * (1.0 - ALPHA) + error * ALPHA); const int64_t ki = error > 0 ? p.nForkKiUp : p.nForkKiDown; const int64_t correction = (ki * avg + p.nForkKp * error) / PID_SCALE; int64_t denom = PID_SCALE + correction; denom = std::clamp(denom, DEN_MIN, DEN_MAX); next.nForkTarget = prev.nForkTarget * PID_SCALE / denom; next.nForkAvgError = avg; next.nForkLastBlockTime = block_time; return next; } /** Recompute and memoize the fork DAA state (target/avg/lasttime) plus the * aggregate-seconds counter for fork-era index entries whose state is * missing. Headers-first sync validates difficulty before blocks are * connected, and the state must be a deterministic pure function of the * header chain - zero-filled state must never be mistaken for "first fork * block" (that made restart and fresh sync compute a different validity * function than uptime nodes). */ static void EnsureForkDAAState(CBlockIndex* pindex, const Consensus::Params& p) { if (pindex->nForkTarget != arith_uint256(0)) return; std::vector missing; for (CBlockIndex* w = pindex; w && w->nForkTarget == arith_uint256(0) && w->pprev && IsForkActive(w->pprev, p); w = w->pprev) { missing.push_back(w); } for (auto it = missing.rbegin(); it != missing.rend(); ++it) { CBlockIndex* cur = *it; const CBlockIndex* pp = cur->pprev; ForkDAAState prev_state; if (pp->nForkTarget == arith_uint256(0)) { // Activation boundary: seed from the parent chain's difficulty. prev_state.nForkTarget.SetCompact(pp->nBits); } else { prev_state.nForkTarget = pp->nForkTarget; prev_state.nForkAvgError = pp->nForkAvgError; prev_state.nForkLastBlockTime = pp->nForkLastBlockTime; } const ForkDAAState next = AdvanceDAA(prev_state, cur->nTime, p); cur->nForkTarget = next.nForkTarget; cur->nForkAvgError = next.nForkAvgError; cur->nForkLastBlockTime = next.nForkLastBlockTime; int64_t e = pp->nForkLastBlockTime != 0 ? cur->nTime - pp->nForkLastBlockTime : cur->nTime - pp->GetBlockTime(); // first block: actual interval if (e < 1) e = 1; const int64_t agg_base = IsForkActive(pp->pprev, p) ? pp->nForkAggregateSeconds : int64_t(cur->nHeight) * 600; cur->nForkAggregateSeconds = agg_base + e; } } uint32_t CalculateForkTarget(const CBlockIndex* pindexPrev, const CBlockHeader* pblock, const Consensus::Params& p, ForkDAAState* out) { // The DAA state of the previous fork block must exist before use: // recompute it from the activation point when missing (headers-only // sync, or an index written before the state was persisted). EnsureForkDAAState(const_cast(pindexPrev), p); ForkDAAState state; state.nForkTarget = pindexPrev->nForkTarget; state.nForkAvgError = pindexPrev->nForkAvgError; state.nForkLastBlockTime = pindexPrev->nForkLastBlockTime; if (state.nForkTarget == arith_uint256(0)) { // Activation block: seed from the parent chain's difficulty. state.nForkTarget.SetCompact(pindexPrev->nBits); } ForkDAAState next = AdvanceDAA(state, pblock->nTime, p); if (out) *out = next; return next.nForkTarget.GetCompact(); } void InitForkDAAState(CBlockIndex* pindex) { // Seed from this block's own difficulty (the parent chain's target at // activation), so the fork continues without a difficulty reset. pindex->nForkTarget.SetCompact(pindex->nBits); pindex->nForkAvgError = 0; pindex->nForkLastBlockTime = 0; pindex->nForkAggregateSeconds = 0; } arith_uint256 GetForkTarget(const CBlockIndex* pindex) { return pindex->nForkTarget; }