# Limenka MTP Cadence Implementation Plan Continuity document. This file is the source of truth for the current implementation cycle. Sessions may be lost; this file survives in git. When every phase in this file is complete and verified, delete this file in the final commit of the cycle. ## Status - 2026-08-17: **PLAN_LOCKED.md exists** - a temporary continuity document written to survive agent-database loss (user traveling). It holds the settled protocol, the 8 locked decisions, the measured results, and the remaining work order. DELETE PLAN_LOCKED.md when its content is folded back into this file's permanent sections. - 2026-08-16: Plan written. Sims at top priority. Consensus rules below are pinned EXCEPT where marked open - opens are gamed in Phase 1 before any consensus code is touched. - 2026-08-16: Phase 1 in progress. mtp_sim.py, shock_mtp.py, tune_mtp.py written. Key findings below - one design fork requires a decision (F1). ## Phase 1 Findings (sim results, seed 7, 50x/30min flood, 600s target) F1. **e measurement fork: MTP-delta vs direct stamp-delta.** The sims show the settled MTP-101-delta e has two structural flaws, both rooted in the median window being a 50-block-delayed measurement: a. **Reward transfer attack.** During a 30-min 50x flood, the attacker earned 58 R_full-equivalents with MTP-delta e; their fair share is ~3. The median "walks" through the displaced pre-flood stamps, crediting each flood block with the 600s spacing of blocks mined 50 back. Repeatable every ~100 blocks - a sustained ~6x-parity steal. With direct e (stamp_i - stamp_{i-1}) the same flood pays 3.3-4.9 across all gain sets. b. **"Whoever catches it gets the bigger reward" is false under MTP.** shock_mtp: after a 90% drop, the catch block waited 1,459s and was paid 289s (0.48 R_full; wait-worth 2.43). Post-flood honest blocks regularly get e=1s for 1000s+ waits. With direct e the catch block was paid 2,128s (3.55 R_full) and the largest catch 17.8 R_full. c. **DAA delay.** MTP-delta error forces gains below ~20M to avoid a 10-day-period limit cycle (original 50M/10M/16.7M gains oscillate 0.1x-4x). Direct e has no measurement lag: original gains work, step response t50 ~5h, no limit cycle. Direct e (nTime deltas, the pre-existing pow_fork.cpp measurement) has none of the flaws: telescoping sum gives exact parity (1.0000 vs 0.9923), fakery bounded at +60s per cadence transition (consecutive +60 stamps cancel in the difference), and the MTP-101 remains for activation gating and the nTime > MTP(prev) floor. DECISION NEEDED: keep settled MTP-delta (accept transfer + slow DAA) or switch reward/DAA e to direct stamp-delta (recommended). F2. **Gain tradeoff (direct e, W=101):** wander (steady-state log-std of d) vs step t50 vs flood damage vs restore: kp ki wander step_t50 flood_dmax restore 10M 10M 0.074 11.0h 2.49x 38.5h 20M 10M 0.091 7.2h 3.12x 31.5h 50M 10M 0.130 4.8h 4.70x 11.2h 50M 20M 0.139 3.8h 5.98x 10.4h 100M 20M 0.181 2.4h 8.98x 14.1h Knee is ~20M/10M to 50M/20M. The D term (kd=16.7M, first-difference of low-passed error) changed nothing measurable in any scenario - the band-pass D is currently decorative; either drop it or rebuild it on the direct signal. Flood attacker reward is 3.1-4.9 (parity-rate) in ALL gain sets - the proportional reward is doing the flood defense, the DAA just picks up the pieces. F3. **O1 (long-stall block size), initial data:** 4-day stall at 0.1% hashrate with fill = 6.6KB/s (sustained full-block demand): backlog = 2.28GB, allowance at catch e = 2.30GB - they match exactly, the catch block carries the entire 4-day backlog. At lower fill the backlog is the binding constraint; at higher fill it outruns the allowance and drains after cadence returns. Self-padding attacker can fill the allowance regardless of mempool. The receive-buffer ceiling question (hard cap vs unbounded) remains open - multi-day stalls are the only path to >100MB blocks. F4. **Boundary bug fixed:** the event loop now resamples across hashrate schedule boundaries (memoryless property) - the first version skipped the entire 30-min flood window in some runs. F5. **Timestamp attacks (tested per request):** attacker stamp modes: future (stamp at arrival+60, max allowed) and past (stamp at MTP floor, can be ~window/2*T behind wall clock). Results: - future-stamp: bounded in both basises. Majority (60%) attacker gains +136 (mtp) / +53 (direct) units over fair share over 8000 blocks (~1-3%): the +60 is cancelled between consecutive attacker stamps and only pays at honest->attacker transitions. - past-stamp is CATASTROPHIC without a monotonicity rule. Direct e: issuance explodes to 10.4x parity (20% attacker) and 212x (60%), because a backward stamp gets clamped to e=1 while the next honest block's e re-includes the full gap - the telescoping sum breaks at the clamp, and the MTP floor walks down indefinitely. mtp e: chain time-base destroyed (median pinned in the past, honest blocks paid giant e, cadence meaningless). An attacker who mixes past-stamps with one honest-stamped block collects their own inflation - it is profitable, not just griefing. FIX (validated): strict stamp monotonicity on the fork - nTime > previous block's nTime (subsumes the MTP floor). Then past-stamping is impossible: the attacker can only donate their reward to the next block. Future-stamping stays bounded at +60/transition. Issuance returns to exactly 1.0000 in every scenario including both attacks. Consequence: a majority future-stamper can pin the chain clock ~60s ahead; honest blocks then wait for the clock (modeled: ~500 waits in the 4-day-stall recovery, all ~1s) and block rate is hard-capped at 1/s until difficulty recovers (a few blocks) - benign. F6. **Candidate final design validated** (direct e + monotonic stamps + 60s future limit + MTP-101 for activation only), gains 50M/10M/0: steady: mean 599s, d 1.04-1.52, parity 1.0000; flood 50x/30min: attacker 3.34 (parity-rate), restore 11.2h; collapse99: tracks d=0.01 exactly; 4-day stall: catch block 2.28GB = full backlog drain, parity 1.0000; ramps to 1e6x: tracks target within 3%, ~48% transient overshoot; both timestamp attacks: issuance exactly 1.0000. F7. **Phase 1.5 battery (griefing, propagation, hardware variance).** a. **Asymmetric DAA - the griefing lever.** PI asymmetric, naming corrected from the swapped 2021 lineage (P = instantaneous error gain kp, I = EMA accumulator gain ki): kp=10M symmetric, ki_up=5M, ki_down=50M, alpha=0.05, no D, with a D=60s delay floor: - 50x/30min flood: d damage 1.0-1.1x, restore 1-13h (symmetric no-delay: 4.7-5.5x, 11-15h) - 1000x/2h flood: d damage 2.5-2.7x, restore 7-12h (symmetric no-delay: 345-391x, 48-152h - the chain is wrecked for a week) - legit growth tracks: 2x/30d -> d 2.03, 10x/1d -> 10.1 (23% overshoot vs symmetric's 48%) - steady-state cadence: 584-585s mean across 10 seeds at target 600 (3% dilation - the rectification bias of the asymmetric integral). Dilation CANCELED by setpoint calibration: target 617s -> measured steady mean 601.1s across 10 seeds (fixed point mean = target/(1+c), c=0.029). The D term has zero effect on the dilation (measured: mean unchanged at kd +-100M) - the dilation is a bias, the D is zero-mean by construction. Reward basis stays R_full*e/600 - parity untouched. - the INTEGRAL is critical (user-pushed, measured): adding the proportional kick kp=10M to the integral accumulator halves the 90%-collapse recovery time (17.4h integral-only -> 8.6-10.1h) for +0.5% dilation. Compensate with ki_up=5M to hold the flood bound at 2.5-2.7x (ki=20M degrades it to 4.6-4.9x - the knee). - asymmetric integral (ki_up=1M, ki_down=10M) dilates 18% - rejected; asymmetric PROPORTIONAL was never tried. The dilation mechanism is the same one that killed the difficulty cap (A3): any up/down response imbalance trades cadence for griefing resistance. - D term measured dead, definitively: the properly band-passed derivative (difference of the LOW-PASSED error, replacing the old raw-error differencing that amplified noise) changes nothing measurable - wander 0.101/0.101, step t50 8.8-10.8h unchanged, overshoot unchanged, collapse unchanged, flood bound unchanged (2.68->2.67x at kd=16.7M). The wander is driven by the I term's instantaneous noise response plus the asymmetry rectification; the D's phase lead is diluted because the loop is slow (EMA TC ~20 blocks) and the per-block noise (std = 600s) dominates the signal. D=0 final, justified by measurement. The 2021 "massive fluctuations" were the unfiltered derivative; filtering neutralizes the damage but also removes the benefit. b. **Delay floor (D=60s) hardware variance - the deciding curve.** The delay's anti-griefing value is only as good as its hardware uniformity. 1000x/2h flood vs attacker delay speedup hw: hw=1.0 (60s): d 1.9-2.1x, restore 11-14h; hw=0.5 (30s): 4.0-4.4x, 27-32h; hw=0.25 (15s): 17.7-19.2x, 36-42h; hw=0.1 (6s): 55-57x, 44-57h. A 10x-faster delay device brings the extreme flood damage back to symmetric-no-delay levels. The delay function MUST be chosen for minimum hardware variance - the long-division analysis (E) is load-bearing, not academic. c. **Propagation (12s base, 100MB/s):** steady-state orphan rate 1.9-2.2% at 600s target - matches the 12s/600s theory, delay invisible when PoW-bound (confirmed the user's estimate). During a delay-bound flood, honest-block survival improves from 50% (no delay) to 80% (D=60). The 4-day stall's 2.28GB catch block propagates fine (35s vs spacing hours). Delay serialization only binds when cadence is delay-bound. d. **Monopoly 90%:** attacker earns exactly 90% fair share (10761 of ~11950) - proportional reward holds; delay no effect. Timestamp attacks unchanged by the delay. e. **The 60s delay also kills the clock-pin throttle:** block rate is capped at 1/D regardless of stamps, so the future-stamp +60 pin cannot throttle honest miners below the delay rate. F8. **Open after Phase 1.5:** activation handoff (C1 first-fork-block e from parent stamps, C2 MTP-101 gate margin ~8.3h lag, C3 PID warmup) - the handoff sim is not yet built. Reorg 2-chain race model (A6) not built. Delay function engineering (E1-E3 benchmarks) not started. ## Decisions Pinned | Decision | Value | |---|---| | Base | Bitcoin Knots 29.3.knots20260508, branch foreverbip110, 37 fork commits (P2SPKH, VDF, PID DAA, CT/P2BPCT, audit round 3, rebrand) | | Parent chain | Mainnet (spamchain). Fork binds to the same P2P network, shares UTXO set and mempool at the fork point | | Activation | Time gate, not height. First block whose `MTP(pindexPrev) >= 1792328400` (2026-10-18 09:00 EDT, anniversary of Event 201 opening) is the activation block. `nForkActivationHeight` is dropped | | MTP window | 101 (consensus param). Used for the activation gate only - NOT for e/reward/DAA (F1). Fork-aware overload of `GetMedianTimePast(int span)`; parent-chain semantics keep span 11 | | Future time limit | Fork blocks: `nTime <= now + 60s` (down from 2h). Honest NTP assumption - fake-timestamp attack requires controlling global time infrastructure, out of scope. Combined with monotonic stamps, caps block rate at 1/s during extreme fast-block phases (benign, F5) | | Time basis | `e = nTime(block) - nTime(prev block)` (direct stamp delta) - one e feeds the DAA, the reward, and the size limit. MTP-101 remains for the activation gate only (F1) | | Stamp monotonicity | Fork rule: `nTime > prev block nTime` strictly increasing (subsumes the parent's MTP floor). Without it, past-stamping inflates issuance to 10-212x parity (F5) | | Reward | `R(e) = R_full * e / 600`, uncapped, floor zero. At e=600 pays exactly the parent-chain subsidy. Late blocks pay back stalled issuance - catch-up is automatic | | DAA | **PI asymmetric** (naming corrected: P = gain on the instantaneous error, I = gain on the EMA accumulator - the 2021 code had these swapped): kp=10M symmetric, ki_up=5M, ki_down=50M, alpha=0.05, no D term. Raises difficulty slowly (flood-proof), lowers fast (quick restore). The integral is load-bearing: it halves collapse recovery time (17.4h -> 8.6-10h at equal flood damage) - F7a revised. The 3% rectification dilation is canceled by a setpoint shift: `nForkIntervalTarget = 617s` (verified: 10-seed steady mean 601.1s, seed-stable +-1.5s). Reward time basis stays 600s - decoupled | | Delay floor | Per-block sequential delay D=60s required (F7b): caps block rate at 1/D regardless of hashrate, serializes the flood, bounds reorg speed. RESOLVED construction: M=1 division-free schoolbook long division (Barrett reciprocal, mul/add/sub only), divisor = prev_hash low 64 bits | 1, K = 2^33 steps, measured 6.04 ns/step (~27 cycles) on Zen 2 - details in PLAN_LOCKED.md item 7 and test/delay/delaybench.cpp | | Hard difficulty cap | Rejected (A3): high variance dilates cadence. The asymmetric gains already trade 3% cadence for flood resistance; a cap adds worse dilation for nothing | | Size | `payload_weight <= S_max * e / 600` with header+coinbase subtracted. Uncapped - late blocks can clear the backlog. OPEN: see O1 | | Halving | Time-anchored: aggregate seconds. Seed `nForkAggregateSeconds = activation_height * 600` at activation (fixes first-block-pays-50-BTC bug). Add actual `e` per block, not the 600 target | | VDF | Dropped entirely: consensus requirement, miner computation, coinbase commitment, RSA-2048 modulus, bignum library. REPLACED by a per-block sequential delay floor (see DAA row and F7b) - a low-variance computation, not a VDF with succinct proofs | | Witness discount | Removed at activation (already implemented - GetBlockWeight fork overload) | | Taproot | Pre-activation: allowed in blocks (consensus), rejected from mempool (reject_taproot policy). Post-activation: no new taproot outputs in blocks or mempool (consensus). Old taproot UTXOs spend freely into any non-taproot output | | Replay protection | SIGHASH_FORKID (already implemented) | | P2SPKH / P2BPCT | Already implemented; active from fork activation | ## Open Questions (gamed in sims before consensus code) O1. **Long-delay block size ceiling.** The proportional size formula is unbounded. A 4-day stall implies a ~576MB block - but that block can only contain what the mempool accumulated in those 4 days, so the mempool fill rate is itself the practical ceiling. Is a protocol hard cap needed at all? If yes, only as a receive-buffer bound (wire format needs a maximum before validation), not as a cadence defense. The DAA will almost never produce such a stall under normal operation. Sim the multi-day stall with realistic mempool fill rates and decide. O2. **PID gains and band-pass cutoffs.** Current gains (Kp=50, Ki=10, Kd=16.7 scaled 1e6) were tuned for nTime-based error. Retune for MTP-101 error signal. Compare window lengths 11/33/101/201. O3. **Stall window after attack exit.** Bounded and small, measured across flood-and-exit scenarios at varying attacker share and duration. O4. **Difficulty rise/decay symmetry.** Confirm symmetric PID does not price out honest miners after a flood (no anchored tiers anymore - the proportional reward is the flood disincentive, the DAA is just tracking). ## Phase 1 - Simulations (TOP PRIORITY) Purge: `test/sim/pid_sim.py`, `test/sim/shock_sim.py`, `test/sim/compare_sim.py`, `test/sim/tune_single.py` (multi-lane dead). New scripts: - `test/sim/mtp_sim.py` - core discrete-event model: 101-window MTP (activation only), `e = nTime(i) - nTime(i-1)` direct stamp delta, reward `R_full*e/600`, payload `S_max*e/600`, symmetric PID + band-pass D, 60s future limit, honest NTP. Scenarios: steady state, slow/fast hashrate ramps, 90% collapse, flood-and-exit (5-60 min), majority monopolization, timestamp-faking attacker (bounded by NTP model). - `test/sim/shock_mtp.py` - drop-and-rise shock rebuilt for the new scheme. Verifies the incentive: on drop, interval rises, the winner of the late block gets the bigger reward + accumulated fees, DAA lowers target, cadence returns toward 600s. Confirms no reward/size expansion on the early side and catch-up restores parity. - `test/sim/tune_mtp.py` - gain sweep (Kp/Ki/Kd, band-pass cutoffs, MTP window comparison). Metrics: issuance rate vs parity over long windows, interval distribution, stall window after attack exit, difficulty rise/decay traces, backlog drain, O1 multi-day stall. Exit criterion for Phase 1: parity within tolerance, bounded stall window, O1-O4 answered, tuned constants recorded here. ## Phase 2 - Consensus (after Phase 1 exit) - `src/chain.h`: `GetMedianTimePast(int span)` overload; keep 11-span default for parent-chain semantics (BIP94, CSV, standardness). - `src/consensus/params.h`: drop `nForkActivationHeight`; add `nForkActivationMTP=1792328400`, `nForkMTPWindow=101`, `nForkFutureLimit=60`, O1 result as ceiling param; drop VDF params (`nVDFDelayIterations`, `vdfModulus`). Rewrite `IsForkActive(const CBlockIndex* pindexPrev, params)` = `pindexPrev && pindexPrev->GetMedianTimePast(nForkMTPWindow) >= nForkActivationMTP`. Update all ~14 call sites (validation.cpp:2720, 2760, 3047, 3064, 4757, 4769, 4816, 5044, 5111, 5180, 5320, pow.cpp:20, miner.cpp:182, tx_verify.cpp:299, tests). - `src/kernel/chainparams.cpp`: fork params per the pinned table; remove VDF modulus/iterations (lines 845-851). - `src/primitives/block.h`: `GetForkBlockSubsidy(e, agg_seconds, halving)` = `R_full * e / 600`; `GetForkPayloadWeightLimit(e)` = `S_max * e / 600`. - `src/pow_fork.cpp`: error on the direct stamp delta; PI asymmetric gains (kp=10M symmetric on the instantaneous error, ki_up=5M / ki_down=50M on the EMA accumulator, kd=0 - naming corrected from the swapped 2021 labels); enforce the D=60s delay floor on the sequential computation input; seed DAA state in `InitForkDAAState` (target from parent nBits, aggregate seconds = activation_height * 600). - `src/validation.cpp`: 60s future-limit branch for fork blocks (4670); replace fixed 4MW fork caps (4484, 4757, 4816) with proportional payload limit + O1 ceiling; ConnectBlock reward = `GetForkBlockSubsidy(e, ...)` with `aggregate_seconds += e` (3066 was adding target 600); remove VDF checks (4767-4795); post-activation taproot-output ban (block-level scan, inputs unrestricted). - `src/consensus/tx_verify.cpp`: taproot-output ban in CheckTxOutputs when fork active (consensus side; inputs unrestricted). - `src/node/miner.cpp`: CreateNewBlock - compute e at assembly, scale coinbase reward and payload cap, delete VDF commitment (229-231). - `src/policy/policy.cpp`: post-activation `reject_taproot` unconditional. - Remove `src/consensus/vdf.h/cpp`, `src/vdf.cpp/h`, `src/crypto/bignum.*` (verify CT/bulletproofs do not depend on bignum first - bulletproofs uses secp256k1, VDF is the only bignum consumer). ## Phase 3 - Tests - Rewrite `src/test/fork_block_tests.cpp`, `fork_pid_tests.cpp`, `fork_chain_tests.cpp` for the new curves (reward/size at e in {0, 300, 600, 1200}, MTP-gate activation, taproot input-ok/output-ban, 60s future limit). - Delete VDF-dependent tests (fork_block_tests.cpp:127-144, fork_chain_tests.cpp:28, vdf_tests.cpp). - New: aggregate-seconds epoch seeding test (first fork block pays parent-epoch subsidy, not 50 BTC). ## Phase 4 - Verification - Long mocktime regtest run: measured issuance vs parent-chain baseline vs sims' parity prediction. - Drop-and-rise shock on regtest; stall window measurement. - Taproot: spend-into-legacy accepted, spend-into-taproot rejected post-activation. ## Termination Delete this file in the final commit when all phases are complete and verified.