Shekyl Changelog
[Unreleased]
[3.1.0-alpha.6] - 2026-07-07
Added
-
wallet-rpc: OpenAPI 3.1 contract for
shekyl-wallet-rpc(Phase 4 spec-first gate) (docs/api/wallet_rpc.yaml;WALLET_REWRITE_PLAN.md§"Phase 4";IMPLEMENTATION_INDEX.md§3 Phase 4 row). The spec lands before the first method per the plan's spec-first rule: JSON-RPC 2.0 envelope, the stable error-code range allocation (-29000..-29999 by domain), and full schemas for the 15 methods whose engine-layer prerequisites (Phase 1 orchestrator + Phase 2a send path) have landed — wallet lifecycle, balance/address, the build/submit/discard send trio (with the CT-5dcontent_gengate and theSubmitVerdictsuccess/rejection projection perDAEMON_SUBMIT_VERDICT.md§2.5), transfers, refresh/rescan, and height/version. JSON shapes are Shekyl-native and locked: atomic-unit string amounts, first-class staking fields inget_balance(zero until Stage 3), explicit capability mode on every wallet handle, structured hybrid PQC signatures. Stake, payment-request, proofs, sign/verify, and air-gapped-bundle methods are RESERVED with named prerequisites (rule 21 reversion shape) — their error-code ranges are allocated now so the contract never shifts under clients. No implementation crate yet; the firstrust/shekyl-wallet-rpcsub-PR conforms to this file. -
archival: segment-freeze pipeline design round 1 opened (
ARCHIVAL_SEGMENT_FREEZE_PIPELINE.md) — the production writer/deleter form_archival_shard_segment, satisfying the M1 §1.3 merge condition. Substrate survey (doc §2): the daemon already maintains the curve tree inside the block write txn with a drain-journaled, pop-symmetric leaf count, so freezing reduces to a first-crossing rule — segmentkfreezes at the first height whereleaf_count ≥ (k+1)·SEGMENT_LEAF_COUNT,R_kread from the layer-2 chunk the same-txn grow computed — and O-1..O-3 discharge by inheritance (doc §3), with the pop hook a derived delete (no freeze journal). PinsSEGMENT_LEAF_COUNT = 25 992(level-2 subtree,38·18·38) into the constants pipeline with the boundary division in a single Rust FFI entry point called by both connect and pop hooks (M1-1 single-source shape). Exercises the maintainer's pre-genesis schema-restructure authorization once:archival_shard_leafis deleted (doc §6.2) — it is a derived copy ofm_curve_tree_leaves; the gate-2 challenge path reads the 38-leaf chunk directly from the tree's own leaf table. Adversarial review of the round-1 draft pending; implementation gated on round closure. -
archival: segment-freeze pipeline implemented — steps 1–7 of the round-1 plan (
ARCHIVAL_SEGMENT_FREEZE_PIPELINE.md§7, onfeat/segment-freeze-pipeline). (1)SEGMENT_LEAF_COUNT = 25 992through the constants pipeline + Rust first-crossing arithmetic (frozen_segment_count,challenge_leaf_chunk_bounds) with boundary/overflow unit tests; (2) FFI exports for both; (3) connect/pop hooks indb_lmdbinside the block write txn —process_archival_segment_freezes_at_heightwrites newly-crossed segment rows readingR_kfrom the layer-2 chunk the same-txn grow computed,revert_archival_segment_freezesis the derived delete (O-3 pop-symmetry); (4)archival_shard_leafdeleted — the gate-2 challenge path reads the 38-leaf chunk directly fromm_curve_tree_leaves; (5) the §9 C++ suite (archival_segment_freeze.cpp: first-crossing, multi-segment catch-up, independentR_krecomposition, bit-identical pop/re-apply, pop-above-boundary no-op, missing-layer-2-chunk loud abort, M1 operand against production rows — the §11.10 fixture caveat retired) + gate-2 fixture regenerated at25 992; (6) tripwire extensions (check_reward_gate_predicate_sites.sh: writer one-site, cursor accounting, division one-site,for_katcontainment); (7) docs (LMDB_SCHEMA.mdv7 note, M1 §1.3 discharge record). O-1..O-3 discharged. Pre-flight additions folded in: PF-8 transposition-distinguishing epoch-close FFI fixture (exact-sigma pin catches any swap of the three leadingu64s); PF-6aKCovercapability newtype (consensus()sole production constructor; KAT injection behind the permanent dev-onlyconsensus-katfeature). -
archival: O(1) pop-symmetric frozen-shard counter — the M1 gate operand's persisted backing store (schema V8) (
ARCHIVAL_SEGMENT_FREEZE_PIPELINE.md§4.4; M1 §11.11; closes theFOLLOWUPS.mdefficiency follow-on from code-review #263).properties["archival_frozen_shard_count"]moves in structural lockstep with the segment table: +1 input_archival_shard_segment(nowMDB_NOOVERWRITE— rows are CREATE-only, enforced at the mutation site), −1 per deleted row inrevert_archival_segment_freezes(underflow-aborted).count_frozen_shards_at_closedrops its full-table walk for an O(1) counter read +MDB_LASTfrontier check (row must decode;freeze_height ≤ h_close; future-dated frontier and counter/table divergence are loud aborts). This is not M1 §11.8 M3-1's cached-counter adversary — the adversary lacked O-3 pop-symmetry, which this counter has by construction, differential-tested against the retained walk oracle (count_frozen_shard_rows_by_walk_for_test, zero production callers) across freeze/pop/re-apply cycles. Tripwire: segment-table cursor accounting 4→5; new invariant 6 pins the counter's mutation surface (key literal ×2, setter call sites ×2, walk oracle unreferenced in production). Breaking (rule 42): DBVERSION7→8;migraterefuses pre-V8 databases loudly (delete-and-resync; no pre-genesis migration code per15-deletion-and-debt.mdc). -
archival: M1 pre-flight dispositions recorded — PF-1 breach record, PF-2 accepted residual, PF-9 seal-before-stressnet pin. PF-1: the §11.9 sequencing breach recorded in
FOLLOWUPS.mdas a breach (not a precedent);26-sub-pr-design-discipline.mdcgains the explicit halt condition — no implementation commits on a branch whose governing design doc names an undischarged pre-flight pass. PF-2: the compile-refusal's out-of-band hand-built-artifact residual recorded as accepted with its upstream dependency named (Guix-reproducible builds + signed-tag release path — M1 §4). PF-9:K_COVERfinalization pinned as a prerequisite of Phase 7.7 stressnet entry (M1 §4 rule-21 entry +RELEASE_CHECKLIST.md) — while provisional the gate runs as identity, so only a post-seal stressnet exercises the activation boundary live before genesis. -
archival: M1 reward gate implemented — steps 1–5 of the §11.9 pinned sequence (
ARCHIVAL_REWARD_GATE_M1.md§11.10 implementation record). Executed onfeat/m1-reward-gate-designafter the pre-flight pass held the §6 enumeration at the audit pin (re-verified post-WI-4-merge at1e89df832; M3-3 discharged — WI-4 §16.3 pinsshard_countas structural, theK_COVERcalibration derives against segment count). (1)k_cover+k_cover_provisionalconstants withbuild.rsvalidation and acompile_error!refusal absent theprovisional-k-coverfeature — armed before the identifier existed anywhere else; sentinel refined in-arc from fail-closedu64::MAXto gate-identity0(provisional ⇔0, sealed ⇒≥ 1) so the pre-seal corpus stays live end-to-end while the compile refusal remains the shipping guard. (2)count_frozen_shards_at_close(single production call site in the close gather;freeze_height ≤ H_closefilter, decode failure aborts loudly) threaded as an FFIfrozen_shard_countparameter intoEpochCloseInputs::{frozen_shard_count, k_cover}with the zero-at-top gate factor inepoch_close_compute. (3) Wire-level positivity:WireError::RewardAmountZeroat bothvalidate()andread_payload()— the zero-amount row is unencodable and undecodable; theEMISSION_AUTH_MSG_V1corpus zero row replaced and its four pinned digests regenerated in the same commit. (4)reward_gate_kat.rsG-1..G-10 plus C++ store-side cases: count-filter boundary (equality counts), malformed-row loud abort, write-txn precondition, and the zero-output stored shape + reorg round-trip via a legitimately-empty epoch (bitwise-identical to a gated close per §2.1; direct gated-path test deferred toK_COVERsealing). (5)scripts/ci/check_reward_gate_predicate_sites.shas consensus-invariants invariant 5: singleK_COVERcomparison site, single counting read overm_archival_shard_segment(mdb_statrefused),≤boundary pinned with strict-<refused, positive controls guarding the guards. Outstanding before the PR merges: the segment-freeze pipeline design round opens (§1.3 condition). -
docs: M1 reward-gate consensus rule — design rounds 1–3 spec + closure (
ARCHIVAL_REWARD_GATE_M1.md). The WI-4 launch posture's cold-start refusal in consensus-rule form: zero reward accrual for every persona in epochs whereshard_count < K_COVER, enforced as a uniform zero-at-top factor inepoch_close_computeand inherited by the emission zero-tolerance compare through the zeroed storedΣwork. Spec-first per05-system-thinkingand26-sub-pr-design-discipline(cited). Carries the WI-4 §16.2 obligations — activation-boundary KAT as a first-class deliverable, the zero-accrual/no-seniority invariant enumeration with source-verification checks, and the spec/constant split (K_COVERfinalization gates on the §14.4 partition run). Round 1 closed against an adversarial wargame (§9): consumer walk verified zero pre-gate readers of the epoch-close outputs andjoin_settlement_epochas eligibility-only; the non-claimable rule amended to the uniform positive-share form; the provisionalk_coversentinel hardened to a compile-time refusal in non-test builds; the reorg-across-activation-boundary argument written out against the existing connect/revert pairing. Round 2 closed against a second independent review (§10, findings M1-1..M1-9): the non-claimable rule resited to the wire level (strictly positivereward_amount_plain, rejected atvalidate()— the zero-row beacon becomes unencodable, andK_COVERis compared at exactly one code site, guarded by a new CI grep tripwire); §2.1 output naming corrected to the real two-fieldEpochCloseResult(no per-Psurface widening); the gate pinned to run through the normal close path, never an early-return skip (close-log/revert symmetry, prune advance, stored-zero sigma), with the zero-accrual-is-not-zero-accountability invariant (serve credits, bad intervals, slashing stay live during gated epochs); the §4.5 lagged-read discipline recorded as load-bearing for non-claimability (never recomputeΣworkfrom primaries); KAT extended G-6..G-9 plus C++ store-shape and boundary-reorg tests; reorg-argument and prune-optimization reversion clauses added (rule 21). Surface enumeration updated; wallet-claim-builder obligation pinned forward. Round 3 closed against two parallel reviews (§11, R2-1..R2-4 + M2-1..M2-5), surfacing a critical defect the earlier rounds missed: §1.1's concrete anchor pinned the epoch-close gather count — credit-derived, a participation measurement (the sensor class §1.2 forbids: non-monotone, withdraw-service griefing lever) — while the security argument, dead-rule acceptance, and reorg triviality are proofs about the segment-table count. §1.1 re-anchored tom_archival_shard_segmentfiltered onfreeze_height ≤ H_close(E), threaded as a newEpochCloseInputs::frozen_shard_countfield + FFI parameter + C++ gather count pass (the §6 "FFI zero-change" row corrected); G-10 participation-independence KAT added. The segment-freeze substrate is confirmed unbuilt (put_archival_shard_segmenthas no production caller, no delete path) — §1.3 restates the substrate properties as named obligations O-1..O-3 (determinism, per-branch monotonicity, pop-symmetry) on the future freeze-pipeline design round, with implementation conditioned on them and the §9.4 reorg argument explicitly conditioned on O-3. WI-4 provenance resolved as a merge-ordering dependency (feat/wi4-gf7-measurement, pushed, unmerged). Closure-review items: lagged-read tripwire made a standing pre-flight check, stored-close reader enumeration recorded, reorg-crossing-claim-attempt pinned as a wallet-builder forward obligation, claim-era-retirement (2615c0d) interaction closed at source (no shared wire/table/path). Round-3 amendment (§11.8, M3-1..M3-3, post-closure pin): the count pass — the surface round 3 itself created — armed to the same standard as the predicate site. Single named helper (count_frozen_shards_at_close, one call site) owns thefreeze_height ≤ H_close(E)filter; the CI tripwire extended to refuse any other counting read overm_archival_shard_segment; C++ unit cases pin the filter boundary (equality counts, per≤— the off-by-one the Rust KAT structurally cannot reach) and the decode-failure discipline (undecodable segment row aborts the close loudly, same class as the gather's FATAL — a lenient skip would be a consensus fork in the gating direction); G-1..G-3 rebound tofrozen_shard_count; named pre-flight item added requiring the WI-4 §14.4K_COVERcalibration to derive against the segment count, not any served/participation quantity. Implementation gates decided (§11.9): §1.3 fork resolved to the second branch — the gate implements now on fixture rows (wargamed: the count helper binds to theLMDB_SCHEMA.md-fixed schema, not pipeline behavior; future-dated freezes handled by≤, non-dense IDs by walk-not-watermark, versioned segments unrepresentable under the existing key; O-3 is chain-wide, so serializing the gate behind the pipeline buys almost nothing while delaying the unpatchable-after-seal item), with O-1..O-3 blocking on the pipeline round and that round opening before the gate PR merges. Pre-flight audit pin = dev head at pre-flight time, not69af41a. WI-4 doc merge holds sequence slot 0. Intra-PR order pinned: constants + compile-refusal before thek_coveridentifier exists anywhere else in the tree (the refusal armed before there is anything to guard), then helper + threading, wire positivity + corpus fix in the same commit, KATs + C++ store tests, tripwire scripts last. -
sim: GF-7 graded genesis gate — WI-4 (PROVISIONAL-PASS, local-daemon posture only) (
ARCHIVAL_BOND_WI4_MEASUREMENT.md;IMPLEMENTATION_INDEX.md§4 WI-4 row). The GF-7 principal↔Pfunding-seam unlinkability property is now measured, not just hooked. The a-priori boundr = P(link)·N < 2(the modeled observer may at most double blind guessing) is committed in the reviewed acceptance doc before any grading code (§3; provenance settled §13.4), then graded by a real S-3 correlator replacing thenearest_principalplaceholder (shekyl-staking-sim/src/gf7_timeline.rs,--gf7-timeline). The 2026-07-06 adversarial review re-grade hardened the round: the observer is posture-conditioned (§4.1.1 — session/refresh markers are loopback-invisible under the local-daemon posture and excluded as direct anchors; the remote/non-isolated-daemon posture is a named unmet residual on the verdict, D-B1 2d-2 reopen family; the indirect session channel is coupled in the generator and graded — blind0.237at window 0 →≈0.13under full jitter); the stress arm is an oracle-panel with an exact seam-consistency-gated member (§4.3.1;lr > s3on every gate row); the D-B3 resume channel is swept (session-lattice resubmit,r = 1.57, under bound). Controls valid; every gate-relevant row clears the bound on all three arms; worst gate rowr = 1.86(honestly thin margin; per §4.3.1's pinned ceiling the margin holds against the strongest observer of the modeled channel, not the strongest adversary). Cold-start reclassified: low-activity fails the bound (panelr = 3.54) and is the genesis regime — disposition is the §14 founder-cover launch posture (refuse the regime: production-path-identical staggered founder cover under permanent consented attributability; shard-schedule structural gate; §14.4 partition-adversary arm spec-committed with a-priori bounds and a pre-implementation-widened hypothesis class — max-statistic over a named rule family vs the permutation null of the maximized statistic, one marked control per failure mode; implementation gated on review). Remote-daemon disposition made explicit (§15, proposed, review-gated): structural refusal at the dispatch driver rather than a warned override — privacy is not a setting; tunneled circumvention the named residual; reversion on a measured decorrelated remote transport. Mechanization addendum (§16, proposed, review-gated): the launch posture converted from policy to structure under the global-and-blind constraint (mechanisms sort on structure, never identity). M1 — reward-eligibility-by-shard-count as a genesis-frozen consensus rule (zero accrual pre-K_COVER, not deferred payout; shard count verified at source as a deterministic monotone function of height; refuses the cold-start claim-cohort hazard in the same window) — is the build-first item and gets its own design round, with two obligations pinned for that round: the activation-boundary consensus test as a first-class deliverable (the once-firing rule's boundary is never re-exercised at runtime), andK_COVERfinalization gated on the §14.4 widened-class measurement (rule shape specs immediately; the constant waits on the cover model that measurement validates). Three closing pins (§13.5, §14.4, §16.10): the sealed claim stays conditional by construction (isolation conditioning never discharges — two-of-three-measured is not three-of-three-cleared); the partition arm's output feeds two consumers with opposite dispositions (distinguishability fail ⇒ posture redesign; sound-but-thin cover ⇒K_COVERcalibrates higher); and the §§14–16 review closure is held to an adversarial read of the bounds themselves, not a consistency check. M3/M7 CI enumerations (gate-11 / F25 shape), M4 global launch-window bond rate cap (WI-3 D-B2), M5 never-serializedPublicByConsentmarker, M6 generator-coupling control (blind@window-0 ≥ 2× chance or INVALID) + leg-(b) wall-clock emission. Irreducible three named. Review-closure round R1 conducted (§17, 2026-07-06; ratification pending): the three §16.10 attacks run to land, each answered attack-shaped on the record. Attack 1:r < 2survives (claim-anchored, observer-independent) but measuredr(N)is only known atN = 10— N-sweep added (§16.7 item 4,r < 2at every sweptN, a-priori). Attack 2: the widened family was still a proper subset — members 4–5 added (spectral graph partition; seeded nearest-neighbor expansion with one granted founder label), and two label-free channels closed: M3 extended to the full persona lifecycle (claim-absence sorts non-claiming founders, §16.4/§14.3.4) and P4 gains a non-disclosure commitment (consent-to-risk ≠ license-to-disclose; enumeration collapses recorded cover retroactively — §14.3.4, founder-strip sensitivity row in theK_COVERcover model). Attack 3: theK_COVERcycle traced and does not close — the validation projection isK-independent (the breaking joint), the calibration projection a monotone fixed point; the cover model gains a gate-open cohort-dynamics row with its demand assumption as a named conditional (§16.2 obligation 3). No bound relaxed; all amendments adversary-strengthening, pre-code. Distinct-position round R2 (§17.5, 2026-07-06): the standing reviewer's post-merge pass. WI-3 (PR #260) traced at source against D-B1..D-B7 — faithful; one finding (A-1): the R2-1 clock-trust marker reasoned about one of two tip consumers, and the alarm horizon's sensitivity is inverted (inflation ⇒ premature alarm, not benign-later) — marker + WI3 doc amended so the 2d-2 clamp covers both reads. On the WI-4 pins: B-1 converts the no-cross-subsidy pin to structure (§9 criterion 9 — aggregate verdict is a computed conjunction over every committed bound, INVALID on absence, FAIL on any miss); B-2 pins that §16.10 attacks come from a distinct adversarial position with the construction recorded; its concrete instance — ther < 2mean-vs-max challenge — survives (exchangeability makes the bound per-persona in-model; the untargeted reading was conceded at the floor) and forces the §3.2 pin naming exchangeability as the load-bearing step and regime-splitting as the mean-to-max carrier in deployment. Closure requires §§17.1–17.5 ratified together. Distinct-position round R3 (§18, 2026-07-06): the mean-vs-max disposition is sound but incomplete (captured the max for the split axis, assumed the heterogeneity enumeration complete). Five surfaces scope what1.86means — one seam (entry; exit/drain is GF-4, unmeasured), one scope (per-post, not per-principal), one channel (timing, not the loud reward-amount value channel), one instant (not lifetime-cumulative), one stratum (unstratified by observable bond attributes). Landed on the verdict line: the cross-seam conditional (§13.1/§13.5) —r < 2promotes per-post → per-principal only where persona mutual-unlinkability (GF-4) holds, parallel to isolation conditioning; the most important previously-unstated dependency (S-1 is per-principal; the bound is per-post). Verified at source: the archival reward value channel is loud by design (reward_amount_plaincleartext, P-tied, per-epoch recurring; confidential reward deleted —emission_wire.rs,REWARD_EMISSION_LEG.md; consensus-inert until PR-E3/C-1) — orthogonal to the timing gate, needs its own measurement; the attribute-strata coupling is real but viaholdings/bond_floor, not the retired lock-tier multipliers. New sealing-path rounds: GF-4 exit seam (co-equal genesis gate, graded jointly with persona rotation) + the value channel. Ride the §14.4 round: attribute-stratified grading, lifetime-accumulation sweep. No bound reopened. R3 addendum (§18.7): tiers-vs-continuous-accrual answered at source — the shipped leg already embodies the strong form (accrual reads shard age, not persona duration;g(age)linear, no knees; duration structurally quantized at settlement-epoch granularity, so the exit→entry back-solve is capped at class width by construction). Four pins routed to existing rounds: curve shape as privacy parameter (reopen on anyg_age_milli/curve_millichange;Curveplateau breakpoints cluster the holdings stratum — §18.3 measures direction),SETTLEMENT_EPOCH_BLOCKSas a-priori duration-class width (GF-4 derives the bound before 10k is a privacy value), per-epoch claim submission as a recurring entry-gap-shaped timing observable (GF-4/value-channel round), and correlated mass-unbonding as a named GF-4 wargame (duration-class anonymity under co-triggered exits). R3 correction round (§18.8): the P-is-public reframing —Pis the pseudonym, public by construction; only theP→user link is protected. Retracted §18.3's intrinsic-stratification claim and §18.7's P-curve privacy-primary escalation; re-anchored claim-timing, lifetime accumulation, and correlated-exit on user-rhythm/user-event correlation. Amount-bridge trace verified at source: closed at entry structurally — the bond's public amount is consensus-forced tobond_floor(holdings)(bond_post.rsFloorMismatch), funding and change FCMP++-hidden, no principal reach-across in the assemble path. Surviving residuals routed to GF-4: V-2a staker-membership prior (universal funding constant vs off-chain user-side amount surfaces) and V-2b exit lifetime-total match (P's lifetime value publicly computable to the atomic unit — the value channel's surviving form). Mechanism pin (§18.9): a follow-on CT reading (bond amount hidden above the floor behind a user-configurable mask) verified at source wrong in mechanism — the bond term is a transparentamount·Hcleartext term in the CT balance (bond_ct_balance.rs; funding and change are the hidden legs),commitment_maskis HKDF-derived and never user-chosen; closure is loud-but-constant, the proposed mask-consistency wargame unrepresentable-by-construction (reopen on any user-supplied blinding factor or above-floor bonding). On-chain bridges are timing-only; V-2a/V-2b survive off-chain. R4 exit-seam inventory (§18.10): both determinative source checks resolved — the reward payout is cleartext by genesis disposition (REWARD_EMISSION_LEG.md§5.5/§9) and exact (no grid), andreward_P(E)is ex-ante computable from public consensus state. R-1 confirmed dominant but reach-bounded (severed at first spend by FCMP++/CT; all bits at the off-chain matching surface — V-2b in sharpest form). CT-committing the payout foreclosed (priority-1 inflation audit); open defenses: quantization (grid width a-priori derived, GF-4 candidate) + gate-6 routing. Claim-cadence, reward amount, and holdings stratum graded jointly on GF-4. R-1 disposition (§18.11): FCMP++ hides amount AND linkage at every on-chain hop (no non-mint cleartext amounts;P's spend set unenumerable — the precomputed sequence has no anchor after hop one). R-1 is a managed residual at the economically-guaranteed off-chain boundary, defended in layers: firewall-routing (primary, complete on-chain), new gate-6 pin candidate drain-amount decoupling (exact-subsum match is a wallet-policy footgun — drain amounts never computable reward subsums), quantization as defense-in-depth for the approximate lifetime-aggregate band. R4 closure (§18.12): whole-path routing obligation retracted (discharged by the base transfer format); drain-decoupling pin tightened to input-level — the drain-amount computation must not read the reward sequence as an input (user target/cadence/randomness only; verified at the selection path's inputs when the gate-6 policy lands). Why-loud recorded: the mint is loud by priority-1 decision (inflation audit) — R-1 is a priority-ordered tradeoff at its floor, not an unclosed defect; do not "fix" it by CT-committing the mint. Arc closure (§18.13): theory ceiling declared; Tor-default trace resolved at source — persona side Tor-mandatory by construction (shekyl-p-transportcompile_error!without SOCKS), principal side opt-in (--proxydefaultNone); principal-side default-on Tor added to FOLLOWUPS as a V3.0 pre-genesis item. Principal↔user seam closing frame recorded: chain-only adversary blind by construction; residual boundaries are the irreducible on-ramp witness, the network default (until flipped), and the enumerated user-mistake set closed by safe-by-default coverage. PROVISIONAL: the block-time sealing re-run is confirmatory by construction (liveBondPostDispatched.atis the due block; dispersal is sub-blockU[0,60s)), so no block-resolution re-run can close reconvergence leg (b); the wall-clock sweep-phase channel is the primary open uncertainty — closing requires sub-block wall-clock emission + re-grade (docs/FOLLOWUPS.md). Sim-only, featuregf7-hooks(non-default); no production telemetry. -
wallet: block-timed bond-post dispatch driver — WI-3 (
ARCHIVAL_BOND_WI3_DISPATCH.md;IMPLEMENTATION_INDEX.md§4 WI-3 row). The sealed pending post now actually reaches a wire at its planned block. The driver (pscan/dispatch.rs) rides theP-scan sweep's own tip read (§3.1 named invariant: the dispatch clock is the daemon's claimed tip, sound under the recommended local-daemon posture; the sweep-corroborated clamp is held in reserve for the 2d-2 reopen), dispatches at most one due post per tick in deterministic order behind a fresh dispersal draw, and seals theDispatchedtransition before any send — every attempt, first or resubmit, re-lifts the stored bytes through thesubmit_boundchoke (pin P-2). Outcome handling per §3.4: accept/F31 holds resubmits for the pscan's own reorg-deep confirmation (a daemon's F40 claim never releases the reservation), terminal verify rejection removes bytes + reservation in one seal and alarms (never auto-re-assembles), unknown outcomes resubmit byte-identical next due tick, bounded by the derived 1.5×ARCHIVAL_REORG_DEPTH_BLOCKSalarm horizon (funds-safety over liveness: the alarmed record is held, not dropped). Schema:PENDING_POST_VERSION1 → 2 for theDispatchedarm; v1 fails closed (pre-genesis, rule 15). Wiring: aDispatchTickseam at the end ofpscan_sweep(a failed tick logs and retries next sweep, never halts the scan); production store is theWalletFile-backed region-2 AEAD seal of.wallet.pending, production broadcast is theLocal-postureBroadcastSubmitter. GF-7: liveBondPostDispatchedemission at the submit call site (per-submit emission-completeness test, gate 8); WI-3's GF-7 acceptance stays open until WI-4's a-priori threshold artifact + live-emission sealing re-run per the §5 reconvergence gate. Enforcement:ci/pending-post-write-path(scripts/ci/check_pending_post_write_path.sh) pins the single write path / single payload kind for pending-post persistence (gate 11) — the torn-state-unrepresentability argument (gate 10) rests on it. -
wallet:
start_pscanlifecycle wiring — WI-1, completing 2d-1 SP-5 (ARCHIVAL_BOND_2D1_PSCAN_PLAN.md§"WI-1";IMPLEMENTATION_INDEX.md§4 WI-1 row). The firewalledP-scan gains its lifecycle call sites:Engine::start_pscan_if_staker(the post-open auto-start — a staker wallet gets its scan at production settings, a non-staker getsOk(None)and pays nothing) andEngine::start_pscan(the on-demand entry mirroringstart_refresh;NoStakeEnginefor a non-staker). Both runPScanConfig::production()atDEFAULT_PSCAN_CADENCE(60 s, rule-75 rationale + bounds documented on the constant); the config-injectable seam stays crate-private (start_pscan_with) for tests. The returnedPScanHandleis embedder-held, not engine-held — the running task's store holds a strong engine arc, so engine-held storage would make the pair immortal; embedder-held makes "close stops the task" ownership-enforced (close(self)is unreachable until the handle is shut down andArc::try_unwrapsucceeds). The transient#[allow(dead_code)]chain throughblock_source/cadence/task/accrualis removed; remaining allows each name their later consumer (2d-2 posture selector, SP-7C_min, SP-R0 reconcile GC, cold-start cover discovery). Lifecycle tests cover staker auto-start, non-staker quiet path, double-start refusal (single-flight), shutdown-releases-engine, and restart-after-stop.
Changed
- net: rotated
NETWORK_IDfor all three networks (mainnet, testnet, stagenet) (src/cryptonote_config.h). TheGENESIS_TXfor every network was regenerated during the post-v3.1.0-alpha.3work, but theNETWORK_IDhandshake identifiers were left unchanged — the exact silent-fork combination theRELEASE_PROMOTION.md§6 frozen-tuple gate warns about (same network id, incompatible genesis → peers attempt to connect, then reject each other's genesis block). Rotating the ids makes the fresh-genesis network cleanly distinct so pre-rotation and post-rotation nodes cannot even attempt to peer. Pre-genesis, no live network stands on the old tuple, so this is arm -rf ~/.shekyl+ re-sync change with no migration path (per15-deletion-and-debt.mdc). Gates the next testnet rehearsal release, which must runTESTNET_REHEARSAL_CHECKLIST.mdfrom clean datadirs per §6. - ci: CodeQL C/C++ runs buildless; ignores vendored
external/code (.github/workflows/codeql.yml,.github/codeql/config.yml). Switched theanalyze-cppjob tobuild-mode: noneand addedpaths-ignore: ['external/**']. The path filter is the goal — dropping the standing vendored highs inexternal/db_drivers/liblmdb/mdb.c(cpp/integer-multiplication-cast-to-long,cpp/comparison-with-wider-type) andexternal/qrcodegen/QrCode.cpp, which Shekyl never patches in place — but config-filepaths-ignoreonly applies to a compiled language analyzed without a build, so the buildless switch is what makes it effective (a trace-based build silently ignores it). Those queries stay live on Shekyl-ownedsrc/; the crypto/consensus/amount logic that once justified a deep trace-based build now lives in Rust (covered by the separateanalyze-rustjob), and C/C++src/currently has zero open Shekyl-owned CodeQL highs. Side benefit: drops the ~150-minute C++ build from CI. Prevents dev→main release PRs from re-attributing pre-existing library alerts as PR-introduced. The pre-existing per-idquery-filters(rust/hard-coded-cryptographic-value,cpp/upcast-array-pointer-arithmetic) are unchanged and remain a distinct, results-stage safety net.
Documentation
- docs: submit-verdict / 2c completion-status sweep (rule 91). Landed
work is now marked landed where the docs still read as pending:
DAEMON_SUBMIT_VERDICT.md§12 gains a series-status banner (all six PRs merged as #244/#247, #245, #246, #248, #249, #250) and §10 item 1 records the F40 wire amendment + R1 pin as landed (#254, R2 riding 2c-2b); thedocs/FOLLOWUPS.mdsubmit-outcome SUPERSEDED entry's absorbed remainders are closed. Two stale-doc residues the PR-6 sweep (#250) missed are corrected:V3_ENGINE_TRAIT_CONFORMANCE_LENSES.md's CL-4 exemplar andV3_ENGINE_TRAIT_BOUNDARIES.md's §4 idempotency row/retry prose no longer cite the retiredAlreadyKnowndedup heuristic —submit_transactionidempotency is stated as the verdict contract's resubmit-is-a-status-query property (§2.5/F31).
Added
-
wallet: submit lifecycle driver — the §5.3 watchdog actor + F40 re-scan executor / R2 breaker land (
DAEMON_SUBMIT_VERDICT.md§5.3 / §2.5 / §10 items 1 & 8;docs/FOLLOWUPS.mdAlreadyInChainentry, now resolved). The landed-inert submit-watchdog kernel gains its production consumer:SubmitLifecycleDriverowns the persistent overlay (escape- ladder wait epochs, alarm latches, F40 re-scan / R2 counters) and, on eachtick, projects the F14-lock-derived held submits, fetches daemon health (newDaemonEngine::get_healthsurface), runs the escape ladder per held tx (projection → health gate → rung-1 resubmit-same-bytes probe → outcome), and drains thePendingTxStaterescan queue to execute the F40 targeted re-scan. The probe uses an ephemeral in-memory held-bytes store (retained at finalize-accept / finalize-already-in-chain, pruned when the hash leaves the held projection); after a restart the bytes are gone and the probe rung degrades to the operator-alarm rung, converting a would-be silent cross-restart re-broadcast into a human decision (rule-21 reversion clause: durability-only reopen, auto-vs-confirm stays a separate human-in-the-loop axis). The re-scan is cheap-check-then-escalate: one block-hash fetch at the claimed height compared against the ledger's stored hash — match + spend still unobserved = fruitless (per-txid consecutive counter), divergence = defer to refresh reorg-heal (counter untouched), capping a lying daemon's per-lie cost at one fetch. The fruitless-soundness guard (claimed_height ≤ synced_height, re-checked at execution) keeps the inference sound: an above-synced request re-routes to the §2.6 path-1 wait, never the counter. F40-R2 trips at 3 consecutive fruitless re-scans →PendingTxDiagnostic::FruitlessRescanBreakerTrippedoperator alarm; F40-R1 is structural (the lock stays placed — no path here releases it). New bounded diagnostics:WatchdogAlarm,FruitlessRescanBreakerTripped,WatchdogProbeDispatched,WatchdogProbeResolved. Exposed via the publicEngine::run_submit_lifecycle_tick(production call site: after each refresh cycle); the former#![allow(dead_code)]on the kernel is lifted. Fifteen driver regressions land over a hermeticStubHost/StubDaemon(submit_lifecycle::tests), including the R2 breaker test explicitly deferred from #254. -
wallet/rpc:
AlreadyInChain { height }— the F40 disposition lands (DAEMON_SUBMIT_VERDICT.md§2.2 carve-out / §2.5 row / §10 item 1;docs/FOLLOWUPS.mdAlreadyInChainentry). The wire verdict grows the requiredheightfield (frozen fixture updated atomically per the §2.3 third-category rule; new skew test D pins a field-lessalready_in_chainto the deserialization-error arm). The daemon reads the confirming-block height under the same lock scope as the membership fact (in_chain_height, FFI snapshot size 80 → 88) and emits it at both the Phase-B short-circuit and the Phase-Dclassify_racesettled arm. Wallet-side,finalize_submit_already_in_chainis reshaped from the superseded "no lock, refresh settles" interim (#252) to F40: the F14 awaiting-confirmation lock is placed in both height cases (no selectable-input window either way), baselined at the claimedheight;heightroutes only the release path — above-synced waits for §2.6 path-1 refresh catch-up, at/below-synced emits the newPendingTxDiagnostic::TargetedRescanRequestedfor the reorg-heal re-scan. F40-R1 (rescan never releases) is structural and pinned by regression; the re-scan executor consumingTargetedRescanRequestedand the R2 fruitless-re-scan breaker land in the submit lifecycle driver (see the driver entry above). -
wallet: posture→submitter dispatch — the frozen SP-T4a §3.1 shape lands (
ARCHIVAL_BOND_2D2_SP_T4_BROADCAST.md§3.1, frozen 2026-07-04;docs/FOLLOWUPS.md2c-2a dispatch-shape entry). The closed-setBroadcastSubmitter<D> { Local, PerP }enum with match-delegation (RPITIT, noBox), the singlefor_postureconstruction choke point carrying the ②→PerProuting guard (anOwnRemotebroadcast can never ride the shared principal connection — the first-seen-origin leak the dispatch closes), thePBoundBytesbyte↔persona newtype equality-checked at thesubmit_boundfaçade (debug_assertloud in dev, fail-closedPersonaMismatchin release), andPTransactionSubmittertightened tofor_personaso the persona↔circuit binding is by construction and the retainedPCanonicalIdfeeds the pairing check. Lands inert (allow(dead_code); proving tests are the callers) — the 2c-2b request path is the lift condition and owns the P-1/P-2 provenance pins. -
standoff/engine/sim: 2c-2b block-timed placement wiring + GF-7 measurement seam (
docs/design/ARCHIVAL_BOND_2C_GF7_HOOKS.md§6 acceptance criteria, all five).shekyl-standoffgainsplan_entry_seam/EntrySeamPlan— the single-sourced consumer of the fulldraw_entry_gaptuple (order-coin cannot be dropped; the triangular mis-build cannot be re-introduced) — and, behind the non-defaultgf7-hooksfeature, theBroadcastTimelineObserverseam with the three-axis jointTimelineEventvocabulary. TheStakeEngine::SignBondhandler now consumes both draw values into the plan and replies withSignedBondPost(signed vin + placement plan, never decoupled), emitting draw-consumption and schedule events to the injected observer (production injects the no-op).shekyl-staking-sim --gf7-timelineis the §6.3 pipeline proof: the only recording observer, all three event classes through one recorded timeline per pair, funding-seam-blind + funding-seam grading arms over the recorded stream only (on the fixed seed the jitter degrades the funding arm 0.999→0.242 across windows 0→600 while the blind arm plateaus ~0.55 over a 0.125 baseline — the residual the measurement round will grade). CI gainsci/gf7-no-emit-guard(§6.4): a dependency-graph assertion thatshekyl-staking-simhas zero dependents and a per-crate feature-resolution check thatgf7-hooksnever resolves on production edges outside the sim, with a positive control against rename-vacuousness. The §6.5 no-behavioral-delta argument is documented in thestake_enginemodule docs. -
docs: GF-7 measurement-hook specification — the evidence pipeline for the genesis gate (2c design round,
docs/design/ARCHIVAL_BOND_2C_GF7_HOOKS.md). GF-7 (principal↔Ptiming unlinkability, GATE6 §10.12) is a genesis blocker whoseP(link | T_obs)first becomes measurable when the 2c-2b scheduler broadcasts a bond-post on a real timeline. The new spec pins what that scheduler must emit before its wiring PR lands: an injectedBroadcastTimelineObserverseam (no hardwired sink), a three-axis joint event vocabulary — principal lifecycle, bond-post with the consumed draw parameters,P's other broadcasts — graded jointly per the co-triggered-firewalls principle, with the funding-seam-blind adversary as a first-class arm (the named certifies-the-wrong-gate failure mode). Containment is build-structural, three independent layers: inert-by-type trait, sim-only recording impl enforced by a CI dependency-graph assertion, and a non-defaultgf7-hooksfeature compiled out of release (theshekyl-standoffconformanceshape). The 2c-2b wiring PR is gated on the spec's §6 acceptance criteria; threshold-setting stays with the sim-side measurement round. -
rpc:
shekyl-rpc-typescrate — the typed wallet↔daemon submit contract (docs/design/DAEMON_SUBMIT_VERDICT.md§2, PR-2 of the submit-path Rust cutover).SubmitVerdict/RejectCause/SubmitTransactionRequestas the single Rust definition bothshekyl-daemon-rpcandshekyl-rpc-clientwill consume, replacing the Monero-inherited status-string-plus-booleans reply schema. Ships with the §2.3 version-skew pins (unknown cause →Unrecognizedfail-safe; unknown verdict tag →Errarm; unknown fields tolerated), frozen JSON representations for every variant, and the §3.4 frozen-hex txid KAT (C++-daemon-captured oracle blobs provingshekyl-wiretxid ≡ C++get_transaction_hash). Contract is draft until the §8 parity matrix's ⚠-rows resolve (that document's freeze rule). -
daemon:
SEEDHASH_EPOCH_*override now refuses to start on every public network. The RandomX seed-epoch schedule is consensus; theSEEDHASH_EPOCH_BLOCKS/SEEDHASH_EPOCH_LAGenv overrides are a fakechain-only (regtest) lever. A node started with the lever active computed wrong seedheights and silently rejected every block its peers accepted (warn-only since the #235 port) — self-forking mainnet, or the testnet/stagenet rehearsal infrastructure, if the lever leaked in from a shared environment. Init now fails closed with an actionable error on MAINNET, TESTNET, and STAGENET; FAKECHAIN (regtest daemons and test fixtures — the lever's legitimate fast-epoch use) keeps the warning-only behavior. -
pow: runtime differential modes wired into CI (RandomX v2 test-regime hardening, runtime-mode change). The harness's
--mode=correctnessthree-leg differential (Rust ≡ C ≡ canonical) is now merge-blocking per-PR at per-PR sizing and full-size on the daily cron;--mode=latency(T5, ≤3.0× in-mode ratio gate, interleaved same-process sampling) and--mode=concurrent(T7 concurrent byte-equality + T8 RSS ceiling — the deferral's last unwired runtime mode) run in a dedicatedruntime-modescron job on both active branches (main + dev matrix; opt-in on dispatch via theruntime_modesinput, so a parity-recovery dispatch is not forced red by T5's disclosed known-over-budget state, and with a triage band that classifies a T5 red as known-state vs fresh regression); and the T6 adversarial worst-case-ratio gate gets a weekly cron (previously dispatch-only; also main + dev matrix) plus a concurrency group and the pinned install-rust composite (replacing adtolnay/rust-toolchain@stablemoving-ref).set_canonical's eager-derive claim is now a state-probe test (epoch rollover cannot stall the first post-boundary verify). The rustup sweep completed repo-wide: every inline/env-block rustup install (14 sites across 8 workflow files, including build.yml and the disabled release workflow) now uses the hardenedinstall-rustcomposite — cross-target sites add a separaterustup target addstep (the depends.yml pattern) — so zerocurl | shor hand-copied recipes remain. -
pow: native-aarch64 CI lane (RandomX v2 test-regime hardening PR-2). New
native-armjob onubuntu-24.04-arm(native silicon, deliberately not qemu — softfloat would mask a real FP-rounding divergence): verifier crate tests, the T2 adversarial byte-equality against an arm-built C oracle, a newcanonical_pins_fulltest re-deriving all 1024 committed canonical pins, and the harness's three-leg--mode=correctness(Rust ≡ arm-built C ≡ canonical over the same 1024) — the first-ever CI executions of thefpu_roundingFPCR path and of the correctness mode. The 1024-pin test also runs per-PR on x86_64. The rustup install (all four jobs) and the out-of-band C-reference build (the three jobs that use it;full-paritybuilds through the parent CMake project) are now composite actions (.github/actions/install-rust,.github/actions/build-randomx-v2-ref). -
pow: the Phase 3a full-dataset parity gate is wired into CI (RandomX v2 test-regime hardening PR-1). New
full-paritycron job inrandomx-v2-differential.yml: daily subset / weekly full sweep of the C full-dataset (miner mode) vs Rust light-cache (verifier) parity harness, now widened to re-check all 1024 Phase 2g canonical pins under C-full-dataset (gen-parity-corpus+corpusharness mode), plus a separate-process miner-shaped KAT provenance test (randomx-v2-miner-kat) and a binarynmsymbol-isolation gate on the linked daemon (check_randomx_symbol_isolation.sh). -
crypto/wallet: real-tree FCMP++ prove→verify roundtrip now works (
feat/ct-5-real-tree-verify). The first test to verify an FCMP++ membership proof over a real multi-layerassemble_path(join_market_bond_post_fcmp_verify_over_real_tree, previously#[ignore]d) was failingBatchVerificationFailed. Root cause:shekyl-curve-tree::assembleemits partial (narrow) branch chunks for incomplete tree nodes, but the FCMP membership circuit requires the full chunk width — a seam the upstream FCMP tests never exercise (they always fill to width), and the prover doesn't catch (it ignores the root). Fixed by zero-padding branch chunks to width inshekyl_fcmp::proof::prove/prove_with_sal: zero scalars vanish in the layer hash, so the consensus tree root is unchanged (no daemon/consensus change, no CT-2 Tier-A regression), and the daemon'sshekyl_fcmp_verify— the same Rustverifyvia FFI — accepts it. The audited vendored FCMP crypto was sound; the bug was first-party (our driving). Closes PR 2c-1's deferred verify half and the 2a real-tree milestone; covers depth-2 (deeper trees tracked inFOLLOWUPS.md). Regression KAT:ct5_partial_branch_chunk_is_padded_and_verifies. -
wire: real consensus-valid FCMP++ spend round-trip (
shekyl-wire/tests/fcmp_spend_e2e.rs). New end-to-end test that builds a real, consensus-valid FCMP++ spend entirely in Rust — a depth-3 Selene/Helios curve tree (700 outputs, exercising bothc1andc2branch layers, the multi-layer path that broke the C++ spend),assemble_path→SpendInput→sign_transaction→sign_pqc_auths— self-validates it against the Rust consensus rules (shekyl_fcmp::proof::verify,verify_rct_balance,Bulletproof::verify), then provesshekyl-wireserializes it byte-identically (read(write(x)) == x, deterministic re-emit). The oracle is the consensus verifier itself, not a captured blob. -
wire: removed the unsound
#[ignore]d live-oracle spend KAT (regtest_spend_round_trips_byte_identicalinshekyl-wire/tests/fcmp_spend_roundtrip.rs) and its capture script (tests/vectors/capture_spend.py). The KAT planned to round-trip a "known-good" FCMP++ spend blob captured from the C++ daemon, but the C++ FCMP++ spend path never produced a daemon-accepted transaction — there is no known-good blob to capture, and building one to capture would be constructing the house to draw the blueprint. The byte-identity proof now rides the Rust-built, consensus-validated spend infcmp_spend_e2e.rs(above). The syntheticread(write(x))round-trip and structural rejection tests infcmp_spend_roundtrip.rsare unchanged. -
wallet: archival StakeEngine lifecycle wiring (Model D) -- inert (PR 2c-2a) (
docs/design/ARCHIVAL_BOND_CONSTRUCTION.md§10.2,feat/archival-stake-wiring). Wires the inert 2b StakeEngine into the engine lifecycle under Model D, which eliminates session-long master-seed retention: the actor no longer owns the seed. Atassemble(), a staker-flagged wallet derives the derive-forward set —{personas with live bonds} ∪ {p_slot ..= p_slot + k}(k = 2, bounds[0, 8]) — synchronously inside the sync open/create call (staker-only; one PQ keygen per slot), hands the pre-derivedArchivalPKeysbundles to the actor, and drops the borrowed seed (&[u8; 64]) at function end, exactly as forAllKeysBlob. The wholecreate/open_fullcall is the blocking unit async callers already wrap inspawn_blocking, so the keygens are off the async hot path at that granularity; intra-call parallelism across the (small,k-bounded) set is a documented perf follow-up, not done here. Non-stakers derive and hold nothing (Engine.stake == None). Lookahead exhaustion / first-stake mid-session / post-panic recovery all resolve via reopen — no re-auth or KEK machinery.- New sealed
StakingBlockinWalletLedger(shekyl-engine-state, region-2 state file;STAKING_BLOCK_VERSION = 1,WALLET_LEDGER_FORMAT_VERSIONbumped, schema snapshot + CI pairing added). Carriesstaking_enabled, thep_slotcursor, andbonded_slots(the per-persona live-bond record). Chosen overSettingsBlock(C++/FFI surface, not loaded byassemble()) andWalletPrefs(advisory tier — a tamper-reset would silently drop funds-load-bearing state); the ledger is the semantically correct home for chain-reconciled bond bookkeeping and gives AEAD sealing +atomic_write_file(tmp → fsync → rename → fsync(parent)) crash-atomic durability. bonded_slotsis a reconcilable hint, not truth (flatVec<u32>, documented semantics) so the 2d scan-reconcile GCs phantom records (from a persist-before-sign crash) without a second version bump.p_slotis a scan-reconciled monotone cursor —current = max(persisted p_slot, highest_bonded_slot_seen + 1)(StakingBlock::monotone_current_slot) — a privacy property: it can never re-activate a retired persona (which would link its activities), and it heals a rolled-back cursor without depending on a region-2 anti-rollback property.PersistedBondTicketpersist-before-use typestate (the cross-split seam):Engine::persist_bond_record(..) -> PersistedBondTicketis the sole producer; the ticket is!Clone, consumed by value, and minted only after a durablesave_state, so 2c-2b'ssign_bond(ticket, ..)makes sign-before-persist uncallable.- Operation-scoped
PersonaHandleminted only for held personas (collapses the can't-happenPersonaUnreachableto one slot→handle boundary; keepsLookaheadExhaustedas a real domain error),!Clone, with an actorgenerationcounter that rejects a stale handle after rotation (StaleHandle) — closing the use-after-wipe on a wiped ephemeral persona. HeldPersonaBonded/Ephemeral with awipe_ephemeralpath that consumes only theEphemeralvariant, so "wipe a persona with a live bond" is uncallable — the bonded-union invariant (retired-but-bonded personas stay resident so unbonding remains reachable) enforced by type. Drops the actor'sStakeMasterSeedroot, in-handlerspawn_blocking, and in-actor derivation error path. Landed inert: wired, derived, spawned, and test-exercised (12stake_engine+ 2stake_persisttests, incl. durable persist+reopen and staker-reopen-over-bonded-union-lookahead), with no JoinMarket request path (that, the typed timing seams, the live RNG check, and the milestone KAT land in PR 2c-2b; broadcast/re-anchor + Arti transport in PR 2d). The CT-5d re-anchor finding (persona signature bindstx_prefix_hash, not the proof) is pinned now so the schema and derive-forward set are correct when 2d wires submission. SeeFOLLOWUPS.md"StakeEngine Model D wiring" for the deferred-work reopens.
- New sealed
-
crypto/economics: archival bond-post construction PR 2c-1 -- real-tree composition KAT (
docs/design/ARCHIVAL_BOND_CONSTRUCTION.md§3,feat/archival-bond-realtree-kat). PR 2a closed the bond round-trip over a synthetic depth-1 tree; PR 2c-1 re-runs the same composition over a real curve tree. The new testjoin_market_bond_post_signs_and_verifies_over_real_tree(inshekyl-engine-core::engine::local_pending_tx::tests) spends a drained leaf of the consistentfunded_ledger_and_treefixture, assembles its membership path via the productionCurveTreeClient::assemble_path(the same pathbuild_then_submit_marks_outputs_spentdrives), carries the bond'sbond_creditthroughshekyl-tx-builder::sign_transaction_with_termsas the sole output-side cleartext term over genuine depth-2 branch layers, and checks BP+, the RCT balance over prover-emitted commitments (with8*C → Ccofactor recovery), andverify_join_market_bond_post/the hybrid signature accept+reject. Scope honesty: the construct→prove half over real branch layers is proved here; the FCMP++ verify-accept half over a real multi-layer path is the one piece still gated on the CT-5 series closing the upstreamFcmp::prove↔verifyroundtrip (no workspace test verifies a real multi-layer path today; the first attempt returnsBatchVerificationFailed), so it rides as an#[ignore]d sibling (join_market_bond_post_fcmp_verify_over_real_tree) rather than a faked pass. SeeFOLLOWUPS.md"real-tree FCMP++ verify" for the reopening trigger. -
wallet: archival staking actor (StakeEngine) -- inert (PR 2b) (
docs/design/ARCHIVAL_BOND_CONSTRUCTION.md§10/§10.1,feat/archival-stake-engine). The gate-6 staking firewall realized as actor isolation: a newkameoactor (shekyl-engine-core::engine::stake_engine) that owns the wallet'smaster_seed_64and the active archival personaP, structurally disjoint from theLedgerEnginetransfer pipeline. It mirrors theKeyActor/KeyEngineHandlediscipline:- Operation-shaped protocol, public-only replies. Messages name operations
(
ActivatePersona,ActivePersona); replies carry only public material --PersonaIdentityholds the typedHybridPublicKey(P_pubkey), a type with no secret field to leak. The secretArchivalPKeysbundle (!Clone+ per-fieldZeroizeOnDrop) cannot be requested or copied out of the actor. - Typed domain values (Rust-first, rule 20):
PSlot(u32)newtype so a persona slot can't be confused with any other index;StakeMasterSeednewtype (Zeroize/ZeroizeOnDrop, notClone, redactedDebug) so the wipe discipline lives on the type; typedStakeEngineError. - Lazy derivation off the hot path. An idle actor holds no persona
(
active == None); the firstActivatePersonaderives viaderive_archival_p_keysontokio::task::spawn_blocking(PQ keygen is CPU-bound). The only copy of the seed leaves as aZeroizingbuffer wiped at closure end. - Atomic rotation + re-derive-on-restart. Slot rotation is a single
state transition (derive-into-local, then one assignment that drops/wipes the
old) -- never two live personas (§10.9), never a gap with none. The bundle is
not persisted; the actor re-derives deterministically from
master_seed_64on every (re-)spawn. - Fail-stop, not supervised. A handler panic returns
ControlFlow::Break; afterward every handle call collapses to the terminal, user-facingStakeEngineError::StakeActorUnavailable("close and reopen the wallet to recover"). Recovery re-derives everyP. Landed inert: registered and exercised by tests only (idle-has-no-persona, lazy derive matches theARCHIVAL_P_DERIVE_V1oracle, rotation, cross-respawn determinism, impermissible-seed-format error, require-ambient-runtime, fail-stop terminality). PR 2c wires it into the open/create lifecycle with its first consumer (the JoinMarket bond request), so no seed-holding actor comes into existence until something uses it.
- Operation-shaped protocol, public-only replies. Messages name operations
(
-
crypto/economics: archival bond-post construction PR 2a -- full-prover synthetic-tree round-trip KAT (
docs/design/ARCHIVAL_BOND_CONSTRUCTION.md§3,feat/archival-bond-roundtrip-kat). PR 1 closed the bond balance against a hand-built (synthetic) balance witness; PR 2a closes it against the real FCMP++ prover. The new testjoin_market_bond_post_signs_and_verifies_through_prover(inshekyl-engine-core::engine::local_keys::tests, reusing the M3c synthetic depth-1 tree and key-derivation fixtures rather than promoting them to public API) builds a JoinMarket vin withshekyl-archival-bond-builder, carries itsbond_creditthroughshekyl-tx-builder::sign_transaction_with_termsas the sole output-side cleartext term, and checks the prover-emitted commitments against the genesis-frozen verify side:verify_join_market_bond_post, the hybrid signature underP_pubkey, the Bulletproof+ range proof, the FCMP++ membership proof, andverify_bond_post_rct_balance. It also asserts the verify side rejects a wrongbond_credit(SumMismatch), a tampered output commitment, a tampered signature preimage, and a replayed post (RecordExists) -- the honest milestone is "valid accepts and invalid rejects." One encoding bridge:sign.rsemits output commitments as8*C(wire cofactoring) while the balance equation is defined over the prime-orderC, so the KAT recoversC = (8*C) * 8^{-1}before the balance check. Addsshekyl-archival-bond-builder+shekyl-archival-retentionasshekyl-engine-coredev-deps (first-party path-deps, no new third-party supply-chain surface, no dependency cycle). Tree stays synthetic; on-chain end-to-end remains CT-5 work. -
wallet: CT-5d submit-time proof re-anchor (reprove) + consent gate (
docs/completed/CT5D_REANCHOR.md,feat/ct-5d-reanchor-closeout). A built-but- unsubmitted FCMP++ proof anchors to a reference blocktip − REF_ANCHOR_AGE; a tip advance past the rebuild horizon, or a reorg that orphans the reference, would otherwise degrade to a submit-time daemon rejection. The submit path now re-anchors instead of rejecting:- The reference is the submit staleness authority (
should_reanchor || reference_orphaned), replacing theSnapshotIdcheck that invalidated on every block — a benign tip advance now broadcasts the existing proof as-is. - When stale, submit reproves in place (fresh reference, re-assemble,
re-sign) three-phase so the prover never holds the pending-tx lock (mirrors
build; the shape the Stage-4PendingTxActormigration needs). The fresh reference is selected under a two-sided ingested-tip gate (anchors at/below the ingested tip; refuses a reference already past the rebuild threshold). - Consent:
submit(id, seen_gen)gates broadcast on acontent_genthat advances only when the re-anchor changes the realized(fee, recipients, change)— fingerprinted semantically + canonically (excludes the re-randomized change address; invariant under the output shuffle). A change is withheld asSubmitError::ContentChangedfor re-confirm; broadcasting unauthorized content is unrepresentable. ConsumerHeldEntrycarries the rebuild substrate (request,reference,content_gen, fingerprint);PendingTxexposescontent_gen+reference_height. Runtime-only state — no schema migration.- Deferred (rule-21): the content-changing reselect path (deep reorg
orphaning a selected input, or fee escalation) fails clean as
SubmitError::ReselectionRequired(discard and rebuild) rather than transplanting locks under the stable rid; seeFOLLOWUPS.md.
- The reference is the submit staleness authority (
-
crypto/economics: archival bond-post construction PR 1 -- single-sourced RCT balance + JoinMarket builder (
docs/design/ARCHIVAL_BOND_CONSTRUCTION.md§7,feat/archival-bond-builder). The construct side of the archival bond, the mirror of the genesis-frozenshekyl-archival-retentionverify side. Three pieces:shekyl-rct-balance(new crate) single-sources the genesis-frozen RCT cleartext balance equationsum(pseudoOuts) + extra_inputs = sum(out_masks) + fee + extra_outputsand its typed-side terms. The side is a type, not a runtime tag:InputTermandOutputTerm(checked-u64newtypes overAtomicUnits) make a wrong-side cleartext term unrepresentable. The verify equation that previously lived inshekyl-archival-retention::bond_rct_balancemigrated here;bond_rct_balancenow wraps it with the bond-specific credit-xor-debit term rigidity. Imported by bothshekyl-tx-builder(construct) andshekyl-archival-retention(verify), so the two cannot diverge on a consensus rule (§7.2 / §11.1 Q2).shekyl-tx-buildergainssign_transaction_with_terms, threading theshekyl-rct-balancetyped-side terms into funds-sufficiency validation;sign_transactionis now a zero-terms wrapper, so the transfer path is unchanged. The crate stays bond-agnostic (never names "bond"; it consumes generic typed-side terms).shekyl-archival-bond-builder(new crate) builds the JoinMarketArchivalBondPostVin(bonded_total == bond_credit == bond_floor,bond_debit == 0), signs its preimage with thePidentity hybrid key, suppliesbond_credit = flooras theOutputTerm, and enforces the credit funding rule. The masked commitments + FCMP++ membership proofs are left to the prover (PR 2); re-deriving them here would be a second construction path for a genesis-frozen commitment. A round-trip KAT (tests/join_market_round_trip.rs) derivesP, constructs + signs the vin, and confirmsverify_join_market_bond_post+verify_bond_post_rct_balanceaccept against a synthetic balance witness -- the honest §3 milestone (not an on-chain bond; that is CT-5 work). Cross-refs:FOLLOWUPS.md(construction-flow item; "RCT" naming-review item deferred to a separate PR).
-
docs: design the wallet-side archival bond-post construction flow (
docs/design/ARCHIVAL_BOND_CONSTRUCTION.md, 2026-06-17,docs/archival-bond-construction-design). The verify side (shekyl-archival-retention) is the fixed, genesis-frozen contract; construction genuinely does not exist yet (no builder, nobond_creditin the RCT balance, noP-identity derivation). The doc specifies the full four-kind architecture (so JoinMarket-first does not paint into a corner), implements JoinMarket only, and single-sources by importing the validated verify-side types. Key calibrations recorded: (a) the honest milestone is the round-trip KAT (construct -> verify accepts) against a synthetic curve tree, not a bond on a real chain -- on-chain is a sequenced dependency on the realCurveTreeClient(CT-5); (b)shekyl-tx-buildergains a generic extra cleartext balance term (the wayfeeis one; the verify side already treatsfeeandbond_creditsymmetrically asamount_h(...)), bond-named only in the bond crate; (c) the other three kinds (Rebond/Unbond/HoldingsUpdate) are provisional (paper-only, reopenable when their verify+construct PRs land); (d) PR 2 runs the standoff self-cert (certify_drawagainst the wallet's actual CSPRNG), not justdraw_entry_gap. Review target 2-3 rounds (not 4-6: the construct side is largely determined by the validated verify side). PR 0 (archival_pderivation) is built in parallel, not gated on the doc's rounds -- its design is settled (gate-6 §9.3/§9.4) and it is the highest-stakes primitive in the project. Cross-refs:FOLLOWUPS.md(construction-flow item),ARCHIVAL_FIREWALL_GATE6.md§9,STAKER_OPERATOR_GUIDE.md(slices i/ii attach here). -
docs: publish the staker-operator opsec + setup guide (
docs/STAKER_OPERATOR_GUIDE.md, 2026-06-17,chore/staker-operator-guide). First slice of the operator-experience work unit (the post-seal pivot: the staker-archival parameters are frozen; the remaining risk lives in operator behavior, where the sim is silent). The guide consolidates the residuals the design rounds routed to operator education into one audience-scoped doc: the drop-to-reallocate capital strand (freed collateral is frozen for the 2-epoch release cooldown -- do not drop a shard to fund another within it), the shared-anchor funding tell (let the wallet draw the uniform-independent entry gap; do not batch/schedule/hand-roll it), the anonymity-transport andP↔principal isolation opsec (loud refusal, never clearnet fallback), and the cold-start thin-cover consequence (earliest operators carry the thinnest entry cover and must be the most conservative). The guide is also the spec the V3.1 wallet-conformance guards implement against; items it can only ask the operator to self-enforce today are marked[operator-enforced until V3.1]. Cross-refs:FOLLOWUPS.md(operator-experience item, slice 1 landed). -
archival: single-source the funding-seam entry-standoff draw into
shekyl-standoff+ generic goodness-of-fit primitives intoshekyl-stats(2026-06-16,feat/standoff-shared-crate). First slice of the implementation-drift-guard substrate: the load-bearing standoff draw moves out of#[cfg(test)]-local sim code into a library crate the wallet, sim, and (future) verifier all import, so "what we validated is what ships" holds by construction.shekyl-standoffships a float-freedraw::draw_entry_gap(s ~ U[0, window]via unbiased integer rejection sampling over a minimalGapRngtrait) in its default build — zero#[allow(clippy::cast_*)], so float-freeness of the wallet's production path is machine-enforced, not a module-boundary claim — and gates the float grading + RNG-genericcertify_drawself-cert harness behind aconformancefeature. The published KAT is split along the determinism boundary:tests/golden_vector.rs(default features, the integer(spread, order)sequence, bit-identical and now run on the aarch64 qemu-user lane) andtests/conformance_grading.rs(conformancefeature, float grading, x86-only because float is not bit-identical across arches).shekyl-statsis the new dependency-free home for the generic primitives (chi_square_upper_crit,Z_ALPHA_1E6, discrete-uniform chi-square,lag1_autocorr);shekyl-staking-simandshekyl-crypto-pq(itsenc_labeltest, previously carrying duplicated literals) both dedup onto it. No wallet call site yet — the crate is made importable ahead of the unbuilt V3.0 funding flow. Docs:ARCHIVAL_FIREWALL_GATE6.md§10.12,STAKER_ARCHIVAL_SIM.md,FOLLOWUPS.md(funding-seam carry 2 + wallet-wiring + alpha-provenance items).
Fixed
- tx-builder: FCMP++ output commitments are the real Pedersen commitment, not
cofactor-scaled.
shekyl-tx-builder::signnow emitsoutPk[i] = C = mask·G + amount·H(×1) instead of8·C. The consensus balance checksum(pseudoOuts) == sum(outPk) + fee·H(verRctSemanticsSimple) sums these points directly and the pseudo-outs are realC, so the output side must match — the prior8·Cbroke balance verification. Surfaced and validated by the newshekyl-wire/tests/fcmp_spend_e2e.rs(itsverify_rct_balancestage). The C++/Rust FFI spend-path debugging that surfaced this was otherwise reverted as out-of-scope C++ debt; the findings are captured inFOLLOWUPS.md("C++ FCMP++ wallet send path incomplete"). The wire/scanner contract already required realC— the production scanner recomputesz·G + amount·Hand byte-compares it against the on-wire commitment, andsign_bridgecopiesSignedProofs.commitmentsstraight onto the wire — so the fix aligned the builder with the rest of the stack. The downstream documentation and the twoshekyl-engine-corebond-post round-trip test helpers (which had compensated for the old encoding by multiplying by8⁻¹) were reconciled to feed the realCdirectly.
Removed
- rpc: delete the legacy transaction-submit surface
(
docs/design/DAEMON_SUBMIT_VERDICT.md§9, PR-5 of the submit-path Rust cutover,chore/submit-legacy-deletions). The typed/submit_transactionroute (Rust admission engine, PR-3) is the only submit surface; the §9 deletion enumeration executes with its greps re-run as the completeness check:on_send_raw_txendpoint (§9.3): handler, epee route maps, theCOMMAND_RPC_SEND_RAW_TXdefs with their eleven-boolean reply schema, thecore_rpc_ffiDJSON dispatch entries, and theshekyl-daemon-rpcaxum proxy routes.wallet2::commit_txports to the typed endpoint via a transport-only epee mirror (COMMAND_RPC_SUBMIT_TRANSACTION); the trezor mock daemon serves the typed route as a test-only pool-admit stand-in.tx_sanity_check(F29, §8.8/§9.2): S1 duplicated the Rust Phase-A parse gate; S2 (coinbase-submit reject) lives in the engine; S3 (decoy-median heuristics) was verified vacuous at runtime under FCMP++ (emptykey_offsets→ unconditional early-returntrue).m_timed_out_transactions(D3, §9.1): the RAM-only eviction memory whose resubmit gate produced unexplained false-terminal rejections, contradicting the §5.2 retry contract. Resubmits of evicted bytes now re-enter full admission and resolve to a definite verdict.- python-rpc
send_raw_transactionhelper: superseded bysubmit_transaction(PR-3). - Kept per §9.4: the P2P
add_txingestion path (with the PR-3 double-spend regression pin),tx_verification_contextas an internal P2P contract, Dandelion++ relay machinery, and the relay-onlyon_relay_txoperator endpoint.
- rpc: delete the RPC-payment subsystem in its entirety (legacy-PoW cleanup,
2026-06,
chore/rpc-payment-deletion). The Monero-inherited pay-for-RPC subsystem — clients hash RandomX/CryptoNight to earn "credits" they spend on daemon RPC calls — is removed across the daemon, the legacy wallet, the daemon↔wallet wire contract, and the Rust wallet's persisted prefs. It was orphaned dead code (the Phase-0 "delete in its entirety" decision,RANDOMX_V2_RUST.md§15) and the named blocker for the deferred Phase 3c/4 PoW C-core cleanup (FOLLOWUPS.md); this PR unblocks that cluster. Plan:docs/completed/LEGACY_POW_CLEANUP_PLAN.md. Net ≈1000–1500 lines across ~20 files; consensus-inert (RPC access control, not block validation — the parity/mining gates are untouched).- Wholesale-deleted files:
src/rpc/rpc_payment.{cpp,h},src/rpc/rpc_payment_costs.h,src/rpc/rpc_payment_signature.{cpp,h},src/wallet/wallet_rpc_payments.cpp,src/wallet/wallet_rpc_helpers.h. - Daemon: removed
core_rpc_server::check_payment(), theCHECK_PAYMENT*macros and all per-handler pay-gates, them_rpc_payment/m_rpc_payment_allow_free_loopbackmembers +init()plumbing, theCOMMAND_RPC_ACCESS_{INFO,SUBMIT_NONCE,PAY,TRACKING,DATA,ACCOUNT}endpoints (structs + handlers +core_rpc_ffi.cppdispatch), therpc_paymentsCLI command (command_server/command_parser_executor/rpc_command_executor), and the payment parameters frombootstrap_daemon.RPC_TRACKERis kept as pure perf timing (itsRPCTrackerpayment-tracking class is gone). - Wire contract:
rpc_access_request_base/rpc_access_response_base(theclient/credits/top_hashfields) collapse into the plainrpc_request_base/rpc_response_base;CORE_RPC_VERSION_MINOR15 → 16(backward-incompatible, but in-tree clients only, pre-genesis —60-no-monero-legacy.mdc/15-deletion-and-debt.mdc). - Legacy wallet: removed the pay-for-RPC client wiring from
wallet2.{cpp,h},node_rpc_proxy.{cpp,h},wallet_args.{h,cpp},wallet_rpc_server.cpp, and thepayment_requirederror (wallet_errors.h). - Rust wallet: removed the orphaned
RpcPrefspersisted-prefs bucket (persistent_rpc_client_id,auto_mine_for_rpc_payment_threshold,credits_target) fromshekyl-engine-prefs— it had no reader or writer;PREFS_SCHEMA_VERSION2 → 3(pre-genesis:rm -rf ~/.shekyl, no migration owed).shekyl-daemon-rpcdrops the threerpc_access_*restricted-method names; the Python RPC test framework drops its sixrpc_access_*wrappers. - Out of scope (still deferred, tracked in
FOLLOWUPS.md): RandomX v1 (rx-slow-hash.c) is retained as the consensus rollback hatch; CryptoNightslow-hash.cstays (live legacy C++ KDF callers —chacha.h,cryptonote_format_utils.cpp— migrate to argon2id first); theRX_BLOCK_VERSION#definestays (still referenced bymining_parity.cpp/chaingen.cppon the PoW-dispatch surface); theIPowSchema/pow_registry/shekyl-consensusabstraction layer stays.
- Wholesale-deleted files:
Changed
-
wallet: submitter error split from the reservation-bound orchestrator error (
docs/FOLLOWUPS.md"Submit-error reservation-id placeholder", closed; 2c pre-wiring).TransactionSubmitter::submitnow fails with a reservation-unawareSubmitterError(RejectedTerminal/RejectedRetryable/Ambiguous— a closed enum carrying noReservationId); the reservation-boundSubmitErrorarms are produced only byLocalPendingTx's finalizers, which bind the reservation under submit. The formerReservationId::new(0)placeholder in the submitter arms is unrepresentable, so the 2cStakeEnginesubmit consumer — which has no orchestrator re-binder — cannot observe a fabricated reservation id. Behavior-preserving for the existing wallet path (the finalizers already re-bound the real id); the change is to the type surface the second consumer will build against. -
wallet:
AlreadyInChainsubmit verdict gets its distinct §2.5 lock-lifecycle disposition (interim) (docs/FOLLOWUPS.md"AlreadyInChainsubmit verdict", still open — F40 conformance pending; 2c pre-wiring;DAEMON_SUBMIT_VERDICT.md§2.5/§2.6).TransactionSubmitter::submitnow succeeds withSubmitSuccess, splitting theOkarm by disposition:Broadcast(Accepted/AlreadyInPool) places the F14 awaiting-confirmation lock as before;AlreadyInChainresolves through the newfinalize_submit_already_in_chain, releasing the reservation and its output locks without placing a fresh awaiting-lock — refresh remains the settlement authority. Fixes the collapsed-Ok(hash)behavior that stranded inputs "awaiting confirmation" whenever the confirming block was at/below the wallet's synced height (nomark_spentre-observation, watchdog confirmed-absent release unbuilt). Note: this change predates the F40 design merge (AlreadyInChain { height }, §2.2 carve-out); it carried noheightdiscriminant and implemented only the "no lock" shape the design round superseded. The F40 reshape landed in this same unreleased span (see theAlreadyInChain { height }entry above); its interim regression test is replaced by the two F40 routing tests. -
docs: constant-work-on-Conceal named as invariant F41 (2c design round,
docs/design/DAEMON_SUBMIT_VERDICT.md§3.1/§10/§11). The stem-presence-oracle closure was held by an accident of absence: theConcealpath runs the full Phase-C battery and no verification cache exists — true, load-bearing, and previously asserted by nothing, so the first well-meaning perf PR adding a txid cache would have re-opened the oracle as a timing oracle (fast cached hit vs full-verify tells the foreign prober what the verdict no longer does). F41 states the coupling: any submit-path cache must exempt the Conceal path or equally delay it. Companions: a §10 timing-uniformity tripwire test (both Conceal and fresh-bytes submits run the full battery, asserted by verifier invocation counting); a §11 reversion clause naming the only terms a cache may land under (rate-limiter + F39 semaphore shown insufficient first); and the DoS-relief sibling pinned at the transport layer — per-source rate limiting, Owner never limited, Foreign per-source-limited — so the pressure that would motivate the cache is relieved where it cannot touch the timing property. Structural round (same day): F41's enforcement decomposed into three layers, each vector matched to what its layer can prove — the engine-cache vector (the realistic kill) becomes a compile error via move-only disclosure-capability tokens minted atdisclose_pool_presence(MustFullyVerifyon Conceal /FastPathEligibleon Reveal/Absent; cache lookups accept only the latter; the Conceal route consumes the former by exchanging it for the §3.3 certificate — the P-1/P-2 mint-once/carry-through idiom applied to a capability); the transport-memoization vector becomes a named assumption with its own engine-entry test; and the invocation-count tripwire guards only the irreducible battery-timing residual, its necessary-not-sufficient seam documented (types don't prove timing — stated, not hidden). The tokens' sole-origin is itself structural, not conventional: both types arepub(crate)with a single constructor reachable only fromdisclose_pool_presence, inheriting the F25 write-site enumeration audit (§3.5 item 4 — "grep token construction = grep the disclosure choke point"), closing the construct-by-alternate-constructor side door (test helpers,Default,From) that the carry-not-reconstruct defeat alone does not answer. The §11 reversion clause is rewritten around the token: a Reveal-path cache type-checks freely; touching Conceal requires a compile-visibleMustFullyVerifyconversion naming F41. Implementation obligations and the ordering constraint (tokens + tests precede the first perf/cache PR touching submit) tracked inFOLLOWUPS.md. -
docs: dispatch-shape post-freeze wargame — byte-holder enumeration confirms the freeze, adds two provenance pins (2c design round,
docs/design/ARCHIVAL_BOND_2D2_SP_T4_BROADCAST.md§3.1.1). The frozen §3.1 shape (closed-set enum + constructor choke point +PBoundBytesequality check) pins the check but not the provenance: the F31 status-query resubmit and the watchdog probe rung re-send held bytes, and a held record storing rawVec<u8>forces a re-wrap at probe time — the choke point then validates the wrapper's claim, not the bytes' provenance, reopening the cross-persona/principal routing hazard on the retry axis. Two freeze-compatible pins close it: P-1 single private mint site in the assemble/sign module (possession is proof of provenance); P-2 the held record stores thePBoundBytesvalue itself, so every resubmit re-sends the stored value through the same choke path. Freeze confirmed, no reopen; the pins are recorded as §3.1 part 6 (mint-once / store-wrapped — an addition beside the frozen five) and are implementation obligations for the 2c wiring slices. -
docs:
AlreadyInChain { height }— the F40 lock-release discriminant (2c design round,docs/design/DAEMON_SUBMIT_VERDICT.md§2.1/§2.2/§2.3/ §2.5/§4.1/§7.2/§10; closes the FOLLOWUPSAlreadyInChaindesign half). The unitAlreadyInChainverdict forced the wallet to guess which release path clears the awaiting-confirmation lock — a guess steerable by anyone who can slow the wallet's own daemon's block delivery into a selectable-input leak plus F28/F37 alarm fatigue. The verdict now carries the confirming-blockheight: the lock is placed in both height cases, andheightroutes the release (refresh catch-up above the synced height; targeted re-scan at/below it, falling through to the F31 status query). Recorded as a reasoned carve-out from §2.2 wire minimalism admitted through the §5 inform-never-drive principle, held by two pinned rules — F40-R1: a below-synced claim authorizes a re-scan and never a release (release stays refresh-/watchdog-authoritative); F40-R2: fruitless daemon-directed re-scans are breaker-bounded (F28/F37 family) to operator alarm, closing the lie-low wallet-work amplifier — plus a new §7.2 trust-rider row (damage-capped both directions), a new §2.3 schema-evolution category (required-field additions: atomic pre-genesis, optional-with-default post-genesis), and a §2.2 disambiguation note keeping F40 (wallet consumes a bounded height discriminant) distinct from F22 leg 2 (daemon cache fails to bind height — the opposite direction on the same field). Implementation lands with the 2c submit-consumer slices. -
docs: submit-verdict series stale-doc sweep (
docs/design/DAEMON_SUBMIT_VERDICT.md§12 PR-6, rule 91,docs/submit-verdict-sweep). Docs describing the deleted legacy submit surface are corrected rather than left misleading:DAEMON_RPC_RUST.mdendpoint coverage recounted post-deletion (the native Rust/submit_transactionroute is now first-class);PHASE_2A_SEND_PATH.md§3.6 carries a superseded banner mapping the historical honest-subset design onto the shipped verdict contract;SHEKYLD_PREREQUISITES.md§5 records its stale-root audit as resolved; and theFOLLOWUPS.mdfcmp_root_staleitem is closed ahead of its V3.1 target — theProofStalereversion clause's reopening criterion fired, satisfied byRejectCause::StaleRootwith finer cause resolution than the item's own spec. -
wallet: submit path cut over to the typed
SubmitVerdictcontract (docs/design/DAEMON_SUBMIT_VERDICT.md§12 PR-4,feat/wallet-submit-cutover). The wallet now consumes the daemon's atomic Rust verdict instead of the legacy status-string reply, and its post-submit state machine is re-shaped around it:- Typed client.
shekyl-rpc-client::publish_transactionposts the nativesubmit_transactionroute and returnsSubmitVerdict; the string-keyedTxRelayResponseis deleted. The transaction id is computed locally from the held bytes, never read from the daemon (wire minimalism, §2.2). - Outcome reshape.
TxSubmitOutcomeis the 1:1 wallet projection of the verdict (Submitted/AlreadyInPool/AlreadyInChain/Rejected { cause }). The wallet-sideAlreadyKnowndedup heuristic is retired in favor of daemon-attested identity facts, and the formerly-deferredProofStaledetection is now constructible asRejectCause::StaleRoot(closes that FOLLOWUPS reopening criterion). - Per-cause dispositions (§2.5). Terminal rejections release locks
and surface a
TerminalErrorKind; retryable rejections (StaleRoot,ReferenceTooRecent,ReferenceNotFound) restore the reservation to consumer ownership losslessly with output locks retained. - F28/F37 loop-breakers. A second consecutive
Malformed,FeeTooLow, orUnrecognizedrejection trips a circuit breaker: further builds are refused withSendError::SubmitLoopBreakerTrippeduntil operator acknowledgment — fee-driven rebuild loops leak co-ownership per iteration, so the bound is a privacy control. - F14 awaiting-confirmation lock (§2.6). Submit-accept no longer
marks inputs
spent = true; it places a persistedawaiting_confirmationlock (tx hash + accepted height) on each input, released either by observed chain confirmation (confirmed-present → spent-final viamark_spent) or by the watchdog's confirmed-absent path. A restart between accept and confirmation can no longer self-inflict a same-key-image broadcast.LEDGER_BLOCK_VERSION7→8,WALLET_LEDGER_FORMAT_VERSION9→10; balance reporting gains anawaiting_confirmationbucket excluded fromunlocked. - Watchdog decision kernel (§5.3 scaffolding). Pure, unit-tested
escape-ladder logic in
engine/submit_watchdog.rs: chain-confirmation-keyed held tracking, presence branching (absent → re-offer same bytes; present-but-unconfirmed past horizon → operator alarm, never auto-rebuild), daemon-health gating, and an escape horizon bounded inside the daemon's 1.5-day re-relay window (F35). The async actor that drives it lands with the refresh-loop integration.
- Typed client.
-
wallet: migrate the scan/refresh block-parse from
shekyl-oxidetoshekyl-wire(§8 step-4 scanner slice, 2026-06,feat/scan-refresh-wire-migration). The wallet's block/transaction parse on the scan and refresh path now rides the canonical, daemon-KAT'dshekyl-wirereader instead of the legacyshekyl-oxideblock/tx serializer.ScannableBlockrelocated + re-typed. It is no longer ashekyl-rpctype built fromshekyl-oxide; it is ashekyl-wire-typed struct owned byshekyl-scanner(block/transactions/first_output_index). Theoutput_index_for_first_ringct_outputfield is replaced byfirst_output_index: Option<u64>(sourced fromget_o_indexes), retiring the inheritedringctname (closes the FOLLOWUPSringct-residue rename item for this surface).- Native fetch. Blocks are fetched through the new
DaemonEngine::fetch_scannable_block(default impl inengine/block_fetch.rs), which drives theshekyl-rpctransport and parses viashekyl-wire.scan.rsandengine/curve_tree_decode.rsconsume the wire types directly;unlock_time: u64is mapped to the scanner'sTimelockat the wire→scanner boundary. - Fixes
Engine::start_refreshfailing at block fetch withRpcError::InvalidNode("invalid block")against a live daemon: theshekyl-oxideparse dropped the coinbaseNullconfidential-tx committed base, so real daemon blocks would not deserialize. New live regtest acceptance gatee2e_refresh_scans_coinbase_balancemines to the wallet and asserts a matured coinbase balance after refresh. - Removed the now-dead
shekyl-rpcScannableBlockstruct and itsget_scannable_block/_by_hash/_by_numberdefault trait methods (zero callers post-migration;15-deletion-and-debt.mdc). - Scope. This is the scanner/refresh slice only; the
shekyl-tx-builderspend-encode cutover and the finalshekyl-oxideblock/tx deletion remain (the scanner still imports theTimelock/StakingMetadomain types fromshekyl-oxide::transaction). Seedocs/design/GENESIS_TX_WIRE_FORMAT.md§8 step 4.
-
consensus: collapse the Proof-of-Work path to RandomX v2; remove CryptoNight and RandomX v1 (RandomX v2 genesis cutover, 2026-06,
feat/randomx-v2-genesis-cutover). Every block — genesis included — now verifies through the Rust RandomX v2 light-cache verifier (shekyl_pow_randomx_v2_hash, exported fromshekyl-ffi); miners run the v2 C library (external/randomx-v2, full-dataset fast mode). The permanent C-JIT-for-mining / Rust-interpreter-for-verification split is sound because v2's light-cache hash equals its full-dataset hash; the release-gate Hole-1 differential test (tests/randomx_v2_parity/randomx_v2_full_parity.cpp) proves C-full ≡ Rust-light over a corpus + a frozen in-process full-dataset KAT (a separate-process miner-run KAT is deferred; see FOLLOWUPS), halt-on-red. Seedocs/design/RANDOMX_V2_RUST.md,docs/design/RANDOMX_V2_PLAN.md, anddocs/design/RANDOMX_V2_PHASE3_PLAN.md.- Removed from the consensus path (
60-no-monero-legacy.mdc):src/crypto/pow_cryptonight.cppand theget_cryptonight_*schema;get_pow_for_heightcollapsed to RandomX for every block version; allRX_BLOCK_VERSIONconsensus guards; both block-202612longhash fossils plus the sibling block-id-202612fossil incalculate_block_hash(whose vestigialblobparam was dropped); the dead CryptoNight RPC branch; and the transientSHEKYL_RANDOMX_V2_VERIFYbuild flag (the v2 path is now unconditional). Seed-hash resolution is unconditional. - New FFI surface:
shekyl_pow_randomx_v2_hashandshekyl_pow_randomx_v2_set_canonical(the synchronous, off-hot-path canonical-cache pin that delivers the ratified sticky-eviction DoS mitigation —RANDOMX_V2_RUST.md§5/§6). - Genesis identity is unchanged, and now gated. Genesis mines at
difficulty 1, so the first nonce tried (
GENESIS_NONCE) wins regardless of the longhash algorithm, and the block id isKeccak(header)— PoW-independent.mining_parity.cpp'sgenesis_identity_is_pow_independentfreezes the per-net genesis nonce + block id (mainnet919f8db5…, cross-checked against the daemon-captured height-0 hash inrust/shekyl-wire/tests/vectors/regtest_coinbase_hashes.json); the frozen coinbase/block-hash vectors needed no regeneration. - Deferred to Phase 3c/4 (blocked by the RPC-payment subsystem
deletion +
wallet2.cpptouchpoints; tracked indocs/FOLLOWUPS.md): deletingrx-slow-hash.c/slow-hash.c, theseedheightexport +shekyl-pow-randomx::consensusmodule, theRX_BLOCK_VERSION#define, and theIPowSchema/pow_registry/shekyl-consensusabstraction layer.
- Removed from the consensus path (
-
wallet: cut the spend path over to real FCMP++ membership proofs; delete the synthetic placeholders (CT-5c assembler cutover, 2026-06-18,
feat/ct-5c-assembler-cutover). The send path now assembles the real curve-tree membership path the daemon will verify, instead of synthetic vectors.buildsplits into synchronous selection → one batchAssembleTxactor round-trip → a fold into the signing context (the tx-levelReferenceBlockcarries the consensus root + block hash; the per-input paths carry the real leaf chunks and branch layers). Owned outputs resolve bygindex(the tree's unique key, X3) with a post-resolution(O, C)consistency check (ClientError::IdentityMismatch) that guards the tree-next_output_seq↔ scanner-global_output_indexnumbering equivalence; the assembler's reference param drops toReferenceBlock(T1) and the depth is read from the assembled path / aroot_and_depth_atsnapshot rather than a hardcoded1(T2). A spend now requires the curve tree — the no-tree synthetic fallback is gone (synthetic_treesurvives only#[cfg(test)]for the weight/signing KATs that need depth-controlled fixtures; the A4 reversion clause ofCT5C_ASSEMBLER_CUTOVER.md).- Fixes a latent FCMP++ proof-input bug:
shekyl-tx-builder's branch-layer validation had the C1 (Selene) / C2 (Helios) parity inverted (expected Selene as the first branch above the leaf; the real curve tree and the prover are Helios-first). The pre-cutover synthetic generator shared the same inversion, so the two agreed while both disagreed with the real tree — a wrong-layout proof the daemon would have rejected. Surfaced by the realassemble_pathand corrected toc2 = ceil(B/2), c1 = floor(B/2). - Calibrates the fee model against measured proof sizes: the
deferred-to-CT-5 placeholder (a flat
+320bytes/layer that under-estimated real depth-2 proofs → under-paid fees) is replaced by a measured[n_in][depth]table. The series is non-monotonic in depth (the FCMP++ inner-product proof rounds), so a closed-form per-layer increment is impossible; the table is emitted over depth-consistent single-path synthetic trees (PF7) and its depth-1 column cross-checks the prior measured row.
- Fixes a latent FCMP++ proof-input bug:
-
wallet/scanner:
eligible_heightnow floors on the output's additional timelock and stake lock, not just+SPENDABLE_AGE(CT-5c X5, 2026-06-17,feat/ct-5c-x5-eligible-height).TransferDetails::from_wallet_outputpreviously seteligible_height = block_height + SPENDABLE_AGEunconditionally. That undercounts for a coinbase (whose leaf is inserted into the curve tree at its block-based maturity lock, not at+SPENDABLE_AGE) and for a staked output (locked until its tier'slock_blockselapse), so reference-block selection could treat such an output as spendable while its leaf is still absent from the tree, attempting an unprovable spend.eligible_heightis nowmax(block_height + SPENDABLE_AGE, additional_timelock_block, stake_lock_until), agreeing with the tree's insertion height. A wall-clockTimelock::Timeis enforced at consensus rather than via the tree-insertion height, so it contributes no block floor. Pure derive-time change, no persisted-schema bump. KATs cover the baseline, the block-timelock floor, the ignored time-timelock, and the stake-lock floor. -
archival: verifier-set spec-level model recorded in firewall §10.4; cold-start weak-cover residual ratified (pre-seal decisions, 2026-06-16,
feat/standoff-shared-crate). Two doc-only pre-seal dispositions, no code or parameter change. Verifier set (§10.4): the bonded-verifier disposition is given a concrete structural model — membership = the bonded archival staker set itself (mutual oversight, no separate privileged registry), rotation = consensus-seeded, per-epoch, firewall-aware (discharges the §10.4 condition-(a) chokepoint structurally: no verifier accumulates a standing{.onion ↔ P ↔ shard}view, assignments excluded where they would leak the map), and them-of-navailability margin (L14b condition (b), provisional11/13) routed to the Round-2 stressnet availability CDF gate for gate-2/gate-4 to ratify from measurement rather than assertion. Cold-start residual: ratified to accept documented weak early-Pcover for V3.0 — decoy injection is self-undercutting (foundation decoys are attributable, so a sophisticated adversary discounts them) and no non-attributable source exists short of reopening confidential staking (S-5, closed); the residual is a bounded, self-resolving cold-start transient (cover strengthens as organic funding accrues), so V3.0 ships it documented rather than adding permanent decoy-injection machinery for a finite problem. Reopen criteria (rule-21) recorded on both. Cross-refs:ARCHIVAL_FIREWALL_GATE6.md§10.4,FOLLOWUPS.mdfunding-seam entry-standoff carry (3). -
archival-sim: mid-band
age_weightlever characterized and CLOSED — keep the minimal floor+no-cushion posture (decision, 2026-06-16,feat/standoff-shared-crate). The faithful-freeze fix reopened economics finding 6 with an apparent emergent 7th replica atage_weight >= 7, and the pre-seal lean was to price it into genesis. A fine characterization sweep (hu_cushion_*:age_weight3→8 × three seeds) refuted the lean: the 7th replica is seed-dependent (reached in only two of three worlds at anyage_weight >= 5; seed0x5EED_2222never reaches it even at 8), so it is not a cushion the network can lean on; and the lever is inert on the metrics that matter (committed_deep_underalready0.0000atage_weight = 3;gini/max_actor_shareflat across 3→8, confirmingage_weightredistributes a fixed budget rather than scaling emission). Genesis keepsage_weight = 3,r_target_deep = floor + 1, no cushion — the lever buys no reliable redundancy and costs nothing to leave alone. Replaces the priorhu_lever_*sweep with the finerhu_cushion_*characterization that produced the disposition. Reopen only if a new friction erodes the deep band and a sweep shows the 7th replica robust across seeds at tolerable concentration cost. Cross-ref:FOLLOWUPS.mdmid-band lever item,STAKER_ARCHIVAL_SIM.md§L18 finding 6. -
archival-sim: faithful
HoldingsUpdaterelease-cooldown freeze — pre-charge held deep collateral; close the same-epoch drop-to-reallocate recycle (Copilot PR#148 #4/#5, 2026-06-16,feat/standoff-shared-crate). The L18 freeze model was one epoch too lenient (escrow pushedepochs_remaining = Cbutadvance_epochdecrements before the next best-response, so capital froze forC − 1) and, more materially, letbest_responserecycle a voluntarily-dropped bond within the same epoch — the futile drop-to-reallocate move the cooldown exists to prevent and that finding 3's prose already claimed was precluded.best_responsenow pre-charges the collateral of every deep shard held at epoch start (it seedsused_bond, so a same-epoch drop cannot refund a fresh acquisition); pre-charge (drop epoch) + escrow (nextC − 1) span the fullRELEASE_COOLDOWN_EPOCHS.release_cooldown_epochs == 0pre-charges only locked shards, so everyc0arm is byte-identical to the pre-cooldown baselines. The seal verdict and every shipped genesis parameter are unchanged (RELEASE_COOLDOWN = 2,r_target_deep = floor + 1,age_weight = 3, no foundation widening): both the old and faithful models clear allc2gates, so this strengthens the seal (bindingcommitted_deep_undermoves0.0138 → 0.0000) rather than altering calibration. The model correction reveals thec4"cliff" was an artifact of the spurious churn (gone; floor holds at6), confirms the rationality-preclusion thesis in the budget arithmetic, and surfaces a mid-bandage_weightlever (aw ≥ 7reaches a 7th oldest-band replica) now tracked inFOLLOWUPS.md. Also guardsfreeze_blocks_recoverageagainstbond_rate <= 0(#1) and aligns theoldest_margindoc comments to the implemented>= 0hold-the-floor gate (#2/#3). Docs:STAKER_ARCHIVAL_SIM.md§L18 "Faithful-freeze reconciliation". -
archival-sim: adversarial-dodge arm completes the cooldown's two-leg seal — and inverts the premise on the committed channel (2026-06-16,
feat/standoff-shared-crate). The faithful-freeze fix left the seal on one leg: under the rational cooldown-aware agent the cooldown is inert, so the sim gave no evidence forRELEASE_COOLDOWN_EPOCHSat all. AddedAgentParams.dodge_pref/SimConfig.dodge_prefand ahu_dodge_{nolock,ship}_{lag0,lag2}_{dp0,dp1}_{c0,c2}sweep modeling a non-cooldown-aware operator that wants the drop-and-refund dodge P2B-7 Pin 3 defends against. Result inverts the expectation on the committed channel: atc0the dodge is pure churn (1.364vs rational0.268) with no committed-coverage harm (oMinCmtRholds at6, instant re-seat); atc2the cooldown causes the sweep's only committed breach (oMinCmtR 6 → 4, 37 % capital stranded) by freezing the refunded bond in the lock-off regime. The coverage defense is the L9 retention lock, not the cooldown — everyshiparm (locks on) holdsoMinCmtRat7at bothc0andc2. Corrected mechanism: the cooldown is a cost/deterrent (Pin 3 anti-dodge property, demonstrated), complementary to the retention lock, not a coverage protector; alone (nolock) it backfires. Two-leg seal (sealed against theshipconfig): good-actor leg costless, bad-actor leg bounded-safe (oMinCmtR 7,frzCo 0.107, no breach). New load-bearing operating-envelope constraint: never ship the cooldown without the L9 retention lock (rule-21 reopen keyed onBOND_DURATION_AGE_SCALE/BASE → 0). No shipped parameter moved. Docs:STAKER_ARCHIVAL_SIM.md§L18 "Adversarial-dodge arm". -
standoff: the conformance grader must not certify an empty sample, and the conformance suite now runs in CI (Copilot PR#150, 2026-06-16,
feat/standoff-shared-crate).grade_samplereporteduniform_ok = truefor an empty (or single-point) sample — a zero chi-square clears the critical value, so "no data" read as "passed uniformity," a false positive a wallet self-cert must never accept; it now requiresn >= 2before a uniformity claim.summarize_gapson an empty sample took thedenom = 1.0fallback and manufactured amax_decile_devof0.10(a real-looking failing shape) for no data; it now returns explicit zeros. Both are locked by newempty_sample_summary_does_not_manufacture_a_shape/no_observations_cannot_claim_uniformitytests. Theconformance-gated suite (chi-square / order-balance / serial-independence grader + triangular-trap negative control) was never exercised in CI because the default-feature workspace test does not compile it;build.ymlnow runscargo test -p shekyl-standoff --features conformanceon the x86_64 lane (x86-only by design — float GoF probes are not bit-identical across arches; the cross-arch guarantee remains the integer golden vector under qemu). -
standoff/stats: public conformance helpers are now well-defined for every
u64window and the chi-square critical value fails closed on invaliddf(Copilot PR#150 review 2, 2026-06-16,feat/standoff-shared-crate). Three latent edge-case panics/NaNs on the feature-gated helpers — none reachable from the sim (window is 600,df >= 1), but the helpers are public and must honor the production draw'su64contract.correlated_walkcomputed its modulus aswindow as i64 + 1, which wraps to a non-positive modulus (panic inrem_euclid) forwindow > i64::MAX; it now walks with overflow-safeu64modular add/sub.grade_samplesized its bin count withwindow as usize + 1, which overflows atusize::MAXand truncates a large window on a 32-bit target; it now computes the count inu64.chi_square_upper_critdivided bydfwithout guarding, yielding NaN/inf fordf <= 0; it now returns NaN explicitly, which makes any downstreamstatistic < critcomparison false — the cert fails closed rather than passing on a bogus critical value. Locked byhelpers_are_well_defined_at_extreme_windows(u64::MAXwindow, no panic) andcritical_value_fails_closed_for_invalid_df. The sim model is untouched: the review surfaced no soundness finding, only input-validation hardening on the shared crate's public surface. -
bench: migrate the iai instruction-count harness from
iai-callgrind 0.16togungraun 0.19(2026-06-16,chore/bench-gungraun-migration).iai-callgrindwas renamed togungraunupstream from 0.17.0 and the 0.16.x line no longer receives backports; depending on it is compounding debt. All five bench crates (shekyl-engine-core,-engine-file,-engine-state,-scanner,-tx-builder) moveiai-callgrind = "0.16"→gungraun = "0.19"(latest stable, 0.19.2), the 13_iaibench files swapuse iai_callgrind::*→use gungraun::*(macro API is source-compatible — no structural changes), andbenchmarks.yml+capture_rust_baseline.shinstall/invokegungraun-runner(the_iaifilename suffix and theiai_callgrindJSON envelope section key are retained as stable identifiers consumed bycompare.py). gungraun is a dev-dependency only, so its 0.19 MSRV bump (Rust 1.85.1 / edition 2024) does not raise any production crate's MSRV (our toolchain is 1.94). This re-lands the disposition fromdocs/investigation/2026-05-09-bench-baseline-flake.md§4.1 (Option A), whose original 0.18 attempt was archived unmerged; targeting current-stable 0.19 rather than the stale 0.18 pin. Whether it fixes theinstructions=0capture flake (the producer guard from PR #35 that was hard-failing per-PRcapture-prjobs) is speculative — the cause remains unknown (§3.3); the upgrade is justified on supported-upstream / debt-reduction grounds. -
wallet: CT-5b — curve-tree reference selection + §3.3 ingest verify + C2 spendability gate (2026-06-16,
feat/ct-5b-reference-verify). Builds on CT-5a's actor/ingest wiring (docs/completed/CT5_ENGINE_WIRING.md§6):- §3.3 ingest-time root verify (O5). After each block is ingested into the
curve tree, the reconstructed root must byte-equal the consensus
header-committed root (producer range from
block_curve_tree_roots, backfill from the daemon-fetched block'sheader.curve_tree_root). A mismatch is the inconsistent-liar daemon: a terminalRefreshError::CurveTreeIngest { recoverable_by_respawn: false }, before the ledger advances (O2). New productionVerifyRootactor message. - C2 reference-block spendability gate. Selection now rejects outputs too
fresh for the reference block (
eligible_height > tip − REF_ANCHOR_AGE) with a cleanSendError::OutputNotYetSpendable { eligible_height, reference_block_height, wait_blocks }, and the pre-maturity window withSendError::WalletTooYoungToSpend— distinct from the self-healingSpendUnavailableRebuilding(tree lag) and fromInsufficientFunds. - Real reference binding. At send time the reference root is re-derived
from the tree (
reference_height = tip − REF_ANCHOR_AGE; never persisted — derive>hold) and bound into aReferenceBlock, threaded into assembly. The signer still builds synthetic membership paths (the assembler cutover andassemble_pathare CT-5c); only the reference selection is real. KATs pin the §3.3 mismatch rejection, the C2OutputNotYetSpendablewait signal, the too-young window, and the reference-root threading.
- §3.3 ingest-time root verify (O5). After each block is ingested into the
curve tree, the reconstructed root must byte-equal the consensus
header-committed root (producer range from
-
archival: long-lived
Pis the committed staking-identity architecture — S-5 fork consciously closed (2026-06-16). The last genuinely-structural firewall fork (long-lived vs. short-lived/rotating archival pseudonymP) is closed in favor of long-livedP, ratifying what serve-credit accrual, lock tiers,bond_duration, the L18 freeze-friction seal, and bond-as-consensus-balance already assume — making the decision-by-accumulation explicit rather than latent. Short-lived/rotatingPis out of scope for V3.0 (it would require a bond-migration-between-identities consensus op, reopening the bond FSM and the gate-4 §3.2 custody seal). The surviving privacy question is characterization of the long-lived handle's fusion surface (S-2 exposure ledger + S-3 adversary sim + operator-behavior levers), not a protocol re-fork. Reopen criterion (per21-reversion-clause-discipline.mdc): a measured S-2/S-3 per-Pattribution breach severe enough to reconsider the confidential-staking rejection, via a freshPHASE_2Bmodel-level round. Docs:ARCHIVAL_FIREWALL_GATE6.md(§10 S-5 + revision 2026-06-16). -
archival: GF-1-carve resolved — dedicated bond-spend key authorizes bond debits (2026-06-16,
feat/gf1-dedicated-bond-spend-key). Releasing bonded collateral (bond_debit:Unbond,HoldingsUpdatedrop-shard) is now authorized by a dedicatedbond_spend_pk— a hybridscheme_id = 1(Ed25519 + ML-DSA-65) keypair committed into theArchivalBondRecordatJoinMarketand immutable for the record's life — not the account identity key (P_pubkey/hybrid_sign_pk). This was the last open "carve": gate-4 §3.5 step 5 previously read "Phybrid signatures on vin," which, implemented at face value, would have silently made the account identity key spend the bond — turning its compromise surface from "reveals nothing spendable" into "drains the bond." Naming a domain-separated debit key at source keeps the Round-1 identity-only invariant intact and compromise-isolates bond-spend authority. The key is derived under two new HKDF labels (shekyl-archival-p-bond-spend-{ed25519,ml-dsa-65}-v1), bound into the bond-post sig-preimage atJoinMarket, and added to theARCHIVAL_P_DERIVE_V1KAT obligation. Credit/identity paths (JoinMarket,Rebond,HoldingsUpdateadd-shard) still authorize againstP_pubkey; the account key never authorizes a value-out. The consensus-balance custody model (gate-4 §3.2, sealed Round 1) is unchanged — no key image, no receipt UTXO; the receipt-UTXO alternative was declined with a named reopen criterion. Spec-only (no code yet); pairs with the gate-4 implementation checklist item. Docs:ARCHIVAL_BOND_GATE4.md(§3.4 / §3.4.1 / §3.5 / §4.1 / §8 / revision 2026-06-16),ARCHIVAL_FIREWALL_GATE6.md(§9.3 / §9.4 / §9.6 / §10.11 / §7 / revision 2026-06-16). -
archival:
HoldingsUpdate(partial-unbond/rebond) promoted to genesis scope (2026-06-15). The full bond lifecycle (JoinMarket / Rebond / HoldingsUpdate / Unbond) ships at V3.0, promoted from deferred-V3.1. Bond balance is consensus-state-machine state under the gate-4 conservation law, so adding mid-life shard adjustment post-genesis would be a hard fork; and without it the only way to add or shed a single shard isUnbond+ re-JoinMarket, tearing down a working multi-shard operation (all collateral into release cooldown, all serving interrupted, all serve-credit continuity reset) to swap one slot. The add-shard credit path covers the voluntary holdings-increase/top-up direction. All bond-lifecycle verify/connect logic is Rust-native (shekyl-archival-retention); C++ daemon retains only thin glue + FFI delegation; wallet-side construction is Rust. Two follow-on dependencies tracked, in order: (1) FSM-friction pin inPHASE_2B_FSM_RETOOL.md(partial-unbond action, per-shard release-cooldown on drop, slashable-when boundary) — landed; (2) age-stratified sim bond-mobility reconciliation — landed (STAKER_ARCHIVAL_SIM.md§L18). Docs:ARCHIVAL_BOND_GATE4.md§4.4,ARCHIVAL_FIREWALL_GATE6.md(R2 / revision 2026-06-15),FOLLOWUPS.md(V3.0 lifecycle item). -
archival/sim: L18 —
HoldingsUpdaterelease-cooldown freeze reconciliation; genesis redundancy floor re-derived unchanged (2026-06-16). The staker sim's frictionless-mobility optimism is closed: a new--axis=holdingsupdate_cooldownsweep freezes released collateral for the release cooldown (World.cooling; spendable budget reduced by frozen capital inbest_response). Two source-faithfulness corrections vs.ARCHIVAL_BOND_GATE4.md— the frozen amount is flatARCHIVAL_BOND_FLOOR(§8.1, not the first-draft age-scaledbond_age(s), which over-stated the deep-band freeze exactly where the redundancy margin lives) and the cooldown anchors at per-shard last-served (§4.4, faithful under serve-until-drop). The friction is flat in amount, age-stratified in incidence (bond_duration(age)immobility + thin deep-tail coverage). Result: genesisRELEASE_COOLDOWNpasses all three absolute seal gates (committed_deep_under < 0.10,sole_source = 0lag-free, hold-the-flooroldest_margin ≥ 0) on both duration arms. The binding seal number is the faithful age-scaled arm (committed_deep_under = 0.0138; gate-4 §3.4 makesbond_duration(age)genuinely age-scaled, so the rosier flat0.0064is the optimistic contrast, not the headline). The flat-floor freeze costs~0floor erosion at genesis, sor_target_deeprequires no freeze-driven increase. The+1redundancy is a provisioning property ofr_target_deep(=availability_floor + 1), not an emergent buffer: no market lever buys slack — neither budget (hu_buf_*) nor deep-tail reward premium (hu_lever_*,age_weight3→12) liftsoldest_min_committedabover_target_deep, because the1/Rreward is anti-over-replication by construction; a wider band is provisioning-only (raiser_target_deep, or lean on the foundation backstop), both priced. The "harmless atc2" result is the conjunction (freeze_harm_cobounded ANDoldest_minholds), notcausal = 0: the causal/transient detector reads0in every epoch of every arm includingc4where the floor eroded6→5. The0is precluded under cooldown-aware optimization, not by construction (best_responsebudgets oncapital−Σfrozen, so it never makes the stranding drop-to-reallocate; validated by positive/negative control testsfreeze_predicate_*inmodel.rs) — the state stays reachable by a naive operator, routed to operator-education + a candidate wallet-side conformance guard (V3.1), not the consensus floor. The freeze's real harm mode is structural (caught byoldest_min, invisible to the transient detector). A cliff check confirms genesis cooldown is not on a bifurcation edge in the faithful age-scaled composition (the super-linear2→4degradation is a flat-duration-contrast artifact); the duration-shape × cooldown sign reversal across the sweep is positive evidence the model captures real dynamics. The rule-21 reopen clause is widened to any newly-discovered friction that erodes the deep band (no emergent cushion beneath the provisioned+1). Docs:STAKER_ARCHIVAL_SIM.md§L18,FOLLOWUPS.md(V3.0 lifecycle item, sub-items 2–3). -
wallet/crypto-pq: retire the
enc_labelreal-label gate; prove the indistinguishability invariant (2026-06-15,feat/enc-label-ungate). The cooperative-payment-request gate (operational.cooperative_payment_requests, envSHEKYL_COOPERATIVE_PAYMENT_REQUESTS,payment_request_flag.rs, and thecooperative_enabledparameters threaded throughoutbound_label.rs,attribution.rs,merge.rs, the FFI, andcryptonote_tx_utils.cpp) is deleted. The gate delivered no privacy or consensus value: the universal sentinel-XOR-k_labelmasking is mandatory and active on every output from genesis, so a non-recipient sees uniform-random 8 bytes whether the plaintext is a sentinel or a real label — gated or not. The gate was a test-coverage interlock standing in for an unwritten invariant, now discharged. Real-label population is ungated: send echoes the URIridwhen present; receive always classifies/matches. The indistinguishability invariant is now stated normatively inSUBADDRESS_UNDER_PQC.md§5.7.10 (cross-referenced fromFCMP_PLUS_PLUS.md§4) and proven by the statistical KATreal_label_indistinguishable_from_sentinelinshekyl-crypto-pq/src/label.rs(chi-square uniformity + homogeneity over the realderive_output_secretspath, with a plaintext-on-wire negative control). GUI tooling emittingridURIs is the de-facto feature boundary. Breaking: wallet config files carryingcooperative_payment_requestsno longer deserialize (deny_unknown_fields); acceptable pre-genesis (rm -rf ~/.shekyl). Docs:WALLET_PREFS.md,FOLLOWUPS.md.
Added
-
wallet: engine-path curve-tree root-match against the CT-2 oracle (CT-5a commit 6, 2026-06-15,
feat/ct-5a-curve-tree-actor). Closes the CT-5a root-match DoD through the engine ingest path. Two KATs driveingest_scan_result_into_curve_treewithOwnedTxLeavesparsed from thect2_tier_aoracle and assert the engine-reconstructed root byte-matches the oracle at every height: a forward ingest of themainchain, and areorg_rewindontoreorg_deepexercising engine-path rollback + re-ingest. Root read-back is a#[cfg(test)]-onlyRootAtactor message +CurveTreeHandle::root_at(the production root-read is CT-5b's §3.3 verify, not this slice). Pending-table-equality post-reorg (E6a) is not asserted here — it remains Tier-B-gated (a coinbase-only fixture cannot express a class-(b) pending migration; that assertion lands at CT-5c when the non-coinbase fixture exists). A green commit 6 proves the engine ingest wiring transports leaves to the actor/client correctly; it does not prove non-coinbase backfill. Docs:CT5_ENGINE_WIRING.md(CT-5a DoD commit-6 status). -
wallet: engine-side respawn-on-poison for the curve-tree actor (CT-5a commit 5, 2026-06-15,
feat/ct-5a-curve-tree-actor). Implements the R1-Q4 recovery body: a curve-tree actor that fail-stops (kameoon_panicdoes not restart it, by design) or whose client isClientError::Poisonedis now healed engine-side by a drop-and-reopen respawn rather than failing the refresh permanently.CurveTreeHandle's innerActorRefis wrapped in anArc<Mutex<…>>shared, swappable cell so a respawn propagates the fresh actor to every handle clone at once (including theLocalPendingTxspend-gate clone) — without the shared cell a respawn would heal refresh but leave spends pointing at the dead actor forever (a partial heal). The newCurveTreeHandle::respawndoes kill →wait_for_shutdown→ reopen → spawn → atomic swap; because kameo drops the actor value (and its redb single-writer lock) afterwait_for_shutdownreturns, the reopen is retried on a short poll bounded by a 2 s deadline so the lock-release lag is absorbed but a genuinely corrupt/held store still surfaces its error.RefreshError::CurveTreeIngestgains arecoverable_by_respawn: boolclassification (truefor a fail-stopped actor orClientError::Poisoned;falsefor non-consecutive-height / root-mismatch / store / decode failures a reopen would only reproduce).Engine::ingest_scan_result_with_respawnwraps the bare ingest with a single respawn-and-retry on a recoverable error and is wired into both refresh ingest call sites (asyncrun_refresh_taskand the synchronous scan pass). KATs:respawn_resumes_from_store_and_propagates_to_clones(handle-level: respawn resumes from the persisted cursor and a pre-respawn clone sees the live actor) andingest_pre_pass_respawns_after_actor_fail_stop(engine-path happy path: a fail-stopped actor is classified recoverable, then heals via respawn-and-retry with the cursor resuming from the persisted tip). The bounded-retry-budget-then-escalation arm that distinguishes a transient hiccup from a permanently corrupt store at the refresh-budget level (O3-sub) is staged for CT-5d. Implements CT-5 §3.3 R1-Q4. Docs:CT5_ENGINE_WIRING.md. -
wallet: surface the pending-incoming summary and rebuild status on the refresh progress channel (CT-5a commit 4b-2, 2026-06-15,
feat/ct-5a-curve-tree-actor).RefreshProgress(thewatch-channel refresh signal) gains three fields, no new persisted state:pending_incoming_count/pending_incoming_atomic_units(the per-attempt "you received" summary — the count and summed amount of outputs the scan detected this attempt) andrebuilding_membership(truewhile the curve tree lags the ledger, i.e. the adopting / tree-wiped wallet whose backfill is in flight and whose spends may be temporarily gated by 4b-1'sSendError::SpendUnavailableRebuilding). Detection is decoupled from spendability: a received output is displayed as soon as the scan finds it, independent of whether its curve-tree membership is yet provable. The orchestrator (run_refresh_task) assembles the summary once from the fullScanResultand emits it on the pre-mergeMergingframe (withrebuilding_membershipread from the tree cursor before the ingest pre-pass, so a long adopting backfill surfaces the rebuild status for its whole duration) and on the terminal success frame (rebuilding_membership: false— the pre-pass acked the full range under ack-before-commit, so the tree is caught up). The forward-from-genesis common case never flags rebuilding (tree and ledger advance in lockstep). Per-block scanning frames and the seed/retry/cancel pings carry the zeroed display fields via the newRefreshProgress::phase_onlyconstructor. KATs:membership_rebuilding_predicate(the pure ledger-ahead-of-tree predicate across fresh/adopting/caught-up/ ahead) andhybrid_refresh_from_genesis_surfaces_not_rebuilding(the wiring smoke test). A non-zero pending-incoming amount and therebuilding == truepath need a wallet-addressed (non-coinbase) fixture and a divergent adopting state respectively — both Tier-B-gated to CT-5c. Implements CT-5 §3.2.1 D3 display surface. Docs:CT5_ENGINE_WIRING.md. -
archival: standoff conformance suite gains the two independence probes + discrete-GoF grade (2026-06-13). The marginal uniform/trap tests are structurally blind to the independent half of "uniform-independent draw"; added the two probes the marginal cannot see (
rust/shekyl-staking-sim/src/standoff.rs): (1) population anchor-independence — the catastrophic shared-trigger mode (an epoch-snapped anchor passes every gap test while clustering absolute bond times → candidate set ~1.01);population_bond_time/max_bin_share+population_anchor_independence_disperses_shared_trigger_clusterswith an epoch-snapped negative. (2) serial independence — load-bearing now that rebond/partial-unbond/re-entry make aPdraw several gaps over its life (a weak PRNG yields a uniform marginal with correlated successive draws, linking recurring bond ops);lag1_autocorr+serial_independence_reference_passes_correlated_fails. Also added the grading form: a discrete chi-square against the uniform (chi_square_uniform, not continuous KS — the gap is integer blocks) at a strict grading alpha ~1e-6 (chi_square_upper_crit/Z_ALPHA_1E6;chi_square_grades_uniform_at_strict_alpha), chosen so a correct wallet essentially never false-fails while the trap fails by orders of magnitude; the fixed seed is reference-determinism, not a PRNG mandate. Suite is 49 sim tests green; economic sweep +--standoffreport untouched. Docs:STAKER_ARCHIVAL_SIM.md§Construction and per-seam geometry,FOLLOWUPS.md. -
archival: executable reference for the standoff draw conformance vector (2026-06-13). Added
draw_entry_gap(the conformance-correct direct draw:s ~ U[0,600]+ fair order-coin) andsummarize_gaps(realized(spread, order)distribution: mean spread, order balance, first-decile mass, decile uniformity probe) torust/shekyl-staking-sim/src/standoff.rs, with two validating tests —correct_draw_is_well_distributed(flat spread, balanced order) anddouble_jitter_trap_fails_the_same_check(the same summary rejects the triangular double-jitter trap; notably the trap's order-coin still looks fair, proving the spread distribution is the load-bearing check, not order balance). This makes the published wallet test vector executable rather than prose: the vector asserts on the realized spread and order over a sample, not the ±600 bound alone. No model change to the economic sweep; opt-in--standoffreport untouched. -
archival: standoff construction + per-seam geometry pinned (2026-06-13). Two conformance-critical construction details folded into the funding-seam entry-standoff spec (
STAKER_ARCHIVAL_SIM.md§Funding-seam entry standoff → Construction and per-seam geometry,ARCHIVAL_FIREWALL_GATE6.md§10.12). (1) Draw the gap directly, do not independently jitter two event-times around a common anchor: the difference of two uniforms is triangular/zero-peaked and clusters the events (near-zero effective standoff) while passing the ±600 bound — a conformance trap that matters because the draw is wallet-side and unenforceable. Correct shape: place event 1 at the private intentt0, draws ~ U[0,600], event 2 att0+s, fair order-coin (uniform separation, free inversion, max latency 600, per-Pindependence). The published test vector must reject the triangular construction. (2) The 600 is per-seam: the ±600 symmetric envelope is the entry seam (announce↔bond, inversion-eligible, 1200-block search width); the exit seam (terminal drain + recurring partial-unbond) is a separate, one-sided standoff (no inversion — collateral isn't spendable before the 20_000-block release cooldown) whose latency is measured from cooldown expiry (breaking the deterministic fixed-offset cooldown tell), so symmetric entry/exit = two independent 600-block draws, each free on its own seam. Also recorded: width is the expensive axis (proven rate-driven), so the cheap thin-regime levers are biasing the gap toward the max and the inversion, not a wider window.FOLLOWUPS.mditem extended (test vector must reject double-jitter; exit-seam geometry named). Docs + harness doc-comment; no model change, no re-sim. -
archival: standoff recommendation reframed after review pass — cap is anti-griefing, cover is conditional on isolation (2026-06-13). Folded four review-pass carries into the
--standoffrecommendation (STAKER_ARCHIVAL_SIM.md§Funding-seam entry standoff → Conditionality and caveats,ARCHIVAL_FIREWALL_GATE6.md§10.12), two of which change what the number means: (a) the max announce↔bond cap is an anti-griefing ceiling, not a privacy control — the privacy floor (minimum spread + uniform-independent draw) is wallet-only and consensus-unenforceable, so the draw is promoted to a hard conformance requirement with a published test vector, and a per-block bond-post smoothing rate-limit is opened as the one (partial, non-free) consensus-side surge backstop; (b) the measured cover isP(link | §10.9 isolation holds)— a multiplier on isolation, never additive — and the rate that drives it is the post-isolation network-event rate, not the on-chain funding-spend rate the sim proxied. Carried as named residuals: cold-start weak cover (pre-seal, L12; foundation-decoy injection is self-undercutting per the injected-decoy critique) and nominal-vs-effective cover (testnet must probe the S-3 modeled observer, not a passive honest-rate measurement). NewFOLLOWUPS.mdV3.0 item; harness doc-comments updated so the code stops overclaiming. No re-sim (the reframings are meaning-changes, not value-changes); no consensus surface touched yet (the consensus questions are opened, not decided). -
docs: track five CT deferrals in FOLLOWUPS (CT survey cleanup, 2026-06-13). A CT-* state survey found five pieces of real deferred work recorded only inside design/closeout docs, not in the central
FOLLOWUPS.mdledger. Added a tracked row (target version + reopening trigger per21-reversion-clause-discipline.mdc) for each, with back-references from the source docs so they are bisect-locatable both ways: (1) CT-2 Tier B reconstruct-root KATs (staked/non-coinbase maturity, therecon_tier_b.rs#[ignore]set) — V3.0, after CT-5; (2) CT-1 full-segment freeze/prune KAT atj=2(~26k leaves/segment vs the CI-scalej=0/Tier-A coverage) — V3.0, with prune-policy; (3) wallet-localO.x → positionmatch index (§4.3 scan cost) — V3.x perf; (4) confirm segment layerj/ shard sizeEagainst mainnet leaf growth (§7.2.2, level-2 ≈26k provisional) — V3.x, with theArchivalEngineshard policy; (5) anonymized (Tor/I2P) routing for non-forward segment fetch (§7.4) — V3.0, riding theSegmentSourceseam. Docs-only; no code or consensus surface touched. -
economics: CI gate enforces the pure-integer contract for
reward_arithmetic(2026-06-13).scripts/ci/check_archival_reward_gates.sh(run fromcheck_consensus_invariants.sh) now rejectsf32/f64,usize/isize, and atomics inreward_arithmetic.rs, with an inlinereward-arith-allowescape hatch for reviewed-benign uses. This makes the cross-arch bit-identity guarantee (M-1 half (b)) hold by construction, not convention:#![deny(clippy::float_arithmetic)]only guards float arithmetic, so ausizeleaking into a credited value would silently drop the aarch64 guarantee from "real" to "emulated-and-hoping" on a future 32-bit target, and qemu-user would not flag it. Docs:ARCHIVAL_REWARD_ARITHMETIC.md§Pure-integer contract gate. -
economics: pre-genesis-seal carry for float-calibrated redundancy floors (tail-margin finding, 2026-06-13). PR 1.5 showed the float sim over-reads worst-shard redundancy by up to one replica. Genesis-sealed redundancy params that gate sole-sourcing — the L12
r_target_deepfloor and any sealedR_target/redundancy floor — must be re-derived against the integer backend with a +1 deep-tail replica margin before the seal (a sealed value cannot move post-genesis without a fork; "watch on testnet" only covers the tunable bands). Availability-scoped (Foundation complete-tree B+C seeds are the durability backstop), not a durability escalation. Tracked as a V3.0 pre-genesis FOLLOWUPS item so it cannot slip into genesis at its float value by omission; cross-refs inARCHIVAL_SIM_ECONOMICS_VERDICT.md§tail-margin,REWARD_EMISSION_VIN_PLAN.md§9,STAKER_ARCHIVAL_SIM.md§L12. -
docs: CT-4 Round 1 closed — membership-path assembly + reference-block horizon (
shekyl-curve-tree, 2026-06-13). AddedCT4_ROUND1_CLOSEOUT.mdrecording the two landed CT-4 surfaces: (1)assemble_path → AssembledPath(gated on the §3.3 root match, FCMP++ proverPathlayout pinned at source, C3c1+c2+1 == depthself-checked) and (2)reference.rsvalidity-horizon arithmetic (select_reference_height,proof_submittable/proof_expired,should_reanchor;REF_ANCHOR_AGE/PROOF_VALIDITY_HORIZON/REBUILD_AT). Both landed ahead of their decomposition slot and were hardened by the CT-3↔CT-4 audit (ReferenceBlocknow carriesblock_hash). FlippedCURVE_TREE_CLIENT.md§9 CT-4 row to Round 1 closed. Test gates:assemble_kat(independent root recompute +OutputNotDrained/RootMismatchrejection),reference::tests(selection/window/aging/re-anchor/reorg), and the archival-retention cross-checks. Deferred-with-reversion-clause: F5 store-backed/pruned assembly (rides prune-policy) and the C++hash_to_p3path-RPC FFI migration (rides the daemon path-assembler). Docs-only; no code or consensus surface touched. -
docs: CT-3 Round 1 closed — CT-3d closeout (
shekyl-curve-tree, 2026-06-13). AddedCT3_ROUND1_CLOSEOUT.mdrecording the landed persistent-client lifecycle (CT-3a store schema / CT-3b open+resume+delta-ingest / CT-3c reorg rollback), the DoD mapping, and the deferred-with-reversion-clause surfaces. FlippedCURVE_TREE_CLIENT.md§9 CT-3 row to Round 1 closed and closed §8 #6 (block-derived forward sync is the confirmed default — the §6 reversion criterion fired). MarkedCT3_SYNC.mdCT-3d landed / Round 1 complete. The bulk-leaf RPC endpoint (R1-Q1) andSegmentSourceseam (R1-Q5) remain deferred-with-recorded-shape, landing with the post-prune refetch path; the F5 store-backed assembly, resume-path/fullR_krecheck, and CT-5 poison-reaction items stay routed inFOLLOWUPS.md. Docs-only; no code or consensus surface touched. -
archival: staking-sim
--standoffmode — funding-seam entry-standoff anonymity model (2026-06-13). New self-contained sub-report (rust/shekyl-staking-sim/src/standoff.rs, peer to--timing-cluster/--failure-confirmation) executing the Gate 6 §10.12 pass-4 BUILD target. Monte-Carlo (200k trials/arm, reproducible SplitMix64) over candidate-set size = 1 target + Poisson background funding-shaped decoys, sweeping window width × background rate × inversion × trigger-independence, with an analytical homeostasis bound (entry latency = window/epoch; economic dynamics are epoch-quantized at 10_000 blocks, so a minutes-to-hours window is off the economic axis — not re-simmed, to avoid over-reading the epoch-granularity model). Findings: anonymity is rate-driven, not width-driven (the background funding-spend rate is the load-bearing unmeasured input, swept likefetch_latency_per_unit); the inversion carries the low-activity worst case (link 0.52→0.32, thin-cover 56%→20% where set-enlargement can't help); a shared trigger is catastrophic (candidate set 16→1.01 — uniform-independent draws mandatory); the standoff is homeostasis-free ≤ ~1000 blocks. Recommended: 600-block (~20 h) uniform-independent window, inversion on; testnet must measure the background rate. Recorded inSTAKER_ARCHIVAL_SIM.md§Funding-seam entry standoff andARCHIVAL_FIREWALL_GATE6.md§10.12. New code- docs; 5 unit tests; existing 325-scenario economic sweep untouched (new mode
is opt-in via
--standoff).
- docs; 5 unit tests; existing 325-scenario economic sweep untouched (new mode
is opt-in via
-
archival: Gate 6 Round 2 adversarial pass 4 — cadence closed, standoff promoted (2026-06-13). Added the organizing principle behind the close: the firewall protects P ↔ principal (a linkage), not P ↔ its own rewards (a conceded-public function of contribution). Earning-function obfuscation (reward-magnitude banding — distinct from the sealed
goperating-band inREWARD_EMISSION_LEG.md— and pay-every-block) spends the anti-whale/trilemma budget twice on a conceded property; the seam mitigation (standoff + inversion) touches no economic quantity and is off the trilemma's axis. Cross-Psybil correlation resolves to seam protection too (signature is timing/pattern, not magnitude), and FCMP++/RingCT already hides seam amounts — leaving timing + origin as the public seam leak. Resolved the two live pass-3 §10.12 scenarios inARCHIVAL_FIREWALL_GATE6.md. CLOSED the pay-every-block / implicit accumulator-emission path and corrected the pass-3 over-claim that accumulator credit "deletes GF-6/GF-10." Reasoning: Shekyl staker payments are a roll-call (every accrual keyed toP_canonical_idbecause dedup demands it — confidential staking was rejected), so continuous attributed payment makes an absence a one-block edge at the GF-4 exit rather than crowd-cover; and consensus cannot mint a hidden-recipient output (it holds no ephemeral secret — the miner coinbase is private only as sender-to-self), so a per-block reward is either publicly-derivable (everyP's reward stream traceable toP_canonical_id) or an invisible accrual that, auto-compounded to exit, concentrates into the terminal event (worse GF-4) and destroys the claim-timing decorrelation lever. The periodic-claim model is the better spot given public attribution (claims timing-decorrelatable; decoupled from unstake per R0-D6). The obfuscation being reached for needs membership-hiding claims = the confidential-staking machinery already rejected, so this is the S-4 bridge cost in disguise; the real question (reopen confidential staking?) folds into S-5. PROMOTED the funding-seam entry standoff + inversion to a build target for the S-3 adversary sim (the door, GF-7), sharpened past "smooth vs surge": candidate-set sizing for the targeted principal (not window width); the low-activity principal as the worst case (set-enlargement can't help, only the inversion does); trigger-independence as the clustering knob; and enumerate the separable funding events first (prep-spend vs. announce vs. bond-post tx — ties to the S-2 ledger). Quick dispositions: terminal-lump → existing wallet-side GF-4 fragmentation (no consensus mechanism); per-block accrual rejected (more state churn + reorg surface for the same exposure); seed compromise = named residual (recoverability ⟺ single-point-of-failure), weight on key hygiene. Docs-only. -
archival: Gate 6 Round 2 adversarial pass 3 — end-to-end trace + cross-layer findings (2026-06-13). An end-to-end
P-lifecycle trace (key-gen → announce → bond-post → serving → emission → bond-adjustment → exit) surfaced model-level findings the per-round decomposition is blind to by construction; landed as a new §10.12 (S-1…S-6) inARCHIVAL_FIREWALL_GATE6.md. S-1: the money seams (GF-7 funding-in, GF-4 value-out) are the only findings protecting principal↔Punlinkability and the only two still deferred — they sit at the on-chain ∧ network ∧ timing conjunction no single layer owns (deferral ≠ ownership). S-2 (build first, cheapest): specified the missing fused, per-observer, whole-life exposure ledger (observer × lifecycle-event matrix + per-observer fusion closure, conceded cells marked distinctly from leaked) — the §10.1 table is only per-adversary "must not learn." S-3: privacy is named, not measured (325 economic scenarios, 0 adversary); reframed as the privacy axis of the R-3 reconciliation already gating the seal — deliverable is an adversary sim on the funding/exit timing seams (P(link | T_obs)vs standoff / jitter / batch). S-4: labeled the onion↔P_canonical_idbridge a conceded staking-model cost traceable to the confidential-staking rejection, not closable at the network layer. S-5: longevity-vs-privacy named a model-level pre-seal question — long-livedPis the worst bridge structure, but rotation relocates the seam (fresh GF-7 per rotation) and reopens the bond FSM (bond = consensus balance), so the answer is open. S-6: pinned key locality — the always-on serving box holds only theP-subtree, never the master seed (cold-derive + provision; HKDF one-wayness preserves recoverability), extending the §10.9 isolation pin from circuits to keys. Recorded three sim scenarios: variable announce↔funding standoff; randomized 0–9-block entry incl.P-before-bond; and implicit accumulator emission (deterministic Σwork credit with no broadcast claim deletes GF-6/GF-10 by folding them into GF-4 — vs. literal per-block pay, which sharpens the unbond cessation edge). Prioritization: ledger → adversary sim → longevity question, all pre-seal. Docs-only; the ledger/sim/model calls live in their home docs (STAKER_ARCHIVAL_SIM,REWARD_EMISSION_LEG, staking model). -
archival: Gate 6 Round 2 adversarial pass 2 — GF-1-carve asymmetry + age-stratified R-3 (2026-06-13). Two sharpenings on the pass-1 landings in
ARCHIVAL_FIREWALL_GATE6.md. GF-1-carve is not a balanced fork — the carve is the default trajectory by omission: gate-4 §3.5 step 5's existing wording ("Phybrid signatures on vin") already presumes the accounthybrid_sign_pk, written before the Round-1 identity-only invariant existed, so the carve happens silently unless explicitly chosen against (the "absence of the claim is a claim of absence" trap). Reframed the §9.6 collateral-out cell + note: collateral-out is the onlyPpath with neither a key image nor a bindable leaf (the bond is a consensus balance), which is exactly why the account key tempts; carving trades the cleanest Round-1 invariant (account key = identifier, compromise reveals nothing spendable) for "compromise drains the bond." Recommended disposition: a dedicated bond-spend key committed in the record (own HKDF label + KAT, domain-separated from identity; non-replay frombond_debit ≤ bonded_total+ tx-height/input binding, no key image needed). Recorded a receipt-UTXO custody direction (non-transferable receipt spent on the standard per-output + key-image path — replay-free, partial-unbond = receipt split, value-leg byte-indistinguishable) as a gate-4 custody-model call, not a prescription. Concrete action pinned: re-word gate-4 §3.5 step 5 to name the authorizing key and resolve at gate-4 source before the bond-post verifier lands (consensus rule — wrong-once = fork-to-fix). R-3 reconciliation must be age-stratified, not a re-tuned flat scalar: the sim's "flat seating cost" stands in for a friction (cooldown, partial-slash, lockup,bond_duration(age)) that is worst on the deep tail, so recalibrating it to a network average stays structurally optimistic on the binding constraint — the pre-seal requirement is "model friction age-stratified," else a re-tuned flat cost looks reconciled while shipping an optimistic floor over the +1 margin it protects. Added a critical-path-out-of-Round-2 note (§10.11): the GF-1-carve wording fix + the FSM→age-stratified-reconciliation→seal chain are the real path; transport tuning defers to testnet replay. Docs-only. -
archival: Gate 6 Round 2 adversarial pass 1 — fork dispositions + rebond/unbond scope (2026-06-13). Worked
ARCHIVAL_FIREWALL_GATE6.md§10 with the reviewer, verifying the load-bearing claims at source before landing (Round-1 verify-don't-infer discipline). Rebond/unbond at genesis (R-1/R-2/R-3): confirmed bond is consensus-tracked balance (gate-4 conservation law), so completing the FSM post-genesis is a hard fork. Source-reframe — the FSM already carriesJoinMarket/Rebond/fullUnbond+ cooldown (=2 < W) +bond_duration(age); voluntary partial-unbond isHoldingsUpdate(post_kind=3), specified but flagged "V3.1 wire" (ARCHIVAL_BOND_GATE4.md§3.2/§4.4), so R-1 = a V3.1→genesis promotion (+ top-up wire), a gate-4/FSM-retool call. §9.6 bond-post row split collateral-in / collateral-out with the GF-1-carve open question named (bond-debit vin auth key — account identity vs per-output); §6 R4 re-scoped to recurring GF-4/GF-7, GF-10 extended to bond ops. R-3 verified: the sim explicitly abstracts release cooldown / partial slash / capital-lockup to "a flat seating cost" (STAKER_ARCHIVAL_SIM.md§steady-state #6) and the genesis-seal carry is integer + +1 deep-tail margin (ARCHIVAL_SIM_ECONOMICS_VERDICT.md); sincebond_durationpeaks on the deep tail where the margin lives, the FSM-friction reconciliation compounds the tail-margin finding — the rebond/unbond FSM is a pre-genesis-seal dependency for the R-3 sim reconciliation. §10 forks: §10.4 bonded-verifier-only + restricted-discovery (L14 oversight volume is challenge≡retrieval / population-independent — the real check is challenger liveness vs L14bm-of-n); §10.5 announce → broadcast (event-sensitivity + R3 sequencing hazard); §10.6 own-node/Dandelion secondary (GF-6 shape orthogonal); §10.7 crypto over-coupling foreclosed (HKDF one-wayness), real risk is restore co-activation. New §10.9P↔principal client/circuit isolation exit pin (forks 1+4: independent Arti clients/guards + restore-flow discipline —StakeEnginemust not co-launchP's HS with principal sync). Pure-rendezvous genesis lean (§10.8); I2P-closed-at-genesis lean (§10.11). Docs-only; the gate-4/FSM promotion + the GF-1-carve resolution are cross-doc calls, not executed here. -
archival: Gate 6 Round 2 draft opened — network + transport layer (2026-06-13). Drafted
ARCHIVAL_FIREWALL_GATE6.md§10 as the opening position for the Round 2 adversarial pass (OPEN, not closed). The round's bar is defense-in-depth (named fingerprint + measurable mitigation + honest residual), explicitly not algebraic separation — the network layer cannot rest on a proof the way the crypto layer (§9) does. Threat model: live network observer (ISP, malicious peer, HS-side, partial passive); property is thatP's serving/challenge/broadcast traffic never links to the principal's clearnet identity or wallet traffic. Two traffic classes: light/privacy- critical over Tor, heavy/archival serving over onion-rendezvous (no clearnet fallback, worst-case L-regime by construction per L16). The five R1-named entry gates were threaded into the draft: GF-12 — Arti service-side onion-service hosting confirmed (done/stable since Arti 1.2.0, 2024-03;onion-service-servicefeature onarti-client0.43.0; web-verified per dependency-discipline, with the at-source pin carried to the transport PR), embed-Arti-vs-external fork decidable on that pin; GF-3 — challenge-response + liveness-re-proof Levin class added to the anonymity-routable set,Prefuses clearnet-arriving challenges (loud, no fallback); GF-5 — pre-join backing presentation pinned to a fresh anonymity circuit with no principal stream reuse (it isP's first network appearance), announce↔anchor timing handed to Round 3; GF-6 —P-tx broadcast-origin fingerprint characterization obligation (cell/fragment granularity) + dummy/fragmentation policy shape, with the explicit distinction that on-chainP-typing is public-by-function and not the concern, tuned ratio carried to testnet replay; GF-9 — HS identity keyp_slot-bound + seed-derived via a new §9.3 HKDF label (launch_onion_service_with_hsid), making.onionrotation structural and seed-recoverable, serving-side key-compromise residual named (and the new label must enter theARCHIVAL_P_DERIVE_V1KAT). Added the single heavy-path relaxation lever that feeds the sim (§10.8, privacy > bandwidth) and ten open questions for the pass (§10.10). Docs:ARCHIVAL_FIREWALL_GATE6.md(§§1-status, 2.2, 6, 10 [new], 11 [renumbered], revision history). -
archival: Gate 6 Round 1 closed —
Plifecycle + pseudonym hygiene (2026-06-13). Adversarial-pass disposition onARCHIVAL_FIREWALL_GATE6.md§9. GF-1 (critical): rewrote §9.6 with a per-transaction-type verifier contract — the account-levelhybrid_sign_pkis the bond-record identity only (the on-wireP_pubkeyfeedingP_canonical_id) and is never a per-inputPqcAuthentication.hybrid_public_key; emission backing inputs authenticate against the leaf-committed per-outputpqc_pkthe membership proof binds in-circuit (FCMP_MEMBERSHIP_ONLY.md§7, no key image), fee inputs / ordinary transfers / terminal drains use per-output keys, and ordinaryPtransfers carry noP-typing (byte-identical to principal transfers — the firewall property, not a gap). This aligns the doc to the already-implementedFcmpMembershipOnlycontract; no consensus-rule change. GF-2 (high): made the dual-scan firewall architectural rather than a naming convention —StakeEngineownsP.view_skas an identification context disjoint from the principalLedgerEnginescan, separation rests on distinctcombined_ss/decap material (shared output-derive labels are safe because the discriminator is the decap layer), a shared scan loop is allowed only with match-routing and no cross-assignment, and the cross-pipeline non-cross-assignment negative test is named; addedP-scan ownership rows to the §5 consumer map. Corrections: GF-8 (the §9.3L = …placeholder was cosmetic — per-rowLis pinned64/64/64/32, matchingaccount.rsandderivation.rs), GF-11 (MAX_CLAIM_AGE_W = 26 > MAX_SETTLEMENT_EPOCHS_PER_ EMISSION = 15already pinned inARCHIVAL_TIMING_CONSTANTS.md§1 — added the cross-ref, no new pin), GF-4 (drain delay floor already pinned; the terminal-drain output-count discipline is the remaining Round 4 hard exit). Remaining findings folded into the §6 round table as named criteria: GF-3 / GF-5 / GF-6 / GF-9 / GF-12 as Round 2 entry gates, GF-10 as a Round 3 exit, GF-4 / GF-7 as Round 4 hard exits. Reviewer sign-off recorded (§9.8). Three post-sign-off review refinements folded in: C-1 — the emission backing-input quantum spend-authority binding is now a named carried dependency (§9.8), verified at source (FCMP_MEMBERSHIP_ONLY.md§7/§8.2/§9: the membership proof and ML-DSA check bind the same proven leaf at the same input index via the in-circuitH(pqc_pk)extra scalar — implemented; the vin-layer ML-DSA equality check is a not-yet-landed hard merge blocker that must precede thearchival_pimpl + emission verifier); C-2 — re-anchored the GF-2 ownership boundary, since PHASE_2B §4.6 is claim-era/pending-retool:StakeEnginesole ownership ofP.view_skis now a Gate-6 forward requirement on the FSM retool (the §2.1 dual-scan pin is authoritative; the crypto basis is actor-independent); C-3 — the cross-pipeline negative test now asserts a loud-fail defensive invariant (double-match unreachable by construction). A second-order C-1 confirmation followed: verified at source (FCMP_MEMBERSHIP_ONLY.md§5.1) that theMembershipSpendAuthR_Oleg proves classical knowledge of the leaf's spend secret (ownership) — "membership-only" omits the key image, not the authority — so the interim "classical security only" characterization is accurate (PQ-weak, not authority-free); and the C-1 dependency is now enforced by a failing test (the §7pqc_pk-mismatch forgery negative rejects until the vin-layer ML-DSA equality check lands), with a stressnet negative-case obligation. TheARCHIVAL_P_DERIVE_V1KAT manifest +shekyl-crypto-pq::archival_pimplementation and the C-1 confirm-at-source dependency are the Round-1 carries. Docs:ARCHIVAL_FIREWALL_GATE6.md(§§1-status, 2.3, 2.4, 5, 6, 7, 9.3, 9.4, 9.6, 9.8, revision history). -
docs: reward-emission vin implementation plan (
REWARD_EMISSION_VIN_PLAN.md, 2026-06-13). Sub-PR decomposition (PR-E0…E5) for theREWARD_EMISSION_LEG.md§12 emission leg, downstream of the closed consensus spec. Round-0 pre-flight substrate audit recorded against actual code; ML-DSA backing-auth hard merge gate bound to PR-E3;txin_stake_claim/C_stakedeletion surface enumerated;07-consensus-atomic-cutovers.mdcevaluated (exception does not apply, standard splitting governs). Architecture is Rust-first: the new emission consensus logic (untrusted-input parse, amount arithmetic, membership + ML-DSA crypto) lives in Rust behindshekyl_emission_vin_verifyjoining theshekyl_archival_*/shekyl_fcmp_*FFI family; C++ keeps only thetxin_vvariant + epee/boost/JSON transport shim, pushing the FFI boundary forward for the Stage-5 cutover (10-shekyl-first.mdc,20-rust-vs-cpp-policy.mdc). No production code lands against the plan. Cross-refs:REWARD_EMISSION_LEG.md§13,FCMP_MEMBERSHIP_ONLY.md§9.
Fixed
-
oxide: transaction wire version corrected from 2 to 3 (2026-06-14).
shekyl-oxide'sTransactionhardcoded wire version 2, but Shekyl's sole admissible transaction version is 3 (CURRENT_TRANSACTION_VERSION,src/cryptonote_config.h; the daemon rejectstx.version < 3). The Rust reader gated onversion != 2and so rejected every real Shekyl transaction — including the genesis coinbase — a latent correctness bug on the RPC sync path that was masked only by the absence of a live v3 chain. Fixed the four version scalars (Transaction::readgate,writeemit,version(), and theTransactionPrefix::hashpreimage — the last keeps the tx hash in sync with the daemon'sget_transaction_prefix_hash) and renamed the single enum variantTransaction::V2→Transaction::V3so the type reflects the one admissible wire version. The scanner's coinbasetx.version() != 2guard was corrected to!= 3in lockstep. New cross-language KAT (rust/shekyl-oxide/shekyl-oxide/src/tests/transaction.rsreal_mainnet_genesis_tx_parses_as_v3) parses the verbatim mainnetGENESIS_TXand asserts version 3 + coinbase shape; the synthetic round-trip test is nowv3_coinbase_round_trip. Prerequisite for the CT-5a genesis-seed facility. No consensus or wire-format change — the wire was always v3; only the Rust constant was wrong. -
archival: standoff harness review-response fixes (PR137, 2026-06-14). Resolved Copilot review findings on
rust/shekyl-staking-sim/src/standoff.rsand the standoff docs: (1)run()guarded withassert!(cfg.trials > 0)(an emptysetsindexedp05/ divided by zero) and dropped to private (no caller outside the module); (2) both block→minute conversions centralized in ablocks_to_minuteshelper that casts tof64before multiplying and maps theu64::MAX"no finite window" sentinel (returned byrecommended_window_blocksforrate <= 0) to infinity, removing au64overflow; (3)inversion_prior_breakcomputed from the discrete gap model0.5·W/(W+1)instead of a constant0.5— accurate for the integer gap and correctly0atW=0(events coincide, no inversion possible); the--standoffreport's inversion arm now reads0.49834(was0.5), all other metrics byte-identical. Also fixed a doc inconsistency inSTAKER_ARCHIVAL_SIM.mdandFOLLOWUPS.md(the spread test was described as continuous "KS against uniform" in one place and the correct discrete chi-square GoF in another — unified on the discrete chi-square the implementation uses). Follow-up review round: (4)search_width_blocksnow reports the width that actually feeds the decoy model — the surge cluster width under a shared trigger, not the inversion-adjusted window — so the shared-trigger arms read3.0instead of the misleading window value (anon_setunchanged; the model already used the cluster internally); (5)recommendation()'sentry_latency_frac_epochmaps theu64::MAX"no finite window" sentinel to∞(was a huge finite number), keepinghomeostasis_freemeaningful and consistent withrecommended_window_minutes. fmt + clippy clean; 49 sim tests green. -
oxide: deserialize the consensus block-header
curve_tree_root(CT-5 pre-0, block-deserializer correctness fix surfaced by CT-5, 2026-06-14). The Rustshekyl-oxideBlockHeaderparsed only five fields and stopped atnonce(block.rs), while the consensus C++block_headerserializes a sixth field — the FCMP++curve_tree_root— afternonce(src/cryptonote_basic/cryptonote_basic.h,FIELD(curve_tree_root)). On a real six-field block this left 32 bytes unconsumed andBlock::readmis-aligned the following miner transaction (parsing the root's first byte as the tx-version varint →version != 2reject), soBlock::readwould fail loudly on any real Shekyl block. Severity verified at source as "not-yet-exercised": no real consensus block had ever been deserialized by the Rust path (tests fed synthetic five-field blocks); there was no compensating offset (the only block-hash special-case is the unrelated inherited202612Monero test-vector remap), andserialize_pow_hashhashed the five-field header so the Rust block hash could not match consensus. This fix addscurve_tree_root: [u8; 32]toBlockHeaderand reads/writes it unconditionally afternonce— never gated onhardfork_version, because the field is present from genesis (Shekyl minimum hard fork is 1) and a version gate would be a dead pre-genesis branch (60-no-monero-legacy.mdc). It also correctsBlock::hash()(the preimage now includes the root) and thusReferenceBlock.block_hash, which CT-5 depends on. KATs: header round-trip preserving the root, a consensus field-layout cross-check (six-field byte order matching the C++BEGIN_SERIALIZE), and a full-block round-trip proving the miner transaction no longer mis-aligns. Establishes block-header/block deserialization correctness for the first time; lands ahead of CT-5a perCT5_ENGINE_WIRING.md§3.6 / §6 (R1-Q6, E3). -
docs: correct stale
REBUILD_ATdefault inCURVE_TREE_CLIENT.md§8 #3 (2026-06-13). §8 open-question #3 readMAX_AGE/2 = 47as a "first cut", but the landedreference.rsdefinesREBUILD_AT = FCMP_REFERENCE_BLOCK_MAX_AGE / 2 = 50(MAX_AGE = 100), pinned by a const-eval assert andreference::tests— and §5/§5.2 already say 50. The47was stale arithmetic. Corrected to 50 and marked the open question CLOSED (value landed + pinned), with the reopening criterion retained (measured propagation/confirmation latency). Docs-only. -
archival: bond load-modify-store no longer wipes the v4 emission-dedup fields (F-S1 / F-E5, PR-E0, 2026-06-13).
put_archival_bond_recordreconstructs a freshArchivalBondValuefrom scalar args and cannot carry the v4claimed_settlement_epochsset orfirst_paying_emission_height. The slash apply/revert paths loaded the full bond, mutated it, then wrote back through that reconstructing writer — silently dropping both v4 fields on every slash and every reorg revert. Added a full-bond writerBlockchainDB::put_archival_bond_value(p_id, bond)(LMDB impl + no-op base + testdb stub) that serializes the entire decoded record;put_archival_bond_recordnow delegates to it (its only caller is JoinMarket connect, a fresh-P create where the scalar path is correct).apply_archival_slash_oneandrevert_archival_slashes_at_heightnow write the mutated bond through the full-bond writer, so the dedup state survives a slash and reverts cleanly withpop_block. Added a public readerget_archival_bond_valueand three regression tests inarchival_substrate_lmdb.cpp(bond_v4_fields_survive_full_writer,bond_v4_fields_survive_load_modify_store,bond_v4_claimed_set_survives_reorg_revert). Theclaimed_epochs_check_and_setC++ FFI stays deferred to its first consumer (the emission write path, PR-E3) per spec §6.3 and15-deletion-and-debt.mdc. Also closed a sibling encode/decode asymmetry in the same write path:ArchivalBondValue::encode()serialized anyholdings_kindbyte whiledecode()rejects unknown values, so a writer could persist a record no read path can decode.encode()now rejects unknownholdings_kindalongside its existing at-rest invariants (bounds, claimed-epoch well-formedness), which protects every writer through the single serialization funnel; regression testbond_encode_rejects_unknown_holdings_kind. Becauseapply_archival_slash_onewas moved to public scope for the slash regression tests, it now carries the samecheck_open()+ active-write-txn precondition the internal scheduler enforces, so direct invocation without a write txn fails loudly instead of dereferencing a null*m_write_txn; regression testapply_slash_requires_active_write_txn. This is a latent-bug precursor the emission vin (PR-E3) depends on; no emission behavior changes. Docs:REWARD_EMISSION_VIN_PLAN.md§1.5 / §3 PR-E0.
Changed
-
economics:
shekyl-staking-simdefault backend is now the authoritative integerCurve(2026-06-13).--curve-impl=integer(imports the canonicalshekyl-archival-retention::reward_arithmetic, the code the emission vin mints with) is the default for all sweeps;--curve-impl=floatis opt-in exploration only and is no longer authoritative — PR 1.5 (ARCHIVAL_SIM_ECONOMICS_VERDICT.md) showed the float backend over-reads worst-shard redundancy by up to one replica (the tail-margin finding). An unrecognized--curve-implvalue now fails loudly (exit 2) instead of silently falling back to float, and every run prints the active backend (with a "NOT authoritative" tag for float). TheCurveImpltype-level default and the scenario base config flip toIntegeraccordingly; the float↔integer reconciliation tests remain as drift guards. Closes the M-1 follow-on "stop treating float as a co-authoritative validation substrate." -
docs: emission-vin plan readiness review — M-2 promoted to supply keystone (
REWARD_EMISSION_VIN_PLAN.md, 2026-06-13). Round-1.5 readiness pass: substrate frozen, PR-E0 ready to branch now (zero open deps), PR-E1/E2/E3 gated on a named §8 gating cluster. M-2 (numerator as-of-E sourcing) is promoted from a Q10 parenthetical to its own pinned disposition as the supply-conservation keystone and a PR-E3 hard blocker: the accumulator schema was pulled and confirmed at source (ARCHIVAL_CONSENSUS_STATE.md§3.5 persists aggregateΣwork(E)+ per-shardR_market(s,E), not per-Pcapped_P(E)), so the vin reconstructs the numerator and the supply property rests on as-of-E frozen inputs; three are pinned, the fourth (held(P,E)) is Q10/F-E6, so M-2 closes with Q10 and is resolved jointly with Q7 (the snapshot ABI = M-2's frozen-input set). Q9/F-E3 (intra-block dedup) flagged as the second hard blocker; Q1 gates PR-E2's wire; gating-cluster table + round-2 leverage order added to §8. -
fcmp:
FcmpMembershipOnly— spend authority + tree membership, no key image (2026-06-12). Implements theREWARD_EMISSION_LEG.md§7.2 verify API (the §12 sibling-API gap) as a sibling type toFcmpPlusPlusin theshekyl-oxideFCMP++ crate; first consumer is the emission vin.MembershipSpendAuthkeeps SAL'sR_Oleg alone (96 B/input — the self-contained(x, y~)opening proof over(G, T)); the BP+/P'/Lkey-image machinery is not carried; theFcmpmembership leg, including theH(pqc_pk)extra-leaf-scalar binding, is unchanged. Both SAL-family challenge transcripts now open with fixed-width 64-byte zero-padded type tags (the full spend path's transcript changed too — consensus transcript change, migration-free pre-genesis, guarded by a named roundtrip regression), with length-regularity of both preimages verified field-by-field as the invariant the tags rest on. Per-input challenges bind the tx hash and the input index (u32LE — forecloses cross-slot transplant); empty input sets reject rather than vacuously verify. All per-proof scalars are synthesized RFC-6979-style (a degraded RNG cannot zero or reuse them) with a loud degenerate-rerandomization guard as backstop, sinceO~freshness is the entire non-linkability property for an input type publicly tagged as archival activity. Measured strictly smaller than the 1-inputFcmpPlusPlusorder it was provisionally sized at (3 104 B vs 3 392 B at 1 layer), closing the §10.1 caveat's size arm. 18 tests: roundtrips, cross-type rejection at the deserialization seam (both directions), mixed-type batch rejection (both polarities), freshness/ZeroRng/guard, index-binding, blob-swap, replay, wrong-root, tamper, sizing. Two named gates carried: external review of the soundness reduction (FCMP_MEMBERSHIP_ONLY.md§5.5) pre-genesis, and the ML-DSA backing-auth hard merge gate on the emission vin PR — the membership-only proof is classically secure only; quantum spend authority lives at the vin (FCMP_MEMBERSHIP_ONLY.md§7). Docs:FCMP_MEMBERSHIP_ONLY.md(new), emission-leg §7.2/§10.1/§12 + layer table. -
archival:
ArchivalBondValuev4 — inlineClaimedEpochSet+first_paying_emission_height(2026-06-12). Implements the schema half of the Stage-3 gate perREWARD_EMISSION_LEG.md§6.2/§6.3 (encoding pinned 2026-06-11; P2B-3). The LMDB codec (blockchain_db/shekyl_types.h) appends the windowed claimed-epoch set (u32count +u64BE entries) and the set-oncefirst_paying_emission_height(sentinel0= unset, unreachable — no emission pays before the first settlement epoch closes); v3 is decode-rejected per the pre-genesis posture. Decode invariants: capW + 6(= 32, §6.3 pin), strict monotone order, span ≤W; the cap derives frommax_claim_age_winconfig/consensus_constants.json, newly wired into the C++ generator (SHEKYL_ARCHIVAL_MAX_CLAIM_AGE_W). The dedup semantics live in Rust only (shekyl-archival-retention::claimed_epochs), per the C++-deletion direction and thegood_throughprecedent: window maintenance is part ofcheck_and_set(prunes entries belowcurrent − Won insert), closing the review round's liveness finding — insert-only semantics would have a continuously-claiming honestPoverflow the cap at ~epoch 33. FFI surface deferred to its first caller (emission vin). Also lands the §4.5 / consensus-state §4-invariant-2 joint finalization-boundary pin (Σwork(E)materializes at the boundary block(E+1)·SEBconnect; citing emissions valid strictly above it; reorg-safe by pop ordering). Docs:LMDB_SCHEMA.mdv4 layout,ARCHIVAL_CONSENSUS_STATE.md§8/§9.1 schema-implemented status, emission-leg §1.1/§6.2/§6.3/§12,PHASE_2B_FSM_RETOOL.mdP2B-3 landed,PHASE_2B_STAKE_LIFECYCLE.md§2.4 gate line (Stage 3 now gates on gate-6 soundness only),FcmpMembershipOnlyproof-type domain-separation requirement made load-bearing (§7.2).
Changed
-
crypto: CT-3↔CT-4 audit cleanup — fold the reference-block hash into
ReferenceBlock; doc-code resync (shekyl-curve-tree, 2026-06-13).CurveTreeClient::assemble_pathno longer takes a loose positionalreference_block_hash: [u8; 32]; the hash is nowReferenceBlock.block_hash, so the caller supplies the full consensus anchor (height,curve_tree_root,block_hash) as one value. This removes a footgun — output keys, commitments, roots, and block hashes are all[u8; 32], and a positional arg invited a silent swap — and brings the API to the two-arg shapeCURVE_TREE_CLIENT.md§5 already documents (the third arg was residue of the now-landed §5 horizon work). All call sites (assemble_kat, archival-retention crosscheck/serve KATs) andReferenceBlockliterals updated; no production caller existed yet (CT-5 engine wiring is unbuilt). Docs resync: theassemble.rs"horizon deferred" comment was stale (reference.rslandedselect_reference_heightet al. in CT-4) and is rewritten;CURVE_TREE_CLIENT.md§9 CT-4 row flipped from aselect_reference_block/clientstub to Landed with the realassemble+referencemodules and symbols; the non-existentselect_reference_blocksymbol corrected toselect_reference_height/should_reanchorinFOLLOWUPS.md,PHASE_2A_SEND_PATH.md, andSHEKYLD_PREREQUISITES.md. FFI audit: recorded (not fixed — it is a planning-activity daemon change per20-rust-vs-cpp-policy.mdc/15-deletion-and-debt.mdc) thaton_get_curve_tree_pathrecomputes the key-image generatorI = Hp(O)via C++hash_to_p3instead of the existingshekyl_compute_output_key_imageFFI, folding into the daemon path-assembler migration. No consensus surface touched. -
fcmp: typed the FCMP++ composition layer to make the verify input-context misalignment unrepresentable (2026-06-12). The Copilot rounds on the membership-only PR surfaced a recurring shape in
shekyl-oxide's FCMP++ crate rather than one bug:proof_sizehard-coded the per-leg byte counts, andFcmpPlusPlus::verifytook the key images andH(pqc_pk)leaf scalars as two parallelVecs whose lengths each needed a guard (one of which misrouted a PQC-count mismatch to the key-image error variant, fixed in PR #129 round 3). This change addresses the cause: per-leg wire sizes are now owned by their types (Input::PARTIAL_WIRE_SIZE,SpendAuthAndLinkability::WIRE_SIZE,MembershipSpendAuth::WIRE_SIZE), withproof_sizecomposed from them and transitively asserted against the serialized length by the existing roundtrip tests; andFcmpPlusPlus::verifytakes a single orderedVec<InputVerification>bundling each input's key image with its leaf scalar, so a length mismatch or cross-input transposition is no longer representable as two independently-sized collections. The two count-mismatch error variants (InvalidKeyImageQuantity,InvalidPqcPkHashQuantity) collapse into oneInvalidInputCount; the wrapper inshekyl-fcmpalready does its own granular length checks before calling the oxide layer, so no downstream granularity is lost. The membership-only verify keeps its singlepqc_pk_hashesVec— it has no second per-input collection by construction, so the bundle is not warranted there (reversion clause: introduce it if a second collection is ever added). No consensus behavior changes; all 18 fcmp++ and 66shekyl-fcmptests pass.
Fixed
- sim: sole-source window tick moved to end-of-epoch (PR #126 Copilot
round 2). The swan-4 tick ran after the mid-epoch wipe-out scan but
before
process_exitsand its post-exit re-scan, so a window opened by an epoch's voluntary exits started counting one epoch late and a window the next epoch's re-acquisition closed immediately was never counted. The tick now reads end-of-epoch serving state (after the post-exit re-scan refreshesdeep_seated). Re-measured swan-4 arms confirm the predicted +1-per-exit-opened-window undercount exactly (ssSE+= run-wideextTon each row): reseed-3 V-trough 365 → 429 (worst window 9 → 10), reseed-12 170 → 204, servo-400 282 → 403, and 5 final-epoch windows on the servo row are now visible at the run boundary (ssOpn0 → 5). Banked figures updated inSTAKER_ARCHIVAL_SIM.md§L17 (table, Finding 4, ledger row),FOLLOWUPS.md, and the public prose (~9 → ~10 epochs worst window). Same round: the per-epoch scan comment's stale "data-extinction" wording aligned with the swan-4 sole-source framing. Round 1 of the same review (commit2fdf33650) aligned theScenarioResult/ExtinctionScandoc comments, added the[0, 1]loud-failure assert on the shock exit fractions, corrected theArchivalLockModelformula doc, and madelocked_atomicpanic onblocks_per_shard == 0instead of silently reporting zero locked supply.
Added
-
crypto: CT-3c persistent reorg rollback (
shekyl-curve-tree, 2026-06-12).CurveTreeClient::rollback_to_fork(BlockHeight)now rolls the redb-backedLeafStoreback to an inclusive fork point, verifies the boundary-adjacent frozen segment'sR_k, rebuilds in-memory state from the authoritative store, and resumes forward ingest atfork + 1. Rollback partitioning is aligned with CT-2's drain boundary (maturity > drained_through(fork_height), whilesync_tip_heightand the orphan filter remain fork-height based), fixing the off-by-one that would otherwise keepmaturity == forkrows drained and make the first post-rollback block remove already-drained pending rows. New structured store errors identify frozen-record absence andR_kmismatch. Coverage adds the primary direct pending-row equality KAT with a re-draining class-(b) witness and a 150k-lock never-draining witness, plus a file-backedreorg_deeprollback/resync KAT against fresh replay and consensus roots.LeafStore::append_drainedis now test-only so production ingest cannot bypass pending-table maintenance. Review hardening (2026-06-13): the rollback poison contract is now machine-enforced — the client sets an internal poison flag at the store commit and clears it only on full rebuild success, so a post-commit failure makes every load-bearing call (ingest_block,root_at,verify_root,rollback_to_fork) fail fast withClientError::Poisonedinstead of relying on caller prose discipline;verify_frozen_tailmaps a corrupt freeze-segment counter toStoreError::CorruptMetarather than panicking on the rollback path; and the persistentreorg_deepKAT asserts itsdeep_pop <= main_tipfixture invariant explicitly. -
crypto: CT-3b persistent client lifecycle (
shekyl-curve-tree, 2026-06-12).CurveTreeClient::open(path)resumes from a persistedLeafStorewith no genesis replay: in-memory state is rebuilt as the gindex-sorted union of the drained and pending tables, element-wise identical to a never-restarted run (B4 invariant, KAT-pinned end to end via a restart round-trip).ingest_blocknow writes both tables atomically throughappend_block_deltasbefore committing in-memory state, making the store-behind-memory divergence structurally unreachable — the CT-2-era self-heal path is deleted with that inversion. Resume refuses pruned stores loudly (ResumeFromPrunedStore; pruned-resume is F5/V3.0 work) and rejects cross-table gindex duplicates as corruption. The client's outward API (BlockLeaves.height,ingest_block,root_at,verify_root,drained_leaf_count, error fields) is retypedu64→BlockHeight, closing the client portion of the CT-3a P5 FOLLOWUPS row. Staked-lock resume coverage lands both B3 halves: a synthetic 300-block stake drains correctly across a restart, and the adversarial 150 000-block stake is asserted byte-correct in the pending table without ever draining.SCHEMA_VERSIONbumps 2 → 3: a CT-3a-window store is byte-identical but its pending table was not yet maintained by ingest, so resuming from one would silently corrupt the drained/pending contract — v2 stores now fail open withSchemaVersionMismatch; the pre-genesis migration path is delete the wallet store and re-sync. -
crypto: CT-3a persistent-resume store schema (
shekyl-curve-tree, 2026-06-12). TheLeafStorenow carries the full CT-3 sync/reorg contract at the store layer (CT3_SYNC.mdRound 1, closed): redb-typed keys end to end (BlockHeight/Gindexnewtypes plus theTreePosition/SegmentIdretrofit, byte/order delegation KAT-verified), aschema_versionstamp checked before any write on an existing file (error-without-mutation),creation_heightonLeafEntry/leaf_meta, a pending-candidates table, the single-txnappend_block_deltaswrite API (collision/missing-row loud, abort-leaves-no-trace), resume read paths with runtime-checked invariants (gindex parity, maturity monotonicity, key-set symmetry), androllback_to_fork— pruned-range-safe partition search, two-class drained→pending migration, the uniformcreation_height > forkorphan filter, and frozen-segment rollback through shared truncate internals. KAT/robustness coverage includes equal-maturity partition boundaries, a full-snapshot atomicity check, deterministicrand_chachacodec round-trips, and a rollback-equals-prefix-replay property test driven through the production write API. Client wiring is CT-3b/3c. -
docs: swan-arc closure notes — seeding SLO recommendation, domain diversity, enforcement pins (2026-06-11). Five small dispositions closing the L17 black-swan round series. (1) Seeding SLO carries the recommendation, not just the measurement: provision foundation re-seed capacity at the crisis multiple, ~4× steady-state flow — the 40 → 10 wipe-out result is the actionable number (surge seeding interrupts the trough cascade before it propagates), and unlike adaptive
m/nwidening it is not gameable (the foundation's own provisioning decision, not an adversary-triggerable consensus rule, so the §3.3 static-margin objection doesn't apply). Landed in the authority pin, L17 Finding 4, disposition, ledger row, FOLLOWUPS. (2) Foundation domain diversity folded into the SLO: during a sole-source window the shard's availability is the foundation's uptime with effective domain count 1 in the L15 sense; internal redundancy counts toward the diversity floor only if theN_active = 3seats sit in distinct failure domains. (3) Liveness-signal escape inherits the unpredictability prerequisite (ARCHIVAL_FAILURE_CONFIRMATION_PIN.md§3.2): whatever replacesm-keyed slashing carries theH_fire-class beacon-unpredictability requirement — a predictable liveness probe rebuilds the §5 gaming surface under a new name. (4) Crisis-price multiplier pinned (rule 21): the deterrence-credible criterion is evaluated at the L17 fatal channel, ×0.25 permanent — evaluators do not choose their own stress level; reopens only with the L17 grid itself. (5) Closure observation banked (L17 Finding 4): the scarcity servo and foundation seeding compose by construction through the sole-source window — foundation isn't inMarket, so a sole-source shard readsR_market = 0(maximal scarcity, maximalg-weighted re-acquisition reward) exactly while the foundation supplies the bytes; the binding constraint isreseed_rate, which is what the SLO sizes. The trough self-heals as designed; no emergency mechanism is needed. Black-swan layer closed in the full sense: measured tails, named guarantees, scoped residues (W16/W17 in the Round-2 enforcement gate, W4/W8 in FOLLOWUPS), reversion clauses keyed to the right triggers. -
sim: swan-4 — retention correction + foundation-sourced re-seed bottleneck (L17, 2026-06-11). Third wargame response; dissolves the swan-2/-3 extinction framing. The genesis foundation seeds retain at least one complete copy of every shard permanently, so market-side
extT/shkExtevents are foundation-as-sole-source transitions, not data loss — the 37/99 price-row results re-read as an availability / re-seed bottleneck, and the sim's previously "sourceless" backfill is coincidentally modeling the real foundation-sourced path. Newreseed_rateknob caps fresh-deep acquisitions of sole-source shards per epoch (bandwidth-bound foundation seeding over the L16 rendezvous path); new metricsssSE(sole-source shard-epochs),ssMxW(max single-shard window),ssOpn(windows open at end). Measured (swan4_vshape_reseed3/12,swan4_servo400_reseed3): atreseed_rate = 3(~1 seeding flow per active seat) the V-trough costs 429 sole-source shard-epochs, worst window 10 epochs, servo-400 403/9 (5 final-epoch windows open at the run boundary; everything else re-seeded by horizon end); 4× provisioning halves exposure (429 → 204) and cuts wipe-out count 40 → 10 (faster re-seat interrupts trough cascades). (Figures re-measured after the PR-126 Copilot fix below; the pre-fix tick undercounted each exit-opened window by one epoch.) Foundation seeding bandwidth is therefore an availability-SLO sizing input, exported to gate-5 ops. L17 verdicts re-worded ("extinct" → "sole-source"), Findings 1/4/5 + disposition re-anchored on the retention guarantee ("no shock in this grid loses data; the grid measures availability"), reversion clause keyed on the no-sunset retention pin (W12/W13 completeness questions parked against it, not deleted).docs/design/STAKER_ARCHIVAL_SIM.md§L17. -
docs: swan-4 retention authority pin + availability re-frame (2026-06-11).
V3_STAKER_ARCHIVAL.md§Foundation complete-tree seeds gains the authority pin distinguishing three previously conflated properties: retention (complete B+C held permanently, no sunset — the durability guarantee of record), internal redundancy (foundation ops: N_active = 3 seats, fiat-diversified reserves), and serving participation (the population-decaying floor the sim models). Names the single-organization threat model the guarantee creates (infra loss, seizure, dissolution over the timeframe-2/3 horizon — the single point the market architecture otherwise exists to avoid) with a reversion clause: if the no-sunset pin is ever reopened, the swan-3 W12/W13 market-completeness questions re-activate at that sunset.FOLLOWUPS.mdtreasury item re-anchored: the swan-2/-3 "deep-set completeness requirement" converts to documentation of the existing guarantee; what remains is fiat-diversified reserves sized for serving + re-seed duty through the servo-400 impaired window, plus seeding bandwidth as an availability-SLO input. Public prose aligned (ECONOMY_EXPLAINED.md,PUBLIC_NARRATIVE_FAQ.md): crash troughs cause bounded foundation-only availability windows (~10 epochs worst measured), never data loss, and through a trough durability rests on one disclosed organization — stated plainly, not hedged. -
sim: swan-3 black-swan iteration — band read, floor-on closure arms, class-correlated exit (L17 W12–W15, 2026-06-11). Second wargame response; converts the swan-2 export's two inferences into measurements and pins the domain-result provenance. Per-band extinction read (W12):
extBbins each shock extinction by age band at extinction time; the price rows are mid-deep-modal, not oldest-concentrated (vshape 20/17 b4/b5; servo-400 5/64/30; permanent-gap 23/181/143) — trough extinction follows holder economics at eviction, not shard age, so the swan-2 oldest-stratum completeness scope under-scoped; gate-5 export re-scoped to the entire deep set. Floored read + closure arms (W13): the swan worlds were bare-lean (floor_replicas = 0— pinned in the ledger row), so 37/99 was the unbacked read; newshkExFcounts data-dead only when market holders andfoundation_floor_agedare simultaneously zero (also catches the floor-withdrawal hand-off race).swan3_vshape_floor/swan3_servo400_floorreproduce identical market damage withshkExF= 0 — closure evidence that deep-set completeness converts every measured trough loss into a covered availability window (incl. the 114-epoch servo-400 window);swan3_vshape_floor_t9shows the P3 tilt ≤ 0.9 doesn't leak (tilt redistributes depth, not membership; any schedule zeroing a deep band's floor share violates completeness — new reversion trigger). Class- correlated exit (W15): the swan-2 domain rows ran with no placement diversity floor (the L15 machinery is scoring-only) — benign results were bucketing luck; newshock_exit_top_deepexits the top deep-holders (honest worst case for the FTX class-correlation pattern): at 50 %, stride 0 / domain-bucketed 1 / class-correlated 28 extinctions; class 30 % costs 4 (outranks every independent 50 %). Reversion criterion (c) re-keyed on class- or domain-concentrated exit ≥ ~0.3. W14: aftershock 4 banked as within the independence-to-no-reseat envelope [(1/2)⁶×120 ≈ 1.9, (3/4)⁶×120 ≈ 21] — above the independence floor because 3 epochs of rate-limited reseat leave the second stride no longer anti-correlated with surviving holder sets.docs/design/STAKER_ARCHIVAL_SIM.md§L17 (table, Findings 4–6, disposition, reversion clause, ledger row). -
docs: swan-3 dispositions W16–W18 (2026-06-11). W16
ARCHIVAL_FAILURE_CONFIRMATION_PIN.md§3.2 gains the fourth joint criterion deterrence-credible: crisis-tailm(W6) and slash deterrence pull in opposite directions — atm = 11/n = 13a strategic actor misses 10 of 13 baselines slash-free, and a crisis-sizedmmakes the slash vestigial exactly when the fetch-on-demand temptation peaks (composes with the W5 residue);E[slash]under the realistic degrade play must exceed storage-opex savings at crisis prices, and if nomsatisfies all four criteria the decoupled liveness signal becomes the design, not the contingency. W17 the W5 telemetry trigger's observables pinned in L17 Finding 6 + reversion trigger (e): challenge-latency distribution shift (weak, continuous — fetch-on-demand is indistinguishable at the challenge interface per 8c) and correlated load spikes on surviving true holders at challenge anchors (confirmatory — the source set's bandwidth is the side channel degraders cannot hide); ledger row honest that detection is open, not solved; stressnet to exercise the source-load observable under induced degradation. W18 public-facing prose aligned:ECONOMY_EXPLAINED.md"graceful degradation" passage gains the coverage-vs-data caveat (survival of irreplaceable history through troughs is a gate-5 requirement, verified by the W13 arms, not an emergent market property);PUBLIC_NARRATIVE_FAQ.md"market archivers thin out" answer pins the condition (complete over all deep history, held continuously, treasury crisis-uncorrelated per W4).FOLLOWUPS.mdtreasury item re-scoped to deep-set completeness. -
sim: swan-2 black-swan iteration — extinction read, domain-correlated exit, aftershock, knee timing (L17 W1–W3, 2026-06-11). Wargame response to the adversarial review of the L17 close; converts three assumed-away tails into measured ones. Extinction accounting (W1): backfill in the model is sourceless (best-response sets
holdings[a][s]with no surviving-source check), soshkRecwas a metric-recovery claim only; newextT/shkExtreads count deep shards whose serving holder set empties after being seated at depth (sticky per slot until recycle; scanned at the shock instant, per epoch, and after voluntary exits — the death-spiral channel's orphanings were otherwise invisible). Domain-correlated exit (W2):shock_exit_domains = nwipes failure domain 0 under the L15a % nbucketing — stride was the most benign correlation structure (holder sets straddle it; per-shard total loss ≈ 0 by construction). Aftershock + timing (W3):aftershock_atre-fires every shock leg against the survivors;swan2_knee_cascadefires at the L13 decayed-purse knee;swan2_cascade_rhocompounds 50 % exit with an entry-suppressing ρ ×2. Fiveswan2_*scenarios + all swan-1 rows re-run: stride 50 % = 0 extinctions but domain-correlated 50 % = 1 and the 50 %+50 % aftershock pair = 4 (analytic match: (1/2)⁶ × ~120 deep); the recovered V-crash and servo-400 rows quietly extinguish 37 / 99 deep shards in their troughs — metric recovery ≠ data recovery, now measured. Knee-timed and domain-1/3 cascades absorbed with zero data loss. L17 re-dispositioned (claim narrowed to measured; reversion re-keyed on domain-concentrated exit ≥ ~0.3, correlation not magnitude; floor completeness over the oldest stratum exported as a gate-5 survival requirement; honest-holding-under-stress named as residue W5 with telemetry trigger).docs/design/STAKER_ARCHIVAL_SIM.md§L17 + ledger row. -
docs: swan-2 cross-gate exports W4–W11 (2026-06-11). The adversarial review's reversion-clause and cross-reference edits, landed with authority: W4 foundation-treasury diversification named as a V3.0 ops requirement (
FOLLOWUPS.md; the floor's fiat capacity is pro-cyclical in exactly the crisis it must absorb, and Finding 4 makes it load-bearing for data survival) and the servo-ceiling pro-cyclicality caveat folded into L17 Finding 3 (crisis-time fee capacity sits below the static ceiling). W6 enforcement pro-cyclicality:ARCHIVAL_FAILURE_CONFIRMATION_PIN.md§3.3 —n − msized statically against crisis-tail run-lengths under induced correlated failure (adaptive emergency widening rejected per the escalation-dodge result); stressnet campaign extended accordingly. W7 T-A1/F1 acceptance recorded as regime-bounded (F1_TA3_TA7_LIFETIME_WINDOW.md§7: the lean-eq premise is temporarily invalidated at swan troughs of 9–25, where the intersection surface is maximally cheap; Form-C not reopened). W8 gate-6 synchronized-exit wargame queued V3.0 (FOLLOWUPS.md; doesRELEASE_COOLDOWN_EPOCHS = 2smear or merely delay the unbond cohort?). W9 AGG reversion clause gains the downwardN_Ptrigger (reopen iff envelope extends below ~25–30; per-archiver claims scale as1/N_P— ~70 kB single tx atN_P= 17 year-30; named guard = per-emission claim cap forcing batch splitting) inSTAKER_ARCHIVAL_SIM.md+REWARD_EMISSION_LEG.md§10.1. W10 gate-7 bonds-only ↔R_marketweighting coupling cross-referenced both ways (ARCHIVAL_CONSENSUS_STATE.md§3.3 note: serve-credit weighting is load-bearing against ~0.75-coin/pseudonym Sybil inflation; gate-7 close is a blocking input to any re-weighting). W11swan_regimeverdict conditioned on the Finding-0 windowed-Gini reconciliation (L17 Finding 7). -
sim: L17 black-swan / acute-shock axis (2026-06-11). Closes the due-diligence gap between the gradual-decline stresses (L13 subsidy taper, P2 price decay) and step-event crises observed in production networks. Gated
shock_*knobs inshekyl-staking-sim(shock_at = 0⇒ inert; legacy rows verified byte-identical): one-epoch token-price gap-down with optional V-shaped recovery (March-2020 class vs FTX/deep-bear class), reservation-yield step with relaxation (2008 flight-to-liquidity vs permanent regime change), and stride-sampled forced exit of an active-set fraction (custody-collapse contagion). New readsshkP/shkRec/shkBA(post-shock worst deep gap, epochs-to-recovery, bonded trough). Tenswan_*scenarios at the pinned genesis economics. Findings (STAKER_ARCHIVAL_SIM.md§L17): population shocks are absorbed by construction (30 % overnight exit and ρ×3 panic never breach the deep bar; 50 % exit recovers in 4 epochs; permanent ρ×2 settles leaner but covered); a V-shaped −75 % crash recovers fully in 17 epochs with no ratchet; the fatal channel is a permanent price collapse with fiat-denominated opex (the Filecoin 2022→24 provider-exodus pattern — confirms P2 for step ignition), survivable only when fee-market headroom scales with the real cost multiplier. Disposition: no new V3.0 mechanism; the L13 adaptive servo + re-engaging foundation floor are the bridge; reversion clause recorded. -
sim: spread gate re-anchored on direct whale gauges — Layer-2 close-out (2026-06-11). Implements the gate the Layer-2 band run sealed:
shekyl-staking-sim'sspread/sprdWverdicts now gate onmax_actor_share < 0.20and oldest-band whale sharewB4 < 0.20(the per-band distinct-actor-seating term rides on the coverage claims, sinceRcounts distinct actors);gini_actor/giniWare printed as reported trend gauges only. Whale-capture bar unchanged at 0.20 — this is a metric re-derivation against the threat model, not a threshold move. Verified by re-run: all lean/whale band rows passsprdWacrossg ∈ [1.5, 4.0], and bare-lean coloc fails re-attribute to coverage oscillation (churn_stable), matching the Finding-3 reading. Verdict columns recorded before this date used the gini-gated definition; raw gauge columns are unchanged (supersession notes inSTAKER_ARCHIVAL_SIM.md§Layer-2 results andREWARD_EMISSION_LEG.md). Stale pre-Curve-repair attractor counts (~79 archivers) annotated to the post-repair ~113 equilibrium inECONOMY_EXPLAINED.mdand the L11 section (Finding-0 fallout; attractor properties unchanged). -
archival:
g(age)normalization pinned — relative-depth age; constant mapped to the sealed band (2026-06-11). Closes the Layer-2 Finding-5 FOLLOWUPS item the day it opened (pre-genesis discount;ARCHIVAL_REWARD_ARITHMETIC.md§Shard age). Consensus shard age is now a relative depth fraction —age_milli = floor(age_epochs · 1000 / chain_epochs) ∈ [0, 1000]— sog(age)spans[1, 1 + age_weight]for the life of the chain, matching the sim semantics the Layer-2 band run sealed. The prior raw-epoch form grewgwithout bound (≈ 700·wfor a 10-year shard atSEB = 10 000), concentratingΣworkonto oldest-band holders over mission timeframes — a shape no sim run validated and the whale-capture surface Layer 2 gates.shard_age_millireimplemented (signature unchanged — callers already passclose_block_height);archival_reward_age_weight_milli1000 → 2000 (sealed targetg* ≈ 2, calibration band[1500, 2500], retunable on stressnet evidence without a design round); epoch-close KAT vector re-derived by hand (Σwork3500 → 2590, derivation recorded in the fixture description);REWARD_EMISSION_LEG.mdChannel-1 prose carries the unit. Mission-timeframe note: this change is what makes theg ≈ 2seal valid at the 30-year horizon, not just at genesis. -
archival: Layer-2 margin-robustness band run and closed (2026-06-11). Built and ran the scoped sweep (
shekyl-staking-simaxeslayer2_band{,_whale,_coloc},layer2_budget{,_whale},layer2_colocbud; 29 scenarios; results inSTAKER_ARCHIVAL_SIM.md§Layer-2 margin-robustness band — results). Decomposition confirmed decisively:giniWtracks population leanness (bondA) in both sweep directions while every direct whale gauge stays flat (mxSW0.013–0.018 vs the 0.20 bar,wB4 = 0, whale ΔginiW≤ 0.003). Dispositions: spread gate re-anchored on direct whale gauges (gini_actordemoted to trend);gsealed as a band[1.5, 2.5], genesis targetg ≈ 2— the band's upper bound is polarized-world coverage oscillation (g ≥ 3.0fails at any purse; theg ∈ {2.0, 2.5}shoulder heals at +30 % purse, the L13 servo's static image); budget cross confirms the purse as the spread lever (giniW0.598 → 0.217 over budget 100 → 200);Curvereserve untriggered. Finding 0: the 2026-06-07gate4_finereadings are superseded — the banded PL Curve repair (4071ec032) moved the L11 equilibrium (bondA79 → ~113; coloc pin row oscillates at bare-lean purse); thebond_rate* = 0.75pin stands under the re-read (Finding 4). Finding 5: sim-vs-consensusg(age)units gap (normalized [0,1] age vs raw epoch counts) blocks mapping the sealed band ontoarchival_reward_age_weight_milli— normalization pin queued inFOLLOWUPS.md(V3.0). Cross-docs:REWARD_EMISSION_LEG.md§1.2 #4 + keystone row + checklist;WALLET_REWRITE_PLAN.mdstatus. Phase 2B design is fully closed; Stage 3 gates on schema implementation + gate-6 soundness. -
archival: Layer-2 margin-robustness band scoped (2026-06-11). The last open 2B design item (
STAKER_ARCHIVAL_SIM.md§Layer-2 margin-robustness band — scope; L1/L2 ledger rows updated;REWARD_EMISSION_LEG.md§1.2 #4). Decomposes theginiW ≈ 0.599grazing reading before sweeping: the whale gauges have wide margins (max_actor_share ≈ 0.011vs 0.20;wB4 ≈ 0.12) and the ~0.59 baseline is the lean L11 attractor's intrinsic inequality (entry-until-breakeven leanness +g(age)incumbency premium + windowed churn flicker), not whale capture. Sweep:g ∈ {1.5–4.0}× pin rows atbond_rate = 0.75+ budget cross {100–200} pricing the gate-1/7 purse as the spread lever. Three named outcomes: seal as-is, re-anchor the gate on direct whale gauges (metric re-derivation, not threshold relaxation), or pull the declining-tailCurve(V3 reserve). -
docs:
ECONOMY_EXPLAINED.md— plain-language economy walkthrough (2026-06-11). Human-friendly explainer of the four control loops (emission curve, release multiplier, adaptive burn, staker emission share) with worked numerical examples from the gate-7 sim run and the L11 attractor tables. Carries the CALIBRATION caveat (coefficients provisional perDESIGN_CONCEPTS.md). Linked fromPUBLIC_NARRATIVE_FAQ.md. -
archival: §2.4 close-condition (ii) closed — per-reward proof aggregate (2026-06-11). Worked byte sweep (
STAKER_ARCHIVAL_SIM.md§Close-condition (ii), ledger row AGG;REWARD_EMISSION_LEG.md§10.1): per-emission tx ≈ 17–19 kB dominated by constant-size hybrid crypto, not the work claim; aggregate at the pinned cadence (SETTLEMENT_EPOCH_BLOCKS = 10_000,MAX = 15) amortizes to 80–310 B/block = 0.027–0.103 % of the 300 kB penalty-free zone across theN_Penvelope {40, 79, 154}; single-tx max ≈ 29 kB; boundary burst self-drains in ≈ 11 blocks; no compounding growth term. Wire confirmed as specced — no format change. Reversion clause: builtFcmpMembershipOnly> 3× estimate,N_P> ~1 500, or epoch < 1 000 blocks. All three PHASE_2B §2.4 close-conditions are now resolved; Stage 3 gates on schema implementation + gate-6 soundness. -
archival: gate-7 bonds-only disposition executed across specs (2026-06-11). Cross-document spec edits landing PHASE_2B §2.4 close-condition (iii) on maintainer sign-off:
REWARD_EMISSION_LEG.mddeletes the admission branch (noadmission_proofvin field §5.3, no admission-threshold verify step §7.1,backing_okloses theΣ ≥ ADMISSION_MIN_ATOMICconjunct §7.2, §7.4 amount proof deleted-not-gated, §10.2 records the closure + reversion pointer);PHASE_2B_STAKE_LIFECYCLE.mdcloses §2.4 (iii), the staking-form admission row, the §2.3 principal wire, and the reward-leg threshold-proof bill item;ARCHIVAL_FIREWALL_GATE6.md§2.5 becomes the sole owner of any funding minimum (wallet hygiene, non-consensus);V3_STAKER_ARCHIVAL.mdadmission bullet updated.ADMISSION_MIN_ATOMICis not a consensus constant; reinstatement only via the G7 reversion clause (STAKER_ARCHIVAL_SIM.mdledger). Follow-up pin (same day, maintainer direction): gate-6 §2.5 pins no wallet-policy minimum either — no funding minimum at any layer (consensus, economics, Sybil pricing, and privacy surfaces each examined; none is load-bearing on an amount floor; reversion clause names the reopening threats). -
economics-sim: gate-7 locked-supply re-pricing built and run — §2.4 (iii) resolves bonds-only (2026-06-11). Iteration 5 per the
STAKER_ARCHIVAL_SIM.md§Iteration-5 scope:ArchivalLockModelinshekyl-economics-simderives locked supply from the consensus pins (compile-tied toshekyl-archival-retention::ARCHIVAL_BOND_FLOOR_ATOMIC/SETTLEMENT_EPOCH_BLOCKS), denominatingstake_ratioand the burn input against consensus circulating per the pinned build constraint; elevengate7_*scenarios behind--gate7(arm A across theN_Penvelope + shard-geometry sensitivity, arm BMINgrid, volume-stress pair, asserted comparators); legacy eight-scenario output verified byte-identical. Result: derived lock collapses tobond_floor × R × shards(t)atlock/circ ≤ 10⁻⁴in every arm; all three macro gauges insensitive to both arms at everyN_P→ the pre-named indeterminate criterion resolves bonds-only (gate-7 ledger row G7; cross-doc spec edits landed same day — see entry above). Corollary FOLLOWUP (V3.1): the(1 + stake_ratio)factor incalc_burn_pctis effectively inert in V3. -
archival: 2B design dispositions — bond-duration shape,
ClaimedEpochSetencoding, gate-7 iteration-5 scope (2026-06-11). Three Phase-2B design closures, docs-only: (1) bond retention-commitment shape pinned age-scaled-constant at the sim-exercised plateau arm (BOND_DURATION_BASE_EPOCHS = 4,BOND_DURATION_AGE_SCALE = 4, numerics provisional until testnetfetch_latency_per_unit;STAKER_ARCHIVAL_SIM.md§L10-hardening disposition,ARCHIVAL_TIMING_CONSTANTS.md§1,ARCHIVAL_BOND_GATE4.md§3.4); (2)ClaimedEpochSetencoding decided underW = 26— inline sorted absolute-epoch list onArchivalBondValue(v3 → v4 at the implementing PR), bitmap / separate-table / DUPSORT / roaring rejected with reversion clause (REWARD_EMISSION_LEG.md§6.3,PHASE_2B_FSM_RETOOL.mdP2B-3); (3) iteration-5 gate-7 locked-supply re-pricing sim scoped — derived archival-lock model replacing the assertedstake_ratioinshekyl-economics-sim, two admission arms, named close criteria for §2.4 close-condition (iii) (STAKER_ARCHIVAL_SIM.md§Iteration-5 scope). -
curve-tree: CT-3 design Round 1 opened (
docs/completed/CT3_SYNC.md). Pre-flight substrate audit (pinned citations) of the persistent-client gap: CT-1/CT-2 landed persistence and KAT-verified block-derived reconstruction, but no path opens on disk, resumes, or rolls back a reorg without genesis replay. Round 1 questions with proposed dispositions: block-derived forward sync confirmed as default (the parent §6 reversion criterion fired — bulk-leaf RPC repositioned to post-prune refetch + archival, deferrable pre-genesis), pending-candidate persistence (long stake tier locks 150k blocks; tail re-scan on resume is unbounded), transactional reorg rollback via maturity binary-search +truncate_from_tree_position, source-seam trait deferred with its first consumer. ParentCURVE_TREE_CLIENT.md§8 #6 / §9 CT-3 row updated; store-backed assembly routed toFOLLOWUPS.md(V3.0, rides prune policy). Round 1 review (same day): R1-Q1/Q4/Q5/Q6 endorsed; R1-Q2/Q3 amended — the original rollback lost still-valid leaves created on the shared prefix but drained on the orphaned suffix, and maturity is not invertible to creation height, so pending rows andleaf_metagaincreation_heightand the rollback becomes truncate-with-migration → creation-height filter → tip reset in one transaction. New findings F6–F9 routed (no-secrets test over the pending table, synthetic equal-maturity partition KAT, restore-from-seed scope totality, freeze-aware truncate source-confirmed); deferredget_curve_tree_leavesendpoint + KAT now a trackedFOLLOWUPS.mdrow bound to prune policy. Round 1 closure verified (same day): three CT-3a/3c implementation riders folded (direct pending-table row-set equality in the CT-3c KAT; shared truncation internals betweenrollback_to_forkandtruncate_from_tree_position; migration read-then-delete contract); title synced to closed.
Changed
-
archival: epoch-close consensus computation moved to Rust (PR 123). All epoch-close arithmetic — market membership, scarcity, curve, shard age,
R_market/Σworkaggregation — now lives inshekyl-archival-retention::consensus_state::epoch_close_computeand crosses the FFI as one coarseshekyl_archival_epoch_close_computecall. The C++ LMDB sweep (process_archival_epoch_close_at_height) is gather/store only; C++ copies of the consensus logic (archival_shard_age_milli,ArchivalBondValue::good_through, in-sweep membership/aggregation) are deleted, as are the fine-grained FFI exports they consumed (shekyl_archival_curve_milli,shekyl_archival_scarcity_milli,shekyl_archival_max_claim_age_w,shekyl_archival_settlement_epoch_blocks). Epoch timing exports (settlement_epoch_at_height,epoch_close_due,prune_below_epoch) andshekyl_archival_good_throughreplace them. KATconsensus_state_kat_v1.jsongains anepoch_closesection; LMDB gather/store/revert covered byepoch_close_gather_compute_store_revert. Sim integer/float curve backends harmonized: degenerate caps credit zero. Fixes pre-existing LMDB cursor bug indelete_archival_*loops (MDB_GET_CURRENTafter delete of last record). -
ci: PR pushes no longer double-run workflows.
build.ymlandrandomx-v2-differential.ymltriggered on both an unrestrictedpushandpull_request, so every push to an open PR ran every job twice (and merge-from-dev pushes spuriously fired path-filtered workflows the PR never touched). Theirpushtriggers are now restricted todev/main(post-merge validation); PR branches are covered by thepull_requestevent alone, which tests the PR merged into its base. Branches without an open PR get CI via a draft PR. -
Workspace MSRV 1.88 → 1.94; CT-1
redb2.6.3 → 4.1.0. Intentional 1.94.0 pin for RandomX PoW + CT-1redb4.1.0 (rustc ≥ 1.89), aligned withshekyl-gui-walletrust-toolchain.toml.shekyl-pow-randomxusesBox::new_zeroed_slice(stable 1.92.0). CI addsrust-msrv-gate(dtolnay/[email protected]+cargo check --workspace). Pre-genesis greenfield: redb v3 format only.
Added
-
engine: Phase 1 orchestrator closeout. The
shekyl-engine-core::Engine<S: EngineSignerKind>domain orchestrator (lifecycle, refresh + scan merge, pending-tx send lifecycle, query surface, tracing forwarder) is complete per the done-matrix indocs/completed/PHASE_1_ORCHESTRATOR_STATUS.md.docs/design/WALLET_REWRITE_PLAN.mdreconciled to the bindingEnginenaming (decision log 2026-04-27): frontmatterphase0_closeoutandphase1_domain_modelmarked completed, global naming-supersession banner added, Phase 1 section banner points at the status matrix, and the stale "orchestrator methods remain pending Phase 1" Phase 2a prose corrected. Carried residue:open_view_only/open_hardware_offloadbodies stay blocked onshekyl-crypto-pqview-only/HW constructors (FOLLOWUPS V3.0 entry holds the reversion clause). -
engine:
Engine::primary_address()accessor + Phase 1 query-pattern docs. Thin&selfaccessor assembling the wallet's one reusableShekylAddress(End-state 5) from theKeyActor's cached public projection and the engine's cached network;ShekylAddressre-exported fromshekyl-engine-core.engine/mod.rsgains a "Query surface (Phase 1 disposition)" section documenting theledger()-guard balance/transfers patterns and replacing the stale "Constructors land next" block; encode→decode round-trip test included. Phase 2c expands the receive surface (payment requests). -
engine:
change_passwordon-disk integration tests (FULL). Two lifecycle tests pin the I/O ↔ KDF ↔ AEAD chain at the orchestrator layer (FOLLOWUPS V3.0 item, Phase 1 closeout):change_password_round_trips_via_independent_wallet_file_openverifies the rotated envelope through an independently constructedWalletFile::open(new password loads state; old password fails withInvalidPasswordOrCorrupt), andchange_password_with_new_kdf_rewrites_envelope_headerasserts the rotated Argon2id parameters on the raw on-disk header viainspect_keys_file. ViewOnly / HardwareOffload coverage rides the capability-dispatch commit when thoseopen_*bodies land. -
logging: single-Rust-image tracing contract, per binary. Rust
tracing::*events are no longer silently dropped (FOLLOWUPS V3.2 item, absorbed into Phase 1): every binary now links exactly one Rust static archive carrying theshekyl_log_*C ABI and onetracing-coredispatcher shared by all Rust call sites.shekyl-ffifolds inshekyl-logging, makinglibshekyl_ffi.athe wallet-side image; the new link-image crateshekyl-daemon-image(shekyl-ffi+shekyl-daemon-rpc, no logic) is the daemon's image, selected per binary by a generator expression incmake/BuildRust.cmake(SHEKYL_RUST_IMAGE_DAEMONtarget property). The standalonelibshekyl_logging.alink and the force-load (WHOLEARCHIVE/--whole-archive) machinery are deleted — with one image per binary there is no resolution race to arbitrate (the MSVCLNK1104flag-parsing failure goes with it). A post-linknmgate fails theshekyldbuild on any secondGLOBAL_DISPATCH. New C-ABI exportshekyl_log_install_tracing_forwarder(decision log 2026-04-25; mechanism amendments 2026-06-10, 2026-06-11) pins init ordering and idempotency (SHEKYL_LOG_ERR_ALREADY_INSTALLED = -12);shekyldcalls it aftermlog_configure. State-machine integration test plus C-harness coverage inshekyl-logging. -
crypto-pq: ADDRESS_DERIVATION_V1 freeze hardening (enforcement only). Published CODEOWNERS-protected KAT corpus at
docs/test_vectors/ADDRESS_DERIVATION_V1/with akat_address_derivation_v1.rsconsumer that verifies the manifest's self-describing fields (vectors_sha256_hex=sha256sum vectors.json, tier counts) and Tier-2 account distinctness; wiredscripts/lint_cpp_clamp_ban.shinto therust-audit-and-testCI job; addedADDRESS_DERIVATION_MANIFEST_HASHtripwire inaddress_derivation_freeze.rswithshekyl-cli derivation-freeze-self-checkas the operator surface (no C FFI export: no C++ consumer exists). Zero semantic changes to the frozen v1 derivation pipeline inaccount.rs. -
archival: consensus state emission read surface (ARCHIVAL_CONSENSUS_STATE). Integer
reward_arithmetic+consensus_stateinshekyl-archival-retention; banded float/integerCurveinshekyl-staking-sim(--curve-impl=float|integer); LMDBarchival_r_market/archival_sigma_workepoch-close sweep; partial-slashbad_intervalF3 fix; KATs (consensus_state_kat_v1, gate4 phase-2emission); docsARCHIVAL_REWARD_ARITHMETIC.md,ARCHIVAL_SIM_ECONOMICS_VERDICT.md; FA-6 Pi scenario B capture inPERFORMANCE_BASELINE.md; CI archival reward gates and aarch64 determinism KAT ondependsARM v8 job. -
curve-tree: CT-1 LeafStore on redb (Round 1).
LeafStorepersists drained leaves and frozen segment metadata (R_k,end_block_height) in redb; height-gated segment freeze, ACID reorg truncate, pin/prune seam, andmixed_composition_roothot path with full-rebuild fallback.CurveTreeClientmirrors drained leaves on ingest. Tier-Astore_kat,upper_layers_kat, and unchangedrecon_kat/assemble_kat. Pins:CT1_ROUND1_PINS.md, close-outCT1_ROUND1_CLOSEOUT.md.Robustness sweep (
CT1_ROUND1_CLOSEOUT.md§5):root_at/verify_root/assemble_pathare&selfqueries bounded by the ingested chain tip (ClientError::ReferenceBeyondIngestedTip); the ahead-of-ingest store catch-up machinery is deleted, making the drained mirror append-only by construction and the freeze clock drivable only by ingested heights.append_drainedrejects non-canonical Selene scalars at write time (StoreError::InvalidLeafBytes); store bounds checks readMETA_LEAF_COUNTinside the operating transaction (single-snapshot, no check/use gap). -
docs: 2026-06-10 doc sweep — key-signature freeze anchored, completed archive consolidated, stale references repaired. Retroactive decision-log entry for the frozen v1 seed→address pipeline (
V3_WALLET_DECISION_LOG.md, from Cursor planstabilize_key_signature_15d8e48a; landed 2026-04-22,f46ddaf56).FCMP_PLUS_PLUS.md"Wallet Restore from Seed" rewritten against the landed pipeline (old 32-byte HKDF diagram andtransfer_detailsstored-secret restore copy removed);USER_GUIDE.mdseed copy fixed to 24-word BIP-39 + opt-in passphrase (Electrum Phase-6 residue).ATOMIC_UNITS_NEWTYPE.mdandPRIMARY_CLAIM_DERIVATION_RENAME.mdarchived todocs/completed/; the 104 references broken by the earlier DAA/Electrum moves todocs/completed/repaired repo-wide (source comments, CI workflow headers, consensus-constants JSON, workspace rules, docs). Four FOLLOWUPS V3.0 items filed: dedicatedADDRESS_DERIVATION_V1KAT corpus,lint_cpp_clamp_ban.shCI wiring,ADDRESS_DERIVATION_MANIFEST_HASHtripwire, USER_GUIDE Rust-CLI realignment. CODEOWNERS freeze-spec path fixed toWALLET_FILE_FORMAT_V1.md; machine-local.cursor/plans/links in active docs repointed to in-repo continuations. -
docs: FOLLOWUPS realigned to the Phase 2b retool; two fired deferral triggers re-dispositioned. The Decision-3C staking-subtree entry is superseded (3C /
h_bind/ 5-scalar leaf are docs-only perPHASE_2B_STAKE_LIFECYCLE.md§2.4; entitlement stack is a deletion target), with its claim-nullifier backstop note retired alongside (dedup is now the per-Pclaimed-epoch bitmap).AtomicUnits::mul_div_rem's reopening criterion re-anchored fromentitlement.rsto the rebased reward-emission arithmetic (PR #123 surface); the confidential stake-UTXO transfer entry re-anchored to the bond model. Freshness: FA-2 (#112) / FA-8 (#113) marked merged (2026-06-08); Stage 3 blocks-on updated (Stage 2 merged; subaddress round resolved; Phase 2b gate list cited); Phase 2b planning-session entry gains a status note (claim-centric FSM scope superseded by the retool). Per21-reversion-clause-discipline.mdc, the two "pre Stage 1 PR 4 kickoff" items whose triggers fired are re-dispositioned:wallet2_ffi_create_wallet/on_create_walletcleanup is superseded by the Phase 5 wholesale deletion (reopens on new consumers or Phase 5 slipping past stressnet), and theepee::wipeable_stringmlock residual is committed as documented-not-mitigated until Phase 5 under the same named reopening criteria. -
docs: confidential-tx surface naming pin (
CT_SURFACE_NAMING_PIN.md). Records disposition for inheritedrct::/rctSigsnaming:ct_signaturesalias is partial fix; verifier →ct_semanticsatwallet2cutover; Rust vocabulary effective now across FFI; no rename PR pre-Phase 5. UpdatesFOLLOWUPS.mdandSTRUCTURAL_TODO.md. -
engine: Phase 2a-4 — TestDaemon fee-bound build→submit; engine send substrate complete.
daemon_fee_estimator_maps_test_daemon_priority_tiersand Custom sanity-ceiling unit tests; output locks reserved before async sign withrelease_build_reservationon sign failure; integration tests for daemon-derivedtx.fee, submit dedup, missing key-image rejection, and reserved-output blocking. Doc closeout inPHASE_2A_SEND_PATH.md/ audit §2a-4. The orchestrator Phase 2a methods, pending Phase 1 when this landed, shipped with the Phase 1 closeout above; remaining 2a residue is tracked inPHASE_2A_SEND_PATH.md§10. -
wallet: Phase 2a-3 sign, wire encode, submit.
KeyActor/LocalKeyssign bridge,shekyl-tx-builderwiremodule, asyncSigner::sign_transfer,TransactionSubmitter, non-emptytx_bytes, depth-1fcmp_proof_sizeKAT, andpredict_weightself-consistency on the build path (PHASE_2A_SEND_PATH.md§5 2a-3). -
docs: Round 4 subaddress design closure (FA-9 / FA-10). New
THREAT_MODEL_WALLET.md— pit-of-success vs adversary, T6/T7, R2-F9 phishing tier. FA-10 cooperative attribution foundation pin inPOST_QUANTUM_CRYPTOGRAPHY.md. FA-4 recorded closed (FA-7 + §5.7.8/§5.7.9);USER_GUIDE.mdMonero subaddress copy removed.SUBADDRESS_UNDER_PQC.md§9–§11 Round 4 closed. -
curve-tree: CT-2 Round 1 close-out. Tier-A boundary pin (
last_empty=60,first_drain=61), fixture provenance baseline, Tier-B#[ignore]drift-canary scaffold (recon_tier_b.rs), scannertx_extra 0x07parse tests (first-match; daemon parity unverified). Close-outdocs/completed/CT2_ROUND1_CLOSEOUT.md;CT2_DRAIN_ORDER.mdempty-window prose aligned to fixture truth. -
crypto-pq: FA-6 §8.5.1 decap pre-filter gate harness.
fa6_decap_prefilter_gateexample (--path fa6|classical, scenarios smoke / a / b), classical counterfactual derivation, criterion smoke benchfa6_decap_prefilter; Pi capture scriptscripts/bench/fa6_pi4_gate.sh. §8.7 recorded fail on §8.4 budget (2026-06-08): ship FA-6 at genesis; §10.1 T6 waiver rejected (Pi classical path slower than FA-6). Close-outdocs/completed/FA-6_CLOSEOUT.md; Round 3 closed / Round 4 scoped inSUBADDRESS_UNDER_PQC.md§10.1. Matrix inPERFORMANCE_BASELINE.md§FA-6; Pi scenario B capture pending. -
archival: serve-credit consensus integration test (gate-2 §10 step 3).
archival_serve_credit_integration.cppseeds in-memory archival substrate from KAT fixtureintegrationblock (CT-2 opening + epoch-0 challenge geometry) and exercisesBlockchain::check_archival_serve_credit_inputend-to-end; duplicate-bit rejection test. -
archival: slash scheduler at H_slash_deadline (gate-2 §10 step 4 / gate-4 §4.2).
process_archival_slash_at_heighton block connect applies per-shard floor slash whenchallenge_failed; updatestotal_bonded_atomic/total_burned; journal +pop_blockrevert. FFI:shekyl_archival_challenge_resolution_blocks,shekyl_archival_epoch_slash_deadline_height. CompleteTree slash deferred. -
archival: gate-4 §8 phase-1 lifecycle KAT and Rust-first join verify (bonded-aggregation).
shekyl-archival-retention:bond_floor,verify_join_market_bond_post(incl. both-terms reject),serve_credit_epoch_ok,verify_conservation_snapshot(audit/KAT only).gate4_lifecycle_kat_v1.jsonexercises join → serveE_firstand bonded aggregation; paying-emit stubnull. FFI:shekyl_archival_verify_join_market_bond_post(distinctSHEKYL_ARCHIVAL_BOND_POST_ERR_*per reject reason),shekyl_archival_serve_credit_epoch_ok.ArchivalBondValueLMDB v3 stores per-Pbonded_total_atomic; pre-v3 bond blobs rejected at decode.blockchain.cppdelegates join-market semantics andE_firstlower bound to Rust; hybrid pubkey +P_idhint stay C++. -
archival: JoinMarket bond-post verify and LMDB connect (gate-4 §3.4.1).
txin_archival_bond_postwire (tag 0x05),put_archival_bond_record, andtotal_bonded_atomicon connect/pop; JoinMarket-only at genesis. -
archival: bond-post RCT balance verifier (gate-4 §3.2 / §3.5 step 6).
verRctSemanticsBondPostclosessum(pseudoOuts) + bond_debit = sum(out masks) + fee + bond_credit; commitment sum is verified inshekyl-archival-retentionviashekyl_archival_verify_bond_post_rct_balance; Bulletproof+ stays in C++. Bond-post path enforces canonical BP+ layout when proofs are present, requires a non-zero bond term, and routes away fromverRctSemanticsSimplein NIC verify. -
archival: bond + shard-registry LMDB substrate (gate-2 §5.3 steps 2, 6–7).
archival_bond,archival_shard_segment, andarchival_shard_leafsubdbs withput_*seeding APIs; serve-credit verifier reads bond posture and registry geometry from LMDB (rejects when substrate is missing). JoinMarket bond-post connect is separate (see entry above); Rebond/Unbond/HoldingsUpdate kinds remain gate-4 deferred. -
archival: serve-credit consensus hook + LMDB bit (gate-2 §10 steps 2–3).
shekyl-ffishekyl_archival_verify_serve_credit_vin;Blockchain::check_archival_serve_credit_inputand pure archival tx admission incheck_tx_inputs;archival_serve_creditLMDB subdb (serve_credit_bitwrite on connect, revert onpop_block). -
archival:
txin_archival_serve_credit_responsedeserializer (gate-2 §10 step 2). C++txin_vtag0x04with path bounds;shekyl-oxideInput::ArchivalServeCreditResponsedelegates toshekyl-archival-retention::wire; KAT cross-check in C++ and Rust unit tests. -
archival:
serve_credit_bitrename sweep (gate-2 §11).retention_bit/proven_retention→serve_credit_bitacross emission/consensus specs,WorkClaimVectorfield pin, andshekyl-staking-simT-A1 fingerprint helper. -
archival: gate-2 serve-credit KAT fixture (v1).
tests/fixtures/gate2_serve_credit_kat_v1.jsontripwires challenge replay, wire bytes, signature preimage, and CT-2 founder opening; regen viaregenerate_gate2_kat_fixture. -
archival: serve-credit response vin wire (gate-2 §5.1.1).
ArchivalServeCreditResponseencode/decode (vin tag4),encode(path)for signature preimage, roundtrip + preimage determinism tests inshekyl-archival-retention::wire. -
crypto-pq / engine: primary claim derivation rename (FA-2 residue).
subaddress→output_claim;subaddress_derivation_scalar→output_spend_offset_scalar;derive_primary_source_secrets_bundlehardcodesPRIMARY_CLAIM_INDEX_LE. Deleted publicSubaddressIndex,subaddress_lookahead(settings v2, prefs schema v2),AddressBookEntry::is_subaddress(BOOKKEEPING_BLOCK_VERSION5); pairedWALLET_LEDGER_FORMAT_VERSION6→7 (wallet_ledger.snap). Spec:PRIMARY_CLAIM_DERIVATION_RENAME.md. -
staking-sim: L14b pins sliding-window over escalation FSM (2026-06-08).
failure_confirmation.rs— paired dodge slash (esc 0 / slide 1), slidingmsweep,policy_pin+ Round-2 joint-mgate note.ARCHIVAL_FAILURE_CONFIRMATION_PIN.mdrestructured: §1 pinned policy, §5 rejected-alternative stratum;R_marketserve-credit substrate in consensus §3.3. -
docs: archival failure-confirmation policy pin (2026-06-08).
ARCHIVAL_FAILURE_CONFIRMATION_PIN.md— escalate-on-failure with randomized near-term recheck (not exponential backoff); not-durably-absent enforcement claim; outage-CDF quantile → false-slash; sim decision gate vs sliding-window m-of-n before per-Pconfirmation FSM. Cross-links in gate-2 §0 andSTAKER_ARCHIVAL_SIM.mdL14b. -
archival:
shekyl-archival-retentionverify crate (gate-2 §10 step 1). Challenge leaf index + beacon fire height (cSHAKEdomain labels perARCHIVAL_RETENTION_GATE2.md§3.3–§3.4);verify_segment_pathreplaysshekyl-fcmp::treetoR_kwith Selene leaf-layer chunk scalars; CT-4 assembled-path cross-check KAT. ProvisionalCHALLENGE_BEACON_SEAL_BLOCKS = 1. -
docs: gate-2 Round 1 on-demand serving obligation (2026-06-08).
ARCHIVAL_RETENTION_GATE2.md§0 — solved shape: affirmative serve-credit per epoch; leafℓdeterministic,H_firebeacon-sealed; no 24/7 disk proof.serve_credit_bitrename; L15 = reach not durability; traffic-pay reopen §0.3. Retires §7.5 reacquisition panic (ARCHIVAL_RETENTION_PROOF_8C_FEASIBILITY.md). -
wallet: FA-8 — payment requests + cooperative attribution (V3.0 substrate).
PaymentRequest/ReceiveAttributiontypes;BookkeepingBlockv4 (payment_requests);TransferDetails.receive_attribution; ledger block v6 /WALLET_LEDGER_FORMAT_VERSION6 (pairedwallet_ledger.snap). Inbound: after scan merge, classify decrypted label (sentinel vs REQUEST) and match open requests whenoperational.cooperative_payment_requestsor envSHEKYL_COOPERATIVE_PAYMENT_REQUESTS=1(default off). Outbound:shekyl-addresspayment URI parse/format;construct_output_with_label_plaintext; C++ send path echoes REQUEST viashekyl_construct_output_labeledwhen destinationoriginalis ashekyl:…?rid=URI. CLI stubs:request new,requests list,history incoming --unattributed. -
docs: 8c retention-soundness hinge + gate-2 ordering fix (2026-06-08). Review amendment: Merkle opening proves membership, not set-B retention (public leaves + reacquire-on-demand).
ARCHIVAL_RETENTION_PROOF_8C_FEASIBILITY.md§7.5 reacquisition-asymmetry hinge OPEN; economics reopen if hinge fails;m=3scoped to partial-deletion only.ARCHIVAL_RETENTION_GATE2.mddowngraded to Round 0 draft;H_anchormust fall inside epochE(fixesH_close+1bug); bytes/KATs held for Round 1. -
docs: archival retention gate-2 Round 0 spec (2026-06-08).
ARCHIVAL_RETENTION_GATE2.md— challenge derivation (cSHAKE,m=3,R_k-local Merkle openings),txin_archival_retention_responsewire, verifier order, slash handoff to gate 4; §4 flags retention-credit vs slash-window tension for stressnet. Cross-refs: consensus state §6, bond gate 4 §4.2, 8c feasibility §10. -
docs: loud 8c retention-proof constructibility pass (2026-06-08).
ARCHIVAL_RETENTION_PROOF_8C_FEASIBILITY.md— BUILD disposition: Merkle opening PoR to shardR_konshekyl-fcmp::treeprimitives; ZK and FCMP++ membership rejected for genesis. -
docs: archival corpus fossil sweep (2026-06-08).
ARCHIVAL_CORPUS_FOSSIL_SWEEP.md— superseded-model rationale audit; 8c constructibility test surface §5. Active fossils fixed (SEB-as-F1-lever, lapse-decorrelation, deferred SEB pin). -
archival: timing cluster pinned + reorg/retention split (2026-06-07).
ARCHIVAL_TIMING_CONSTANTS.md:W=26,RETENTION_HORIZON_BLOCKS=420_000,ARCHIVAL_REORG_DEPTH_BLOCKS=720,RELEASE_COOLDOWN_EPOCHS=2,CHALLENGE_RESOLUTION_BLOCKS=10_000(T-A16 margin);prune_horizon_epochs=W. Deleted conflatedREORG_HORIZON. F4 drain inequality signed.shekyl-staking-sim --timing-cluster+src/timing_cluster.rs.config/consensus_constants.jsondocumented (codegen deferred). T-A4 consensus leg unblocked; gate-6 Round 3–4 wallet defaults remain open. -
docs: P2B-6 §7 landed + LMDB substrate verify (2026-06-07).
PHASE_2B_STAKE_LIFECYCLE.md§7 — archival threat model (T-A*, G11 extended, F1/T-A1 gate); claim-era wargame → §7.A. §7.11: LMDB ondevverified (m_propertiesscalars,already_generated_coins,pop_blockrevert pattern; bond wire greenfield). FSM retool substrate-verify row closed. -
staking-sim: T-A1/T-A2 F1 re-linkage instrument v2 (2026-06-07).
rust/shekyl-staking-sim/src/fingerprint.rsre-pointed: timeline channel (baseline-relative rotation advantage) + cohort channel (portfolio co-holder size). Axesta1_f1(four scenarios) andta1_cohort(lean vs distinctive pin). v2 run: timeline passes at lean eq (baseline 0.959, lapse 0.928 — rotation harder than chance); cohort fails (mean cohort 1.0, singleton ~98%).ta1_cohort_sharedpositive control: cohort 92.6, singleton 0.075, F1 PASS (instrument validated; lean failure is emergent portfolio diversity). SEB=20_000 does not move cohort. F1 qual wargame T-A3–T-A7: conditionally finally accepted (Form-C reopen not triggered); T-A4/T-A6 pending timing + wallet-default pins. Disclosure draft §10.F1_TA3_TA7_LIFETIME_WINDOW.md. SeeSTAKER_ARCHIVAL_SIM.md§T-A1 / T-A2. -
docs: P2B-6 §7 threat model review round 1 (2026-06-07).
PHASE_2B_SECTION7_DRAFT.md— F1 SEB structural lever + T-A1 sim gate; T-A16 (A6); G11 KAT invariants; G1 three-tier slash surfacing; partial slash stays Bonded (FSM). Timing doc §1.1 SEB/F1 coupling. -
docs: Archival timing constants stub + PHASE_2B §3 FSM (2026-06-07).
ARCHIVAL_TIMING_CONSTANTS.md— joint cluster enumeration and couplings (W, reorg, release cooldown, SEB). PHASE_2B §3.1–§3.4: archival FSM,ArchivalPInstance,P_canonical_id, pending emission, reorg (P2B-5). §4–§7 bannered claim-era pending retool. Gate-4 §8.2 → timing doc; FSM retool forward order updated. -
docs: Gate 4 round-1 custody base (2026-06-07).
ARCHIVAL_BOND_GATE4.md— consensus-balance model;bond_credit/bond_debitbalance terms; conservation law;== bond_floor; slash forward-only; reorg all-types-atomic; §8.2 numeric cluster. Retool P2B-5/forward order. -
docs: Gate 4 G4-1–G4-7 review pins (2026-06-07).
ARCHIVAL_BOND_GATE4.md—Unbondclean release + cooldown; escrow vs spend-lock (reject attestation-only); supply/burn coupling;E_join+1boundary; unified Market predicate; HoldingsUpdate principle;bond_floorCompleteTree exception. Corpus sync: emission, archival state, PHASE_2B, sim note. -
docs: Gate 4 join-Market + bond-post wire (2026-06-07).
ARCHIVAL_BOND_GATE4.md— join-Market seam (lag-forced distinct from paying mint);txin_archival_bond_postsketch; reorg. Emission §6.4/§8, PHASE_2B §2.4, archival stateMarket, gate-6 lifecycle amended. -
docs: PHASE_2B FSM retool disposition P2B-1–6 (2026-06-07).
PHASE_2B_FSM_RETOOL.md— master keyP_canonical_id; dedup inversion; state collapse; reorg re-read; §3 supersession banner. Emission §6.3: reject LMDB DUPSORT dup-keys option. -
docs: Gate 6 Round 1 —
Phybrid derivation draft (2026-06-07).ARCHIVAL_FIREWALL_GATE6.md§9: archival HKDF labels,p_slot, account-level ML-DSA,P_canonical_id, dual-scan contract, V4 reversion clauses. Builds on Round 0 scaffold. -
docs: Gate 6 firewall design scaffold (2026-06-07).
ARCHIVAL_FIREWALL_GATE6.md— Round 0: four firewall layers + bond-funding,Plifecycle, invariants, adversarial round plan; couplings to epoch-length (F1), L16 transport, E-4 rotation threat.
Changed
-
staking-sim + docs: F1–F4 archival pin closure (2026-06-07).
churn_stablere-pointed to coverage oscillation (oUmx), not participation churn (F2 bank — pin rows passall_pass).ARCHIVAL_CONSENSUS_STATE.md: timeline-axis honest residual (F1); §9.2 settlement-epoch length joint gate-2/6;good_throughinterval event log (F3);W× per-Pbatch drain invariant (F4).REWARD_EMISSION_LEG.md§6.5–§6.6 synced. -
docs: Archival read contract + gate-3 ν dissolution (2026-06-07).
ARCHIVAL_CONSENSUS_STATE.mdrewritten: two-half contract (read surface + invariants), publicP_idkeying, derivedR_market,Wprune horizon. Gate-3ν = H(P, shard)dropped (incompatible with form C). Corpus sync:V3_STAKER_ARCHIVAL.md,PHASE_2B_STAKE_LIFECYCLE.md, foundation docs,REWARD_EMISSION_LEG.md§1.1 sim reconciliation (gate4_fineendogenous;l11_budbond-rate caveat; churn vs spread). -
staking-sim: windowed spread discipline gate (
sprdW) (2026-06-07).shekyl-staking-simreportsgini_actor_window/max_actor_share_window;ALLsub-claim uses windowed spread (L9 lesson). Keystone holds atbond_rate* = 0.75(graze survives); fails at ≥1.00 on both reads. -
docs: Archival consensus state critical path (2026-06-07). New
ARCHIVAL_CONSENSUS_STATE.md— gate 2/3 schema,MAX_CLAIM_AGE_W, interface vs 8c deferral.REWARD_EMISSION_LEG.mdLayer 1 closed; §4.5 collapsed; §1.2 forward path; Layer 2 margin-robustness gate; FSM unblocked. -
docs: Spread thin-margin + §4.5 scope reframe (2026-06-07).
REWARD_EMISSION_LEG.md§1.1: sprdW passes thinly (~0.001–0.007); whale window worsens (0.581→0.594); sim↔spec coherence (reward.rsΣ capped). §4.5: denominator scope (all-recorded lagged leading); E-3 slash stays in denominator. STAKER_ARCHIVAL_SIM L1×L2 entanglement. -
docs: Reward-emission E-1–E-4 pins (2026-06-07).
REWARD_EMISSION_LEG.md§4.0: genesis form Cbudget·Curve(work)/Σ Curve(work)(reject post-servo cap); market-onlyΣwork; per-epochgood_through(E); drop consensus backing rotation;Σworkaccumulator. V3 gate-list + reward-curve prose reconciled. -
docs: Reward-emission leg spec — Layer 1 structural core (2026-06-06/07).
docs/design/REWARD_EMISSION_LEG.md: three-channel reward stack; open §4.5 accumulator timing (joint gate 1); ordinary-transfer admission (noC_stake); four-layer sequencing; spread windowing verdict in §1.1. -
docs: Transfer-shaped admission as leading genesis staking form (2026-06). Elevates §2.4 in
PHASE_2B_STAKE_LIFECYCLE.md: principal↔Pas ordinary FCMP++ main-tree transfers; bond + reward emission as the two irreducible special surfaces; reward dedup via per-Pclaimed-epoch bitmap on bond record (noN_archtag); membership-only control on emission (not ClaimLinkability); off-chain backing before first reward; intra-epoch window covered by bond slash; gate 7 reopen if admission principal drops. Mirrors inV3_STAKER_ARCHIVAL.md,WALLET_REWRITE_PLAN.md,FCMP_PLUS_PLUS.md§15 (3C not genesis),CONFIDENTIAL_STAKING.mdstatus,STAKER_ARCHIVAL_SIM.mdgate 7 coupling. Round 3–4 close-conditions (i)–(iii); next spec: reward-emission leg. -
docs: End-state 5 sequencing + checklist cleanup (2026-06-07). Pins implementation land order in
WALLET_REWRITE_PLAN.md(YAML todos, Phase 2c sequencing table: FA stack → 2c outbound/Wallet API → 4b RPC; 2a-3 not blocked). UpdatesSUBADDRESS_UNDER_PQC.md§9–§11 (FA-7 landed, FA-2/FA-8 PR refs).PHASE_2A_SEND_PATH.md§6 gate table +FOLLOWUPS.mdsubaddress-round status. -
wallet: FA-2 — subaddress deletion (End-state 5). Per
docs/design/SUBADDRESS_UNDER_PQC.md§5.7.4 / §6.2: scanner claims outputs when recovered spend key matches the account primary spend key; removedKeyEngine::derive_subaddress, subaddress registry/labels fromBookkeepingBlock(version 3),TransferDetails::subaddress(ledger block version 5),WALLET_LEDGER_FORMAT_VERSION4→5 (pairedwallet_ledger.snap),TxRequest::from_subaddress, andTxOutputContext::Changesubaddress index. Closes FA-2 item indocs/FOLLOWUPS.mdsubaddress round. -
docs: FA-7 — End-state 5 supersedes flat-subaddress wallet plan. Amends
docs/design/WALLET_REWRITE_PLAN.md(cross-cutting decisions, Phase 1–2c, CLI/RPC method lists) to match End-state 5 indocs/design/SUBADDRESS_UNDER_PQC.md§5.7: one primary address per account, nocreate_subaddressat V3.0; payment requests +enc_labelfor invoice attribution. Supersession entry indocs/V3_WALLET_DECISION_LOG.md;PHASE_2A_SEND_PATH.md§3.10.3 interimSubaddressIndexnote;STAGE_1_PR_3_KEY_ENGINE.md§3.1.3 superseded banner. Closes FA-7 item indocs/FOLLOWUPS.mdsubaddress round.
Added
-
wallet: Phase 2a-1 — daemon fee-snapshot + broadcast primitives. Implements the
DaemonEngine-trait primitives onDaemonClient(docs/design/PHASE_2A_SEND_PATH.md§3.3/§3.6, §5 row 2a-1):get_fee_estimatesissues oneget_fee_estimateJSON-RPC call and stores a single snapshot-levelquantization_maskacross all tiers (no tier-vs-tier skew);submit_transactionparsestx_bytes, computes theTxHashlocally (never trusts a daemon-returned hash), and maps the daemon relay verdict ontoTxSubmitOutcomevia the honest-subset rule (status == "OK"→Submitted;double_spend/fee_too_low→DaemonRejectedTerminal { DoubleSpend | FeeTooLow }; every other failure →Malformed).Rpc::publish_transaction(shekyl-oxide, Shekyl-owned) reshaped to return aTxRelayResponseso the full daemon verdict (includingFailedstatuses) is always surfaced to the caller.ProofStaledetection is deferred to Phase 6 — the C++ daemon reports a stale FCMP++ root as a generic verification failure with an emptyreason, indistinguishable client-side; the reopening criterion (a daemon-sidefcmp_root_stalesignal onsend_raw_transaction) is recorded indocs/SHEKYLD_PREREQUISITES.md§5 anddocs/FOLLOWUPS.md. TheLocalPendingTxasync boundary + capability narrowing (§3.1/§3.2) were re-split out of 2a-1 to land with their first consumers (fee-source in 2a-2, submitter in 2a-3), since both capabilities are consumer-less in 2a-1 (see §5 re-split note). -
wallet: Phase 2a-2 — signing context + fee plumbing. Wires the
LocalPendingTxasync build path perdocs/design/PHASE_2A_SEND_PATH.md§5 row 2a-2:FeeSnapshotSource/DaemonFeeSnapshotSource(PF2) fetch oneFeeEstimatessnapshot per build;FeeEstimationContextcarriesoutput_count,fee_snapshot, andtree_depth(PF1); structuralpredict_weight/converge_fee/FeeDirectiveland intx_fee_model;assemble_tx_to_signbuilds populatedTxToSignfrom ledgerTransferDetails;TxInputSigningContext/TxOutputContext/FcmpPlusPlusContextreshaped per §3.9. Canonical dust threshold (shekyl-rpc::tx_fee::dust_threshold) shared withshekyl-scanner::coin_select. Realsign_transfer/tx_bytes/ submitter remain 2a-3;fcmp_proof_sizeKAT andpredict_weight == tx.weight()grid deferred likewise. -
stake-lifecycle: Round 3 dual-wargamer synthesis — top finding F0 re-gates closure. Two independent adversarial passes over the §7.5 wargame were synthesized into
docs/design/PHASE_2B_STAKE_LIFECYCLE.md§7.5.3 (2026-06-05). Headline F0: a confidential claim does not inherit FCMP++'s whole-chain anonymity set — its effective set is the{tier × creation_height}cohort. The reveal-vs-ZK question is resolved at source:tier(u8) andcreation_height(u64) are cleartext public wire fields (CONFIDENTIAL_STAKING.md§6.4.8; verifier recomputesh_bind+ multiplier from them, §6.4.3) — not merely brute-forceable, published directly. The reveal is load-bearing for inflation-safety (the single revealedtierexcludes tier-forgery by driving bothh_bindand the multiplier; the 3C-over-3A "no new primitive" win), so hiding(tier, creation)re-opens the 3C-vs-3A decision — L4 is V4 co-design, not a v1 bolt-on. Because every claim of one stake carries the identical cleartext(tier, creation_height), claim↔claim linkage is deterministic exact-match — not residual (the DDH split protects claim↔unstake, not claim↔claim), and at cold start the cohort approaches one (the Zcash small-shielded-pool lesson with a tier×exact-creation-height multiplier). F0 reframes T1/T2/T3 into one variable (cohort size), re-opensCONFIDENTIAL_STAKING.md§14.4 item-9, and re-gates Round 3 closure on a consensus policy decision that is three-way, not binary: (1) accept-exact (a high, eyes-open product bar — P1>P2 licenses the reveal but does not price the residual, which is confidentiality materially weaker than the spend path's whole-chain set for stakers); (2) cleartextcreationbucketing (the cheap middle — bind a bucketed creation intoh_bind; cohort →{tier × bucket}; all cleartext, no circuit/primitive; hard inflation constraint: round creation UP as the single canonical value for lower-bound +eff_lock+ lock-enforcement — round-down silently inflates; cost = bounded ≤W lock-shift; tier axis irreducible without V4); or (3) V4 in-circuit hiding (removes the tier axis, at 3A circuit + 8a re-audit cost). The reveal also shrinks the 8a entitlement circuit (cleartext checks stay out of the circuit → tighter soundness perimeter on the existential item; a V4 hide is therefore not security-neutral — it grows the 8a re-audit surface, a named V4 cost). The §14band_sumfloor does not close this — it smooths the public-rate readout (a separate leak); cold-start needs both ("incentivize more staking" fixes absolute size, not the partition). T7 (DDH /G_S-NUMS-independence) is elevated by F0 to the soundness tier — cohort linkage is now chain-owned, so DDH is the sole on-chain protection for within-cohort claim unlinkability, and per-claim circuit hygiene is re-scoped to "don't let the network undo DDH." Baseline-budget calibration (2026-06-05) de-escalates the decision: F0 is the same category of cost the project accepted before confidential staking (staking is the exposed overlay; the spend path is untouched; the core promise is intact), exceeding baseline only in scale-resistance (the block-granular cohort does not dilute with growth — a steady-state thinness) and targeted-cascade; the marginal delta over the realistic already-linkable counterfactual most penalizes the privacy-conscious staker. So option 2 (bucketing) is the recommended v1 disposition (restores dilution-with-growth toward the accepted membership-shaped baseline), and the remaining consensus decision shrinks to ratify bucketing + pick widthW. De-tiering (the economic redesign) is deferred V-future — reject-now-with-reopening-criterion (a high-value-target threat model on the within-tier within-bucket residual); the economics are a designed counterweight and bucketing likely already closes the gap, so it is not a forced move: a derivative (uniform) reward would drop thetierfield from the wire (cohort →{creation_bucket}), butcreation/height stays (time-accrued reward needs a provable accrual-start = the same 3C-vs-3A choice). Other synthesis outcomes: inflation T8 split into 8a entitlement-proof soundness + 8b band-declaration binding (co-equal silent-inflation surfaces — the band↔C_stakerange proof must carry the same rigor as the value range proof); new adversary A5 (economic/rational — bank-run / mass-unstaking, MEV, stake-rental) with a required mass-unstaking servo-stability simulation; and sharpenings — T9 induced-duplicate (daemon claimability is advisory, never a license to reveal a second nullifier), T1/T4/T14 circuit hygiene (fresh circuit per broadcast + Dandelion++-equivalence as acceptance criteria), T10/G12 stake-age leak via nullifier-count (accept-vs-pad decision), T3 public-rate whale/cold-start readout, T5 constant-time claim-build pin + view-key-resident residual, T7G_SNUMS independence, T6 §11 all-five- members-in-one-txn atomicity as load-bearing. Recorded as a result (not a gap): confidential-claim staking structurally excludes the delegated-PoS attack class (slashing-griefing, nothing-at-stake, long-range revision, equivocation, stake-grinding). §7.4 gains A5; §9 row + §10.1 boxes re-gated on F0;FOLLOWUPS.mdV3.1 entry updated. -
stake-lifecycle: F0 bucketing quantified + window-adaptivity + self-advertisement input.
docs/design/PHASE_2B_STAKE_LIFECYCLE.md§7.5.3 (2026-06-05). Bucketing math: cohort≈ W·λ/3; exact-height baseline isW=1(per-block, scale-resistant);W>1is a tunable multiplier and restores dilution-with-growth. Cost is a bounded, tier-relative lock-shift (flatW=720≈ +72 % on a tier-1 1,000-block lock vs ~0.5 % on tier-3 150k) → scaleWper tier, all publicly derived from the revealedtier(no new leak). Window-adaptivity (openW-choice): sliding the window to a participant floork— hard count-closed buckets rejected (future-state ceiling → non-causal binding, reorg-unstable membership, cold-start deadlock, new high-resolutionλ(t)width-leak, Sybil binning-knob, caps cohort atk); soft DAA-styleW(tier,epoch)=clamp(⌈k·B/λ̂_tier⌉,W_min,W_max)frozen-at-binding is the candidate (past-state → reorg-stable, no new aggregate leak, adapts cost to need) under servo-stability discipline (clamp+lag+slew per75-system-autonomy.mdc); honest limit — no scheme makesk-anonymity from<ktotal stakers.λ̂_tierestimator reuses the LWMA-1 difficulty algorithm, estimator-half only (no setpoint/retarget; over per-tier height-keyed creation counts → immune to the timestamp surface difficulty defends; window re-tuned longer than difficulty's, since responsiveness is double-edged — aids cold-start but eases Sybil flood-thinning). Self-advertisement threat-model input: Shekyl ships shareable shard-art (V3_SHARD_VISUALIZATION.md) and tier↔archival-role legibility (V3_STAKER_ARCHIVAL.md) → many stakers self-doxx; for them F0's marginal cost ≈ 0 (reinforces "F0 penalizes the privacy-conscious"), and for the silent tail self-doxx by others erodes the cohort by elimination (soWmust clearkafter attrition). Cross-track: share feature + on-chain holder-registry + distinctive held-shard-sets can bridge identity → on-chain-holder → claim-cohort — an archival /shekyl-shard-visualprivacy-review item, not Phase 2B scope.FOLLOWUPS.mdV3.1 entry extended. -
stake-lifecycle: F-ARCHIVAL refinement — per-shard retention bonds (the keystone), replace-not-converge framing, and gate-list correction. Iterates the resolution below across
docs/V3_STAKER_ARCHIVAL.md,docs/design/PHASE_2B_STAKE_LIFECYCLE.md§7.5.3,docs/FOLLOWUPS.md(2026-06-05). Framing (replace, not converge): F0 and 8a dissolve because pay-for-retention abandons the subsystem they live in — the reserve-DLEQ entitlement, bounded-remainder,entitlement.rs/tiers.rs/rewards.rsbecome dead code, not adapted; "converge" wrongly implies the entitlement proof still applies. What transfers: the adversarial method + the primitives (membership proofs, DDH-independent nullifiers, firewall hygiene). Keystone — per-shard retention bonds (collapses the old tier-decision + Sybil-overloaded-lock gates into one): demonstrated longevity is a past signal and cannot bind future retention; a slashable per-shard retention bond can (drop the shard → lose the archival bond, principal untouched). One mechanism gives (a) the deep-history guarantee (bond-at-risk, not inferred); (b) death of the staker tier → F0 + the portfolio tier-oracle both close because bonds replace tier (the third convergence, earned not free); (c) Sybil-immunity — total bond = shards × rate, pseudonym-count-independent, so splitting buys nothing, flipping the scarce input back to expensive-countable capital; (d) de-overloaded lock (small eligibility lock vs. scaling per-shard bonds = two separate parameters). Slogan corrected: not "pay for work, not wealth" (capital re-enters proportional to work) but "pay for work, where doing the work requires proportional capital-at-risk that bonds the work" — capital re-based from "yield multiplier" to "slashable service-collateral," the better answer to the founding Problem 1 critique (the old principal bond bonded nothing). Two over-reaches in the prior gate-list, corrected: (1) theΣworkservo is differencing-clean —band_sumleaked because it aggregated confidential amounts;Σworksums continuously-public numbers (challenge-responses, replication counts), so differencing reveals nothing not already on P's public record (supply-safe and strictly better thanband_sumon privacy); (2)publicsuperseded 2026-06-07: gate 3 dissolved —Rvia ν-hidingR_marketis ledger-derived; form C makes holdings consensus-public. 8a → loud 8c: the existential item moves to retention-proof unforgeability, detectable not silent.Psimplified: needs unlinkability + backing-proof, not uniqueness (bonds carry Sybil; replication-Sybil is self-defeating). Bootstrap: flat per-active-bonded-shard, foundation-overlapped (no zero-coverage instant), sunset. Genesis-class: capital-bonded yield cannot re-justify itself in the fee-only era (the rent the mission forbids; the one part that fails once the subsidy ends), so this is not a deferrable refinement — the corrected foundational gate is the economic sim re-priced for capital-collateralizing-work, not curve-tuning. Corrected gate-list: (1)Σworkservo (supply-safe + differencing-clean); (2) retention-proof soundness + state cost (8a → loud 8c); (3)per-shard-nullifier countingderivedR_market(2026-06-07); (4) per-shard retention bond replacing tier (keystone; residual = bond-rate calibration); (5) bootstrap = flat per-bonded-shard, overlapped, sunset; (6)Pbacking+firewall (unbuilt; uniqueness relaxed); (7) economic sim re-priced (the foundational gate). Next step — the sim, spec-first:docs/design/STAKER_ARCHIVAL_SIM.md(new), iteration 1 isolating coverage dynamics (1/R+ plateau-cap + per-shard bonds → covered/spread/none-holds-everything/deep-history-held), gates 1/4/5/7 layered as later iterations; no code until the spec is reviewed. -
stake-lifecycle: F-ARCHIVAL RESOLVED IN DESIGN — pay-for-service rebasing + firewalled-pseudonym identity model (supersedes the two-stream framing; pending sim/soundness ratification).
docs/design/PHASE_2B_STAKE_LIFECYCLE.md§7.5.3 (F-ARCHIVAL resolution subsection) +docs/V3_STAKER_ARCHIVAL.md(new canonical §Pay-for-service rebasing and the firewalled-pseudonym identity model, with Decoupling / Tier interaction / Conclusion forward-annotated as superseded)docs/V3_SHARD_VISUALIZATION.md(graceful-degradation-achieved resolution) +docs/FOLLOWUPS.md(2026-06-05). Enumeration: archival is the only service staking provides — consensus is PoW's job (no block-production/fork-choice/finality; the classic-PoS attack class was retired for this reason); the capital "bond" is slashable for nothing so it bonds nothing; supply/monetary effects are achievable by just-holding; governance signaling is downstream of staking existing. So the staking reward is payment for archival, "stake without archiving" is the Problem 1 rent the design exists to kill, and there is no opt-out privacy tier. Model: one archiver type; one work-paid reward (proven-retention-based — not retrieval — scarcity-weighted, banded plateau-cap reusing the rate-servo band machinery for deterministic recomputability); principal = never-slashed collateral serving as admission + per-pseudonym Sybil cost + longevity-credibility, not a yield multiplier. Archival cannot be unlinkable like a claim (reachability + persistence + challengeability require a stable holder), so the goal is a firewalled pseudonym P: replaceStakeEngine::is_active_staker(entity_id)with membership-proof registration ("some active stake backs me") + an archival nullifierN_arch = x·G_archover a base independent ofHp(O)andG_S(one stake → one P Sybil-resistance; uniqueness without revealing which stake; extends T7 DDH to a third base{Hp(O), G_S, G_arch})- HKDF-derived independent keypair P + periodic liveness re-proof that lapses on
unstake. Tier-neutral, longevity-priced shard pricing breaks the tier oracle
(critical-shard stability premium on demonstrated holding-longevity, which is observable
but is not tier) — paying the loss of the "tier-3 → archivist" elegance to stop the
portfolio re-publishing tier. The firewall is a 4-layer stack (crypto + network +
timing + output). Convergence: publicly-computable reward dissolves F0 (tier
leaves the claim wire — reward no longer needs
tier_num) and F-INFLATION 8a (no confidential amount in the entitlement → silent inflation becomes loud/recomputable); the only ZK left on the reward path is membership/backing + stealth payout. Gate-list before consensus-real: (1) aggregate supply normalizer (the §14 servo guaranteedΣreward ≤ budget; a per-staker cap does not — abudget·work_P/Σworkservo restores it but reintroducesΣwork-differencing,band_sum-differencing reborn); (2) retention-proof soundness + per-P/per-shard consensus state-cost (8a transforms, not vanishes — the irony being archival-reward accounting becomes the state-growth problem archival exists to solve); (3) private replication counting becomes central (scarcity drives the whole reward now); (4) tier decision — no free option (dies system-wide with longevity-pricing recovering deep-history retention-horizon matching, vs. lives-with-oracle); (5) Sybil scarce-input shift capital→storage+bandwidth (cheap/invisible/firewall-hidden) + overloaded lock (small for monetary supply vs. large for cap-teeth); (6) bootstrap shape (cold-start coverage cliff vs. named time-limited subsidy whose privacy shape matters); (7)P/N_archunbuilt — the persistent-pseudonym lifecycle is the crux. Scope: this replaces the confidential-yield subsystem (CONFIDENTIAL_STAKING.md+rust/shekyl-staking/entitlement.rs/tiers.rs/rewards.rs) — a large audit-surface deletion, which is the point;CONFIDENTIAL_STAKING.mdleft untouched until gates 4 + 1 settle. Nutshell: staking is the opt-in to archival because staking = archiving.
-
stake-lifecycle: F-ARCHIVAL — archival commitment binding is the load-bearing whole-system tier/membership question, outranks the F0
W-choice.docs/design/PHASE_2B_STAKE_LIFECYCLE.md§7.5.3 (new finding, 2026-06-05), withdocs/V3_STAKER_ARCHIVAL.md+docs/V3_SHARD_VISUALIZATION.mdupdated. Reading the archival + visualization docs together surfaces three couplings the sharing reality activates: (1) archival is staking ("if you stake, you archive") → archival visibility = membership disclosure, now wired to a virality-engineered feature; (2) the portfolio is a tier oracle by economic design (tier-sorted shards → held shards ⇒ tier), a channel separate from the claim wire — so whole-system tier privacy needs both the claim-wire tier (F0) and the archival tier-weighted pricing weakened, further devaluing de-tiering-alone (reinforces parking it); (3) gamification selects high-value targets (rarest = most shareable = most identifying). Load-bearing undecided question: is the archival commitment public address-bound or privately membership-proof-bound? Draft mechanisms (on-chain challenge-response, holder registry, identified reward routing) lean public — and public + mandatory archival + tier-sorted shards = every staker's membership and tier public by construction, no sharing required (dwarfs F0). Private is the presumption (architectural consistency with the privacy-first claim/unstake model; public is the inherited "easier to count" convenience); the hard part is private replication counting for1/Rscarcity pricing (private set cardinality). Priority actionable set: (1) resolve public-vs-private binding (gates all); (2) weaken tier-weighted pricing (explicit economics-vs-privacy); (3) HKDF-separate archival identity from claim/spend; (4) share-UX privacy warnings (graceful degradation). Non-sharer bucketing privacy is conditional on (1)+(2) — bucketing softens only the last cascade link. Organizing principle: graceful degradation (share discloses only what you chose, no silent cascade to tier/claim-history/amounts). Decide beforeArchivalEngineships;FOLLOWUPS.mdV3.1 entry extended. -
stake-lifecycle: Round 3 wargame executed — threat model exhausted against privacy-crypto history.
docs/design/PHASE_2B_STAKE_LIFECYCLE.md§7.5 drives every §7.4 agenda item (T1–T9, G1–G10) to a disposition and augments the agenda with vectors that deployed privacy chains actually suffered: T10 claim-tx-type / staker-set distinguishability (Zcash transparent↔shielded lesson), T11 stake↔unstake commitment linkage (MimbleWimble graph reconstruction), T12 one-time-key collision (Monero 2018 burning bug), T13 remote-node query-pattern leakage (Monero malicious-remote-node), T14 whole-lifecycle broadcast-origin linkage, G11 proof-soundness / inflation (Zcash 2018 counterfeiting + Monero 2017 RingCT bug), G12 wallet/tx- construction fingerprint (Monero wallet2), G13 claim front-running / censorship (MEV); Janus subaddress-linkage confirmed N/A under FA-1's single static address. Each disposition is mitigated-in-design / FOLLOWUP+trigger / cross-track / priority-reject per21-reversion-clause-discipline.mdc. Four load-bearing findings (§7.5.1): (1) inflation is the worst historical class — the wallet's job is non-masking (derivesclaimableonly from its own secret weight × publicρ_e, loud-fails local inconsistency; priority-1); (2) T11 stake↔unstake commitment linkage — investigated and resolved at source: unstake is an unlinkable FCMP++ membership spend appending a fresh main-tree output (CONFIDENTIAL_STAKING.md§6.3/§6.4.3/§7), so the literalC_stakeis never re-published and there is no point-equality link — no Priority-2 finding; (3) bulk-fetch-only nullifier scan (T13, inherits the curve-tree-client never-per-item-query invariant); (4) staking is publicly observable as a class — v1-accepted with full intra-cohort uniformity (not "privacy as a setting"), structural hide deferred to V4/L4. Residuals (§7.5.2): 7 FOLLOWUPS (V3.1, recorded indocs/FOLLOWUPS.md), 2 cross-track upstream asks (T3 cohort floor, T10 L4-for-V4 — T11 resolved at source, no longer an ask), 5 Stage-3 load-bearing implementation pins, 1 priority-reject (G10 fee-bump, PR 5 G3 precedent), 3 N/A. A first clean close was recorded 2026-06-05, then re-gated the same day by the dual-wargamer synthesis (see the entry above, §7.5.3) on new top finding F0. Stage 3 merge remains gated on the §8.0 confidential-consensus dependency, independent of this round. No wallet design change; no disposition reversal. -
units:
AtomicUnitsamount-newtype (shekyl-unitscrate). A new foundationalshekyl-unitsworkspace member ownsAtomicUnits(u64), the wallet's monetary-amount type, replacing rawu64on the money path. It is checked-only — noAdd/Sub/Mul/Divoperator impls and nosaturating_*, soa + bdoes not compile and a local accounting bug cannot silently wrap or saturate; arithmetic goes throughchecked_add/checked_sub/checked_sum, whoseNonearm is surfaced as an error, never defaulted to zero.#[serde(transparent)]+#[repr(transparent)]make it wire- and ABI-identical to theu64it replaced (no serialized-format version bump).Display/Debugrender raw atomic units with anaumarker (never SKL) to kill the unit-confusion bug class; SKL formatting is the explicit, single-sourcedto_skl_string/from_skl_str. The10^9atomic-units-per-SKL relationship is generated bybuild.rsfromconfig/economics_params.json(the same JSON the other economics build scripts read), with a const drift-guard. The amount domain (shekyl-engine-state,-prefs,-scanner,-staking,-engine-coreincluding theOutputClaimsecret site,-tx-builder,-engine-rpc) now carriesAtomicUnits; rawu64survives only at the enumerated edges (theshekyl-oxideCommitment/fee/output amounts, the multisig signed-hash sites, the FFI#[repr(C)]boundary, thecommitment_bytespostcard serializer, and the verified JSON-number RPC field). The CLI's inherited10^12format_amount/parse_amount(the constant that overflowedu64on Shekyl's2^32whole-SKL supply) is reconciled to10^9through the newtype. Design + complete edge inventory:docs/design/ATOMIC_UNITS_NEWTYPE.md. -
curve-tree: reference-block selection + proof validity horizon (§5). New
shekyl-curve-treereferencemodule lands the wallet-side anchor arithmetic:select_reference_height(tip − REF_ANCHOR_AGE, the privacy-canonicalMIN_AGE + 1 = 6offset every honest wallet shares), the proof validity horizon (PROOF_VALIDITY_HORIZON = MAX_AGE − REF_ANCHOR_AGE = 94), and the proactive re-anchor threshold (REBUILD_AT = MAX_AGE / 2 = 50), plus total predicatesreference_block_age/proof_submittable/proof_expired/should_reanchormirroring the daemon acceptance window (MIN_AGE ≤ age ≤ MAX_AGE,SpendIntent::validate_temporal). Selection is height arithmetic, not aCurveTreeClientmethod — the client stores no header roots; binding a height to a consensus root stays the caller's job (caller-supplies-root model, §3.5).REFERENCE_BLOCK_{MIN,MAX}_AGEare emitted fromconfig/consensus_constants.jsonby a per-cratebuild.rs(same JSON authority as the C++ header; keeps the crate lean, drift-safe per the 2026-05 FFI constant-drift audit), with const-eval sentinels guarding the 5/100 baseline. ResolvesPHASE_2A_SEND_PATH.md§9 #5 (proof lifetime "unbounded in Round 0"). Spec:docs/design/CURVE_TREE_CLIENT.md§5. -
curve-tree: CT-4 membership-path assembly. The
shekyl-curve-treeassemblemodule landsCurveTreeClient::assemble_path, producing the FCMP++ prover'sPathfor one owned output at a reference block: theleaf_chunkas compressed-point tuples (O,I = Hp(O),C,h_pqc) plus the full child chunk of each path node from leaf to root, with the root point excluded (carried asTreeContext.tree_root). Layout pinned to the prover at source (C1 = Selene,C2 = Helios): odd tree layers →c2_layers, even internal layers →c1_layers. Assembly runs theverify_rootintegrity gate first (OutputNotDrained/RootMismatchrather than a bad proof). New public typesAssembledPath/ChunkLeaf/TreeContext(owned by this crate, mapped toSpendInputat the engine boundary); drained leaves retain theirOutputIdentityso the compressedIcan be re-derived via the newshekyl_fcmp::tree::key_image_generator. A Tier-A KAT (assemble_kat) independently re-hashes the assembled branches back to the recorded consensus header root and checks the C3 depth invariant. Spec:docs/design/CURVE_TREE_CLIENT.md§3.5 / §5. -
curve-tree: CT-3 block-derived reconstruction client. The
shekyl-curve-treeclientmodule landsCurveTreeClient, the wallet-facing orchestrator that ingests synced blocks reduced at the caller's decode boundary (BlockLeaves/TxLeafInputs/RawOutput, carrying the scanner-Extra-parsedtx_extra 0x07blob), resolves per-outputh_pqc, threads the global output index, owns the reference-height → drain-cutoff mapping (drained_through = H − 1), reconstructs the root, and enforces the integrity gate (verify_root→RootMismatchon a root the wallet cannot reproduce — loud failure, not a silent bad proof). Reorg is rebuild-from-post-reorg-chain (from_blocks), no separate machinery (derive-don't-accumulate,CT2_DRAIN_ORDER.md§7.1). The crate stays lean (noshekyl-scannerdep). The CT-2 Tier-A KAT now also runs end-to-end through this production path and pins client-path == recon-path height-for-height. Spec:docs/design/CURVE_TREE_CLIENT.md§3. -
curve-tree: CT-2 reconstruct-root KAT (Tier A) against consensus. New
shekyl-curve-treetestrecon_katreplays a checked-in regtest fixture (tests/fixtures/ct2_tier_a.json, generated offline bygen_ct2_fixture.py, never run in CI) and asserts the wallet's block-derivedbuild_layersroot byte-equals the C++ consensus headercurve_tree_rootat every height across three chains (main + deep/shallow coinbase reorgs). Pins S1 (index order), the S2 drain boundary (drained_through = height − 1; empty window 0..=60, first founder drain at 61), and S3 reorg position-stability + freeze-lag. Spec:docs/design/CT2_DRAIN_ORDER.md§8. -
wallet: Phase 2b stake-FSM pending-claim model pinned (Round 2,
PHASE_2B_STAKE_LIFECYCLE.md§3.4). A confidential claim is non-spending, so it has noOutputIdand nothing forPendingTxEngine's output-keyed reservation (selected_transfer_indices, verified atengine/pending.rs) to lock; the resource a claim reserves is the epoch-nullifier set. Pin: the stake actor gains a runtime-onlyclaim_pending_epochsreservation (not persisted, not on the wire/sealed region — so theEpochSetencoding carries one set per stake, not two), mirroringPendingTxEngine's own crash-self-heal model.PendingTxEnginestays spend-pure (Hybrid-B): confirm/reorg is already shared via RefreshEngine scan + §5.2 (nullifier marker; verification carried by the still-open §5 reconciliation box), and the only spend-engine-specific sliver — the submit-time staleness gate — is duplicated on the claim path (~10 lines) rather than abstracted (E shape), because the gate is a consensus-backstopped UX optimization (a stale claim is rejected at consensus regardless), so copy drift is benign. Rejected: Hybrid-A (locklessPendingTxEngineentry) and Option C (generalize the reservation to abstract resources) — both re-merge claim/spend separation or drag staking-domain semantics into the spend engine. Reversion trigger (§8.9): factor a shared staleness/broadcast primitive on the third claim-tx-type OR a non-backstopped check, whichever first; Stage 5ArchivalEnginedisbursement is the live third-consumer candidate (archival is otherwise additive and does not reopen the FSM). Ratified:Accruing/Claimablestay separate;PendingBroadcastdiscard drops the instance (no terminalDiscarded); R0-D6 post-unstake claims confirmed. Backstop dependency for any future nullifier-set pruning workstream flagged inFOLLOWUPS.md(only out-of-claim-window nullifiers prunable). §5.2 reorg reconciliation collapsed to a single authoritative full-rebuild (claimed_epochs = {S ∈ window : x·G_S ∈ post-reorg chain set}): removes the prior incremental "un-claim epochs observed at/abovefork_height" step, which presumed a per-epoch observation height theEpochSetdoes not carry. The collapse is forcing, not preferential: the incremental path requires a field that does not exist and should not (storing it would foreclose the bitset encoding the roaring-vs-bitset wire choice is weighing), so the height-free rebuild is the only implementable mechanism — in-session reorg and post-Restoreresync are now one operation,claimed_epochsstays a pure (payload-free) index set, andclaim_pending_epochs(runtime-only) is never repopulated by reorg. The reorg path persistsledger → stake → persist(§5.3 parity) with no transactional atomicity — a crash mid-rewind self-heals on reopen via the rebuild, which runs before any claimable balance is exposed. §5 is designed-complete but box-pending two forward-dependencies. §4.7 message protocol now pinned (D1–D4): reorg is folded ontoApplyStakeEvents { reorg_rewind }rather than a separateRewindTo— handled rewind-first in one uninterruptible actor turn (atomicity: nothing interleaves between rewind and forward replay; mirrors the ledger'sreorg_rewindfield). Reorg semantics are clear-all / replay-all of the full surviving own-nullifier set across the whole post-reorg chain (a windowed[fork_height, tip]replay would drop the below-fork survivors of a lock that straddles the fork), with heights living in the scanner, never in the payload-freeclaimed_epochs. Theclaim_pending_epochslifecycle is wired as aPrepareClaimBuildside-effect (set, idempotent under union) + a newAbandonClaimmessage (clear on staleness/discard, touchingclaim_pending_epochsonly, neverclaimed_epochs— whose sole authority is the on-chain nullifier).Snapshotexcludesclaim_pending_epochsandRestorestarts it empty. This discharges §5's first forward-dependency (§4.7); only the consensus-trackpop_blocknullifier-revert (cross-tree atomicity) the wallet reorg mirror assumes still holds the §5 box open.EpochSetwire/sealed encoding pinned (§3.3.2): a fixedu16relative bitmask anchored at the publiccreation_height(2 fixed bytes, one set per stake), not roaring/croaring. Consensus boundsclaimed_epochsto ≤15 elements over a contiguous ≤15-wide window (tier-3 = 150,000 blocks ÷ 10,000 blocks/settlement-epoch = 15 settlement-epochs =MAX_EPOCHS_PER_CLAIM), so roaring's large-sparse-domain compression has nothing to compress and would only add overhead + a crate dependency. The bitmask is payload-free by construction — the anchor is the public creation height and the mask is purely relative, so there is no field in which a per-epoch height could live, which is what forecloses the incremental reorg path (§5.2) structurally rather than by discipline.StakeIdderivation pinned (§3.3.3):cSHAKE256(customization = "shekyl/stake-id-v1", input = tx_hash ‖ le_u64(index_in_transaction)), full 32-byte XOF read, mirroring the in-workspacederive_output_handle(shekyl-crypto-pq/handle.rs) idiom — cSHAKE256, notcn_fast_hash, not BLAKE3.cn_fast_hash(shekyl-crypto-hash) is inherited Keccak-256 with CryptoNote 0x01 padding, a consensus-critical primitive whose purpose is byte-identity withsrc/crypto/keccak.c/ upstream Monero; deriving a new wallet-local id with it drags inherited legacy crypto into post-CryptoNote code, against the00-mission.mdcmodern-not-inherited / PQC-hardened posture (an intermediate pass had "corrected" the Round-1 BLAKE3 placeholder tocn_fast_hashon reuse grounds — that read the intent backwards). cSHAKE256 (FIPS 202 / SP 800-185) is the workspace's PQC-aligned hash (already used bykey_actor/local_keys/traits::key/handle) and dependency-clean:sha3 = "0.10"(exposingCShake256) is already a workspace dependency (verified at source:shekyl-crypto-pq,shekyl-shard-visual), whereasblake3is not — so modern-not-inherited and17-dependency-discipline.mdcselect the same primitive, and BLAKE3 would add a crate for no gain. Domain separation is native via cSHAKE'scustomizationparameter (no hand-rolled input prefix;"shekyl/stake-id-v1"is distinct from"shekyl/output-handle-v1", so no collision despite both absorbingtx_hash); input fields are fixed-width so no length prefix is needed; noview_secret(the id is non-secret, not a secret-keyed handle). The id derives only from the on-chain(tx_hash, index)and is therefore rescan-stable (load-bearing for mappingStakedOutputConfirmed/OwnNullifierObservedevents back to the same persisted instance); thev1customization + a wallet-internal KAT guard persistence stability (a bump is a pre-genesisrm -rfmigration). -
staking:
pop_blockreorg-atomicity spec unified for 3C +h_bindre-stamp closed in design (CONFIDENTIAL_STAKING.md§11 / §6.4.3). Verified at source that 3C state (staking subtree, nullifier table,h_bind) is spec-only —grep nullifier|h_bindinblockchain.cppis empty, and the livepop_block_from_blockchainreverts the pre-3C accrual/pool/burn model. The Apr-07LMDB_WRITE_ATOMICITY_AUDIT.mdPASS is therefore not "for a dead path" wholesale: its curve-tree half is live and is the reusable pattern (deferred insertion viaadd_pending_tree_leaf(maturity)/drain_pending_tree_leaves, withm_pending_tree_drain+ block-pending-additions journals giving exact, DUPSORT-free, atomic pop); only the staking-economics half (accrual/pool reversal) is superseded. Rewrote §11 from the C-2-era subset (which cited the staleblockchain.cpp:800–822and omitted cross-tree atomicity) into the complete reversal surface, mapping each element onto the proven journaled-pop pattern: (1) staking-subtree leaves via their own pending/drain/block-additions triple, (2) cross-tree stake/unstake transitions both directions (§6.4.3), (3) nullifier-table insertions (new reorg-state, C-2), (4)band_sum/rate-epoch records, (5) minted rewards + staked-output insertions — all journaled, all inside the onebatch_start/batch_stopblock txn; the Round-2 audit re-run is mechanical.h_bindre-stamp concern closed: the MIN_AGE-buffer composition holds tier-independently because the buffer is themining → eligibledeferred-insertion drain (no tier term;tier_lockgoverns only the claim-validity window, smallest tier≫ MIN_AGE). A leaf is membership-provable only after drain (creation_height ≜ eligible_height = mining + MIN_AGE, whereh_bindis frozen), and claims prove membership at the current root, so re-stamping a drained leaf'sh_bindrequires un-mining its source block = a≥ MIN_AGE + 1reorg (catastrophic, out of scope); a shallow reorg (≤ MIN_AGE) catches the output undrained inpending_tree_leaves, where the journaled pop restores/removes it and re-mine re-derivesh_bindon re-drain — no drained-leafh_bindmutation ever occurs. Upgraded §6.4.3 ground (iii) from a one-line assertion to this deferred-insertion argument, and disambiguated "consensus-set at inclusion" (§3C decision / re-stake-after-transfer) to "at subtree drain" so it cannot be misread as mining-height stamping. Cross-linked intoPHASE_2B_STAKE_LIFECYCLE.md§3.3.2: theEpochSet2-byte relative-bitmask anchor (creation_height) is reorg-stable by the same MIN_AGE buffer (shallow reorg catches a stake pre-drain — no anchor to move; the §5.2 full-rebuild re-anchors anything caught early), closing the anchor-mutability seam theh_bindre-stamp surfaced. Net: the §5 box's consensus-trackpop_blockforward- dependency is now a written spec (Round-2 implementation + mechanical audit re-run), not an open question. Follow-on (convergence reframing + source verification): verified at source (blockchain_db.cpp::collect_outputs/db_lmdb.cpp::drain_pending_tree_leaves) thatmaturityis computed from the source block height, never the drain-time tip, and the drain gate is purelymaturity ≤ current_height— so the drain anchor is source-relative, transitively. This dissolves theh_bindre-stamp concern within bound by convergence rather than by getting pop mechanics right: a source-relative anchor is invariant under any reorg≤ MIN_AGE, so pop-then-re-extend re-drains at the identicaleligible_heightand re-derives the identicalh_bind— "restore the stamped leaf" and "recompute on re-insert" are two spellings of the same value within bound, making pop-correctness unobservable within bound (the guarantee no longer rests on pop carefulness). Corrected two framings: (a) the protection is structural on the insertion side (the drain-gate admits only≥ MIN_AGE-deep-sourced leaves, so every root — current included — holds only stable-h_bindleaves), not on the reference side, so "membership at the current root" and "h_bindstable" coexist with no tension; (b) §6.4.3 no longer conflates the live main tree's lock-based staked maturity (max(effective_lock_until, H + DEFAULT_TX_SPENDABLE_AGE), pre-3C single tree) with the 3C subtree's owneligible_height = mining + MIN_AGEbuffer — the mechanism is reused verbatim, only the buffer constant is 3C-specific, both source-anchored by the same gate. §11 item 1 now pins re-drain-recompute as load-bearing: re-drain MUST recomputeh_bindfrom the re-mined source height and MUST NOT cache/restore the stamped leaf across a pop — a within-bound no-op (convergence) that silently becomes a beyond-bound permanent corruption if a future implementer "optimizes" by stashing the stamped leaf in pending. The beyond-bound (> MIN_AGE) regime inherits the FCMP reorg envelope (already invalidates in-flight FCMP references broadly) — no stake-specific new exposure. §3.3.2 sharpened: the hot reorg path pays nothing (mask base unmoved when only the drain block is popped); the deep-reorg re-anchor is a rare-path correctness item, not a common-path cost. -
wallet: stake-opening persistence DISSOLVED —
(amount, z)re-derived, never at rest (PHASE_2B_STAKE_LIFECYCLE.md§3.3.1 / §4.1 / §4.2 / §4.7 / §8.8). The opening was pinned to thefile_kek-protected sealed wallet region (claim-forgery-grade secret at rest). That persist-half is redundant with a re-derive-half the doc already specifies and relies on: source check confirmedC_stake = z·G + amount·His plain Pedersen withz = OutputSecrets.z— the standard output blinding, not a fresh subtree mask and noτ·H_tterm (CONFIDENTIAL_STAKING.md§"Pinned commitment construction (staked outputs, Decision 3C)"- the
zmask-table row) — so(amount, z)recover from the staked output alone viaderive_output_secrets, the same HKDF expand the §4.2 resync row (and every received-output scan) already runs, with no whole-tx-balancing context. Pin: the stake record persists public fields +claimed_epochsonly; there is no sealed stake region. The opening is re-derived on hydration (Restore), held in memory for the session (zeroized on drop), and constructed in-process at build-time registration (Secret<StakeOpening>move, not a sealed blob);Snapshotcarries no opening. Exposure flips from claim-forgery-grade sealed material to no claim-grade secret at rest — the dissolution stops persisting a half already re-derived, it does not invent a recovery path. This generalizes R0-D8 (which already excludes theft-gradexfrom persistence) to claim-grade(amount, z): nothing claim- or theft-grade is at rest. Held-on-hydration over fully-transient (sub-decision):xstays transient-per-claim (theft-grade, claim path already composes it);(amount, z)is re-derived once atRestoreand held in memory rather than re-derived per operation, because (a) it is read on every display poll and holding avoids a per-readKeyEngineround-trip, (b) it adds no marginal at-rest exposure (nothing persists either way) and no marginal in-memory exposure that matters (the view key — root of all derivation — is resident all session regardless), and (c) fully-transient would force a claim-build-flow change (R0-D4 /PrepareClaimBuildwould move(amount, z)from held-and-returned to orchestrator-re-derived-via-KeyEngine) for that marginal gain. The §3.3.1 severity asymmetry (x= principal-theft,(amount, z)= claim-forgery) justifies the different treatment; the claim-build flow is unchanged. Two distinct reopen clauses (rule 21), do not conflate: (1) the original R0-D8x-cost clause (reopen persist-xonly if measured resync derivation dominates UX budget and cannot be batched,PERFORMANCE_BASELINE.mdevidence); (2) the dissolution's own clause — a future ledger-pruning pass that evicts a spent staked output before all its accrued epochs are claimed breaks post-unstake re-derivation, and since accrued epochs are claimable indefinitely after unstake (R0-D6, not age-bounded), this reopener is claim-completion-gated, not height-gated: a pruning pass MUST check per-spent-output claim-completion before evicting. Landed coherently across §0.8/§0.10 framing, R0-D3 transport (in-processSecretmove, not sealed blob), theStakeInstancedoc, the §4.7 message table (Restorere-derives,Snapshotopening-free), the §7 threat table (in-memory not at-rest), §9 Round-2 record + §10.1 (was decided-in-principle → gate-confirmed-at-source → now landed; closes the last two Round-2 R-residuals alongside the async split).CONFIDENTIAL_STAKING.mdneeds no change (consensus-side; it only referencesxnon-persistence and defers to §3.3.1).
- the
-
wallet:
StakeEngineasync/sync split resolved — last Phase 2b field residual (PHASE_2B_STAKE_LIFECYCLE.md§4.6). Verified at source that RPITIT-+ Sendis the established idiom across five engines (KeyEnginekey.rs:803/887,RefreshEnginerefresh.rs:268,PersistenceEnginepersistence.rs:204/244/280,PendingTxEnginepending_tx.rs:197/263,DaemonEnginedaemon.rs:206/235), each mixing sync reads with RPITIT actor ops and carrying theSendbound in the trait contract for the async wallet-RPC/CLI/GUI call sites. Ratified, not chosen:StakeEnginestaysSend + Sync + 'static; every actor-routed method returnsimpl Future<…> + Send(not bareasync fn, notasync_trait— a fifth-engine wart). Marked the §4.6 all-sync draft accordingly. Forced-async four (register_pending_stake,apply_stake_events,prepare_claim_build,abandon_claim) — architecture-forced by secret-locality + rewind-first atomicity. Reads all-async now, noStakeViewcache (anti-pre- provisioning): safe becauseprepare_claim_buildis the authoritative path (stands alone, re-validates against currentclaimed_epochs/claim_pending_epochsin-turn), so no read feeds an unvalidated security decision — every read is display-grade / stale-tolerant / consensus-backstopped (§8.9 staleness-is-UX-not-soundness). Corrected the prior lean that keptclaimable_rewards_atomic/unclaimed_epochsasync "regardless": their own backstop argument makes them display-grade like the rest, so the claim-feeding-vs-public split collapses. Cache is a measured-performance reversion (rule 21;PERFORMANCE_BASELINE.mdevidence, not a passive "snapshot already exists") with a named Stage-5 candidate (ArchivalEnginecross-actoris_active_staker/stake_tierpolling); its shape (almost certainly uniform over all reads,prepare_claim_buildthe single validation point) is confirmed at build time, not pre-baked. Closes the field-residual pass without pre-provisioning either the cache or its shape. Deferred sub-item (now resolved): theregister_pending_stakeopening-persistence descriptor was held pending a §4.2 review — the source check correctly found the opening dissolution had not landed (§4.1/§4.2/§3.3.1/§4.7 still persisted(amount,z)to the sealed region; an earlier turn had referenced it as done from conversational memory, the document-analogue of the enumeration-brittleness the project guards against in code). That review is now complete and the dissolution landed as the coherent multi-section change recorded in the next entry, which turns this descriptor to "runtime-only opening from the build context"; the RPITIT-async signature is unaffected. -
wallet: Phase 2b §6 user-facing orchestrator API signed off (Round 2,
PHASE_2B_STAKE_LIFECYCLE.md§6 / §4.6 / §7). Closes the last §6 review residual after a completeness/surplus/inherited-shape pass.StakeViewpinned as a concrete owner-grade struct (id,tier,band,state, per-stakeclaimable: AtomicUnits,claimed_epochs,unlock_height) — it was previously prose-only ("public fields + claimable amount"), so the per-stakeclaim(stake_id)decision had no surfaced bridge from the globalclaimable_rewards()total. Trust-grade split made explicit: per-stakeclaimableis a deterministic function of the confidential principal and public params, so it discloses the staked amount (andclaimed_epochsthe claim pattern);StakeViewis therefore owner-only (owner UI / owner-authenticated RPC) and the §7 lens-3 diagnostic projection is a distinct, more-redacted shape (global total + coarse state, no per-stake fields) — one struct cannot serve both grades. Amounts areAtomicUnitsend-to-end: rejected the "wrap-u64-at-the-boundary" layering (it Shekyl-ifies the already-safe public return and leaves the engine holding the inherited raw-uint64Monero primitive at exactly the cross-instance-sum site where overflow/unit-confusion bugs live). The newtype is the in-Rust domain type across trait/orchestrator/computation;u64reappears only at the true edges (postcard, FFI, consensus amount boundary). Consequence:claimable_rewards_atomic -> u64becomesclaimable_rewards -> AtomicUnits(the type carries the "atomic" meaning, so the suffix is redundant),projected_yield -> AtomicUnits,ClaimableRewardsAtomicmessage →ClaimableRewards, andStakeOpening.amount_atomic/Accruing.accrued_rewards_atomicdrop the suffix.StakeFilterdeclared as a closed by-state enum (All/Accruing/Claimable/FullyUnstaked) mirroringStakeState— the open-endedfilterparam had no named consumer (rule 21). Abandon-claim wiring pinned (§3.4/§6): a builtclaim()is an ordinary claim-typePendingTx; the orchestrator's general pending-tx discard path dispatchesAbandonClaimfor claim-type txs specifically, so "rides the normal path" does not silently drop theclaim_pending_epochsclear (AbandonClaimtouchesclaim_pendingonly, neverclaimed, D3). Pre-stake yield projection omitted by decision (a forward estimate would project the public rate forward and overpromise, §8.6). FA-1 regrounded: dropped the inherited "primary account address" framing (which pre-provisions an account/ subaddress hierarchy Shekyl does not have) for "the wallet's single static address," with an independent-accounts reopen-pointer (rule 21) — when seed-derived independent accounts land, cross-account staking becomes a new opsec decision designed then, not baked into a one-address API now. Also fixed a staleStakeOpeningdoc-comment ("SECRET — sealed at rest") the dissolution's closing grep missed on phrasing drift — verify-by-reading, not re-grepping. Round-2 confirm pass (2026-06-05) hardened four of the above against source: (1) lens-3 redaction now names the derived-value class explicitly — the old "no view/spend, noz, no per-output secret correlation" wording was written for the secret class and does not coverclaimable/claimed_epochs, which are derived leaks (claimable→ principal via public params,claimed_epochs→ claim pattern); the redaction now excludes both by name. (2) Abandon-claim keying located —PendingTx(engine/pending.rs) carries no claim discriminator and no(stake_id, epochs)(verified at source; it stays spend-pure, so threading a claim ref throughReservation::extensionsis the rejected Option-C boundary break). The discriminator + key therefore live in the orchestrator (the one site holding both theReservationIdfromPendingTxEngine::buildand the(stake_id, epochs)fromprepare_claim_build), which recordsReservationId → (stake_id, epochs)for claim builds and consults it on discard. (3)StakeFiltermade total — corrected from the 4-variant subset to a 1:1 mirror of all 7StakeStatevariants + anAllsentinel, both#[non_exhaustive]and held in lockstep, so filtering is total (no inexpressible state, no dead variant). (4)AtomicUnitsis forward intent, not an existing type — verified at source (rule 17): the newtype does not exist anywhere inrust/(the wallet uses rawamount_atomic_units: u64). The §6 sign-off is therefore recorded "modulo theAtomicUnitsconvention" (§10.1 box); the method set is signed off, the amount type is a dangling dependency introduced by a separate interim PR before broad wiring (bounded-cost-now), now tracked indocs/FOLLOWUPS.md(V3.0). Note the most load-bearing constraint for that PR:OutputClaim.amount_atomic_unitsis a secret (wiped on drop,[REDACTED]Debug), so the newtype must preserveZeroize+ non-leakingDebug. -
staking:
EconomicsEngine::rate_at_epochfinalized;pool_weighted_totalretired (PHASE_2B_STAKE_LIFECYCLE.md§8.6 +V3_ENGINE_TRAIT_BOUNDARIES.md§2.7). Closes the R0-D5 Round-2 box.rate_at_epoch(rate_epoch) -> Result<u64, Self::Error>is pinned as the sole yield-schedule surfaceStakeEngineconsumes, with seven semantics pins: (1)rate_epochis a rate-epoch index, not a height — the caller converts via the publicrate_epoch_blocks; (2) returns the public rateρ_eas fixed-pointu64(a rate, not an amount, so notAtomicUnits; consensus-defined scale the wallet consumes); (3) fallible withOk(0)≠Err—Ok(0)is a settled empty-staker epoch,Erris "cannot determine" (unsettled epoch / unsynced mirror / overflow), un-overloading the0that the infalliblepool_weighted_totalconflated; (4) consensus-derived from the on-chainband_sum, not wallet-recomputed (Bug-2-class avoidance); (5) theAtomicUnitscrossing is the yield productown_weight · K_S, not the rate itself (this is the checked-arithmetic centralization site theshekyl-unitsnewtype exists for); (6) no per-stake state — index in, nostake_id(§8.2 reversion preserved); the signature uses the trait'sSelf::Errorconvention (the earlierEconomicsEngineErrorsketch was wrong vsbase_emission_at/burn_amount).pool_weighted_total(-> u128) retired — delete, not renarrate (rule 15): verified at source in both the trait rustdoc and the boundaries doc that its sole named consumer wasStakeEngine::projected_yield's pool denominator, which the confidential redesign eliminated ("no daemon-supplied denominator anymore", §7); zero V3.0 consumers. Reopen-criterion is rule-21-shaped (a future consumer needing a pool aggregate not composable fromrate_at_epoch- chain state). The
band_summirror (ChainEconomicsSource::active_weighted_stake) is repurposed asrate_at_epoch's internalρ_e-derivation input, not deleted. The boundaries doc is amended across all 7 sites (§2.7 Ownership list, method sketch,projected_yieldconsumer narrative, discipline-test example, and the 3.3.6 / class-a / blocking-semantics tables), framed "retired pending Stage-3 code removal"; the code swap + grep-retire targets (economics.rs,local_economics.rs,economics_differential.rs,chain_economics_source.rs,mod.rs,lifecycle.rs) are enumerated in §8.6 for Stage 3. Round-2 status: both wallet-design boxes (§6, §8.6) are now closed; the only remaining open box (§5 reconciliation) is designed-complete and held open solely by the consensus-trackpop_blocknullifier-revert cross-track dependency — no further wallet design work blocks Round 3.
- chain state). The
-
staking: §0.1 pre-flight re-verify trio closed; boundaries-doc §10.5.1 pool-denominator residual fixed (
PHASE_2B_STAKE_LIFECYCLE.md§0.1 +V3_ENGINE_TRAIT_BOUNDARIES.md§10.5.1). Re-walked the three §0.1 engine-identification boxes against the §0.10 secret-ownership shift: (1) §2 trait-binding — §2.7 / §3.3 already discharged by therate_at_epochlanding (rate_at_epochis a §3.3.6 pure-read, carries no cross-engine.await-ordering obligation), §8.3 lens row clean; (2) §1.5 three-condition test re-confirmed — the actor's only secret state is in-memory(amount, z)(re-derived on hydration, never sealed), and fail-stop failure isolation holds precisely because that secret is reconstructable, not authoritative-at-rest; (3) surface-amendment confirmed — the Stage-2 "no secrets in stake actor" inheritance is superseded by the "in-memory-only, re-derived, never-sealed" invariant. One residual fixed: §10.5.1 still describedStakeEngineas owning "principal-pool aggregation state at Stage 4" — an 8th dead pool-denominator site the literal-namepool_weighted_totalsweep missed — corrected to per-stake in-memory openings with no principal-pool aggregation (exact yield =rate_at_epoch× own secret weight, §7 / §8.6). -
staking: §5 reconciliation box split — wallet design signed off; consensus
pop_blockcarry isolated (PHASE_2B_STAKE_LIFECYCLE.md§10.1 / §5 / §9). The §5 planning box conflated the (discharged) wallet-design legs with the (open, external) consensus dependency. Split into: (a) a checked wallet-design box — the single full-rebuild un-claim mechanism (§5.2, in-session reorg == post-Restore), no-transaction self-heal-on-reopen, cross-engine ordering (§5.3), and the §4.7 message-protocol leg (D1–D4) — no further wallet design work remains; and (b) an explicitly-external cross-track carry — §5's reorg correctness assumes the consensuspop_blockpath atomically reverts the stake-claim nullifier set (cross-tree atomicity); the wallet mirror is already maximally correct and cannot fix a consensus pop that fails to revert. The carry closes only by confirmingpop_blockbehavior upstream (CONFIDENTIAL_STAKING.md, consensus Round 2), not by any wallet change. This makes the consensuspop_blocknullifier- revert the sole remaining open §5 item, and confirms no wallet design work blocks Round 3. -
staking: Round 3 agenda pre-staged (
PHASE_2B_STAKE_LIFECYCLE.md§7.4 / §9 / §10.1). With both wallet-design Round-2 boxes closed, pre-staged the threat-model-exhaustion + wider-substrate- audit round (Principles 5–7). Added §7.4 enumerating: adversary models A1–A4 (adversary- controlled daemon / passive observer / memory-disclosure / collusion); a 9-item threat- exhaustion agenda T1–T9 (per-stake claim-sequence correlation, claim↔stake linkage across a sequence, cold-start band-cohort leakage, claim-timing correlation, in-memory opening exposure, nullifier-reorg shapes, nullifier/key-image cross-link, silent-inflation wallet role, fake-event injection); and a 10-item wider-substrate audit seed G1–G10 (Principle 6 — slashing N/A, delegation N/A, lock-up surprise, dust/fee-starved claim, resync-in-flight, mempool eviction, long-range reorg, HW-wallet latency, locked-during-claim, fee-bump rejection mirroring the PR 5 G3 precedent). Pre-staging is not closure (Principle 5): every T/G item isOPENand Round 3 closes only when each carries a disposition (mitigated / FOLLOWUP+trigger / priority-reject+ criteria, rule 21). Cross-track deps recorded: consensuspop_blockcarry (T6/G7), upstream Round 2 wire/KAT (T8/G7), possible cold-start cohort-floor upstream ask (T3). Also fixed a dangling §7.3 cross-reference (line-134 "§7.3 projection axes") by giving the lens-3 diagnostic projection its §7.3 heading. -
staking: confidential-claim entitlement
Dpinned + remainder range-proof construction closed (Round 2, §6.4.1 decisions 1(a)/1(b)). Corrected the underspecifiedD = SCALEtoD = D_tier · SCALE_rate = 2^(k+1):D_tier = 2is the LCD of the pinned tier multipliers{1.0, 1.5, 2.0}(sotier_num_reduced ∈ {2,3,4}exact, one bit) andSCALE_rate = 2^kis the precision dial. Decision 1(b): the remainder commitmentC_ρis folded into the reward output's existing native 64-bitAggregateRangeProofas one aggregated commitment — not a separate or tight⌈log2 D⌉-bit proof — and is the same group element the entitlement relation consumes, bound across both sub-proofs in one transcript (decision 3). The inflation-critical bound is the lower boundρ ≥ 0(over-claim forcesρ < 0; the field-wrap escape needsreward > 2²⁰³while the reward output's own range proof bounds it< 2⁶⁴— a 139-bit margin at the pinnedk), so any width∈ [⌈log2 D⌉ … ~203]is sound and the cheapest is the one already on the wire; a tightρ < Dwould only forbid harmless under-claim. Because the fold fixes the proof width at 64, precision became free up to the field-wrap margin, sokis re-pinned from the obsolete "smallestkclearing a 0.1 % floor" (k = 38) to the floor-dominated kneek = 48 ⇒ D = 2⁴⁹(SCALE_RATE_K): the precision sweep's worst-case relative rate-quantization error falls untilk = 48(2.1e-5) and then plateaus (the irreducible reward floor, not the rate scale, dominates), with a 139-bit margin and 132-bitN·amountoverflow headroom. The knee is lifetime-invariant: it is set by the smallest meaningful rate, floored by the era-independent meaningful-yield cutoff, and the sweep grid spans down to the terminal tail subsidy — so emission decay (0.90/yr) does not move it (knee_is_lifetime_invariant_not_genesis_only). New integer core + de-risking bundle inrust/shekyl-staking/src/entitlement.rs(tier_num_reduced,denominator,SCALE_RATE_K,reward_and_remainder,precision_sweep,recommend_k_knee,wraparound_over_claim_margin_bits) with soundness/overflow/fold proptests; committed sweep (with margin column)docs/test_vectors/staking/entitlement_precision_sweep.json. Spec:docs/design/CONFIDENTIAL_STAKING.md§6.4.1; audit:docs/AUDIT_SCOPE.md. -
staking: confidential-claim transcript composition / anti-splicing closed (Round 2, §6.4.1 decision 3). All four claim components (subtree membership, ClaimLinkability, entitlement Schnorr, folded remainder range proof) bind to one shared root
μ_claim = signable_tx_hash— reusing the existing FCMP++ message rather than a monolithic transcript (the proof systems are heterogeneous and the BP+ is self-contained; a single transcript would re-architect upstream proofs for zero soundness gain).μ_claimcovers the full claim — prefix (tier,creation,settlement_epochs, the nullifier vector, subtree root), rct base (C~,C_claim,C_ρ), and the folded BP+ hash — so moving any component onto another claim, mutating the nullifier set, or swappingC_ρflips the challenge and rejects. The entitlement Schnorr challenge iskeccak256_to_scalar("shekyl-stake-entitlement-v1" ‖ G ‖ H ‖ N_le ‖ D_le ‖ C~ ‖ C_claim ‖ C_ρ ‖ R ‖ μ_claim); its consensus-critical field set + order are locked dependency-free inrust/shekyl-staking/src/entitlement/transcript.rs(EntitlementChallengeInputs,entitlement_challenge_preimageviaoffsetconstants) with KAT / field-presence /μ_claim-load-bearing tests, so the prover and C++/FFI verifier cannot drift on what is hashed. Spec:docs/design/CONFIDENTIAL_STAKING.md§6.4.1 (decision 3) + §6.4.8 verifier ordering; audit:docs/AUDIT_SCOPE.md(item 5a + negative anti-splice vectors). -
crypto-pq: FA-6 ML-KEM-keyed view-tag pre-filter (T6 account path). Re-key on-wire
view_tagfromderive_view_tag_x25519toderive_view_tag_prefilter(ml_kem_ss); scanner leg-swap (universal decap → tag compare → X25519 on match). HKDF saltshekyl-view-tag-prefilter-v1. Regenerateddocs/test_vectors/PQC_OUTPUT_SECRETS.json. Regenerated mainnet/testnet/stagenetGENESIS_TXincryptonote_config.h(coinbaseview_tag_prefilter; block 0 hash changes). Reference hex in theshekyl-devrepo attools/genesis_builder/genesis_tx_*.hex. Spec:docs/design/FA-6_VIEW_TAG_ML_KEM.md.
Fixed
-
fcmp: the Selene leaf layer is never the curve-tree root.
build_layers/build_upper_layersreturned the bare layer-0 Selene leaf node as the root for any1..=SELENE_CHUNK_WIDTH-leaf tree, diverging from the daemon'sgrow_curve_tree, which always wraps the leaf chunk into the layer-1 Helios node before its root-stop check. The wallet's reconstructed root therefore mismatched consensus at every height with a small tree (e.g. the founder coinbase at height 61).build_upper_layersnow stops at "single node at layer ≥ 1" — every non-empty tree is depth ≥ 2; the empty tree remains theselene_hash_init()sentinel. Proven by the new CT-2 KAT; corrects the CT-0 freeze model (undeepen_helios_collapse_39_to_38renamed tohelios_root_shrinks_39_to_38_without_undeepening;reorg_compound_45_to_35target re-wrapped). Docs:CT2_DRAIN_ORDER.md§5,CURVE_TREE_CLIENT.md. -
Genesis
GENESIS_TXregen (FA-11enc_labels). Rebuilt mainnet, testnet, and stagenet genesis coinbase hex with currentbuild_genesis_coinbase_from_destinationssoenc_labelsmatchesenc_amountson load. Mainnet and stagenet use a single treasury output (100_000 SKL combined founder allocation) with deterministic placeholder Bech32m keys until production wallet addresses land; testnet keeps five developer outputs (20_000 SKL each) with deterministic raw-seed keys. Recipient JSON and reference hex live in theshekyl-devrepo (tools/genesis_builder/); address generation:rust/shekyl-crypto-pq/examples/gen_genesis_addrs.rs.
Added
-
engine:
KeyEnginemigrated into akameoactor (Stage 2). Key material now lives in aKeyActorrunning on its own task and owningAllKeysBlobprivately; the orchestrator holds aCloneKeyEngineHandleinstead ofkeys: Arc<AllKeysBlob>.Engine<S, …>stays seven-parameterEngine<S, D, L, E, R, P, F>(no inlineK: KeyEnginegeneric — the deferred-inline-shape reversion clause resolved in favor of the handle). The request half of the actor protocol is public (OutputDetectionInput→OutputClaimResult); the replyOutputClaimcarries secret-derived bytes (the decrypted amount plus the privacy-linkable handle/key_image) and isZeroizeOnDrop(see Security below); theSignTransactionmessage exists but its reply is a Phase-1 stub. Handle-resolved reads (account_public_address, primaryderive_subaddress) serve from an immutableKeyPublicProjectionwith no mailbox round-trip; non-primaryderive_subaddress(Audit)is served from a construction-timeAuditSubaddressSecretprojection (it touches the view scalar — the §2.4/§3.1 "pure function of public keys" claim was corrected during pre-flight). The scanner merge post-pass reads a distinct construction-timeHandleDerivationViewSecret(the 6-i disposition:apply_scan_resultis synchronous under astd::sync::RwLockguard, so routing it through the actor mailbox is foreclosed until the Stage 4 Ledger-actor lock cutover).KeyActoris fail-stop by construction: a handler panic runson_panic→ stop (not restart),on_stopwipes the blob, and the handle maps everykameo::SendErrorto the terminal, non-retryableKeyEngineError::KeyActorUnavailable.KeyEngineHandle::spawnrequires an ambient Tokio runtime and asserts it (panics with a contract message if absent) rather than self-hosting an engine-owned runtime: an owned, long-lived runtime would panic on drop inside the production async context, andkameo's spawn istokio::spawn, which any flavor hosts, so the spawn needs nort-multi-thread. Tests that reachspawnuse#[tokio::test]; sync tests driving a post-create method that forbids an ambient runtime enter a leaked runtime only aroundcreate.tokio'srt-multi-threadfeature is promoted to production[dependencies]as an independent fix for the pre-existingdrive_persistence::block_in_placefeature-unification bug (decoupled from the spawn decision, which does not need it). §5.2 contract/protocol tests landed, as did the §5.3 B9 dispatch-overhead benchmark (see the next entry). No consensus or wire-format change. Design:docs/completed/STAGE_2_KEY_ENGINE_ACTOR.md. -
bench: Stage 2 §5.3 B9 dispatch-overhead + merge-path benchmarks. Five
bench-internals-gated bench targets inshekyl-engine-core:engine_trait_bench_key_dispatch(criterion; three IDs —baseline_claim_minedirectLocalKeys::try_claim_output,actor_claim_minevia theKeyEngineHandleask, and the cheapactor_claim_not_mine),engine_trait_bench_key_dispatch_baseline_iai(iai-callgrind for the deterministic crypto baseline only), and the 6-i merge-path projection pairengine_trait_bench_key_merge_projection(criterion) +engine_trait_bench_key_merge_projection_iai(iai-callgrind). B9 is a bench-vs-bench ratio (actor ≤ 1.05 × baseline), not an absolute gate. The actoraskpaths are criterion-(wall-clock)-only — a cross-thread async round-trip's Callgrind instruction count folds in nondeterministic runtime-scheduling machinery, so there is no iai actor sibling (reversion-claused: reopen if a deterministic async-dispatch measurement method lands); only the synchronous crypto baseline and the synchronous merge projection get iai siblings. The merge-path bench is decision evidence for the §8.1 6-ii deferral. NewKeyDispatchBenchHarness,KeyBaselineBenchFixture, andMergeProjectionBenchFixtureshims are exported through__bench_internals;populate_engine_handle_fieldswas widened topub(crate)so the merge bench drives the real post-pass.compare.pyroutes all five IDs into theengine_trait_benchthreshold class by prefix (no script change). The baseline iai drives its synchronously-completing future with a no-op-waker poll (a current-thread Tokioblock_onunder Callgrind collapsed the count to ~4.8k handshake instructions instead of the ~15.2M decap); the three B9 rows were added toscripts/bench/capture_rust_baseline.shso CI captures them. Baselines captured by CIworkflow_dispatch(run 26732235292, SHAd377edfdb) and transcribed intodocs/PERFORMANCE_BASELINE.md: B9 ratio 1.039 — PASS (actor 1.386 ms / baseline 1.334 ms; ≤ 1.05); baseline iai 15,163,668 instr; merge projection ≈ 1.71 µs/output (iai 5,160,059), confirming eager-6-i. No production-path or consensus change. -
engine:
EconomicsEnginetrait surface (Stage 1 PR 7). Extracted the canonical economic-derivation surface —base_emission_at(base subsidy on the neutral trajectory),burn_amount(absolute adaptive fee burn),pool_weighted_total(theu128pool-weighted stake denominator), andparameters_snapshot— as thepub(crate)EconomicsEnginetrait with theLocalEconomicsStage 1 implementor, itsChainEconomicsSourceread-abstraction (LedgerChainEconomicsSourceadapter), and theEconomicsParametersSnapshot/CalibrationStampreturn types. The snapshot'sCalibrationStamp.params_digestis a canonical Blake2b-256 over the full calibration surface the snapshot exposes — theEconomicParamssub-digest plus the staker-emission consts and theshekyl-stakingtier table — so a staker/tier recalibration that omits agenerationbump cannot false-accept a stale snapshot (shekyl_economics::params_digeststaysEconomicParams-scoped for the C4 fixture lineage). TheEnginegeneric list grows theEslot (Engine<S, D, L, E, R, P, F>); theeconomicsfield is carried for struct-shape stability with zero V3.0 consumer call sites (R6) — production fee/burn/supply computation is not yet wired through the trait. Method rustdoc carries the §6.3 G4 (burn_amountpoint-estimate /as_of_heightstaleness) and G5 (parameters_snapshotgeneration/digest re-compare) consumer contracts. Differential tests replayRecordedChainFixturevectors through aChainMirrorSourceagainstshekyl-economics; Criterion + iai-callgrind benches coverbase_emission_atandparameters_snapshot. This is the 7-trait PR (C0, C1, C2b–C7); the sibling 7-cutover (C2c) that delegates the C++ ESF base subsidy to Rust landed separately (PR #93) and wires the consensus path to theshekyl_base_block_rewardprimitive, not to this trait — the trait remains consumer-free until its own cutover perdocs/design/STAGE_1_PR_7_ECONOMICS_ENGINE.md§6.2. No consensus or wire-format change.
Security
- crypto-pq/engine: zeroize secret-derived values on the scan/sign hot
path (Stage 2 zeroization audit, Findings 1–4). A memory-disclosure
audit run while Stage 2 was touching the scan/sign code found that the
zeroization discipline protecting
AllKeysBlob(sound and unchanged —ZeroizeOnDrop,#[repr(transparent)]wrappers, borrow-returning accessors) did not extend to the values derived from it.- Finding 1:
RecoveredOutput's hand-rolledZeroizesilently omittedrecovered_spend_key(a per-wallet, per-subaddress linkable identifier) andh_pqc. Replaced with#[derive(ZeroizeOnDrop)]so field coverage is compiler-enforced, not implicit in an editable function body. - Finding 2 (highest severity): the wallet view secret — reconstructed
as a bare
curve25519-dalek::Scalaronce per scanned output on a daemon-drivable path — and the per-outputho/y/zscalars and the decrypted-amount bytes were left resident on the stack on every exit path (Scalar/MontgomeryPointareZeroizebut notZeroizeOnDrop). Enabled thezeroizefeature oncurve25519-dalekandZeroizing- wrapped the locals inscan_output_recover. - Finding 3: swept the same bare-scalar pattern across the rest of
output.rs's production scan/construct/key-image paths andZeroizing- wrapped every secret-derived scalar / cleartext-amount local (public points left unwrapped). - Finding 4 (Stage-2-specific): the actor mailbox introduces a new
non-zeroized surface — replies traverse kameo channel buffers this crate
does not own. Established the contract that every actor message/reply
type carrying secret-derived bytes is
ZeroizeOnDrop:OutputClaimnow is (its decryptedamount_atomic_unitsis treated as a secret, reversing an earlier "balance-display, non-secret" disposition), closing theask-cancellation leak where the channel drops an un-taken reply. Areply_path_wipes_on_droptest asserts the reply'sDropwipes. No consensus or wire-format change. Design:docs/completed/STAGE_2_KEY_ENGINE_ACTOR.md§4.3.1.
- Finding 1:
Changed
-
engine/docs: Stage 1 cleanup + status-doc reconciliation. Closed Stage 1 trait extraction's accumulated debt without touching Phase 2 functional stubs. (1) Reconciled the status docs to the ground-truthed
devreality: the orchestrator is the seven-parameterEngine<S, D, L, E, R, P, F>andEconomicsEngineis landed (PR #94), not the pre-#94 five-parameter / "economics spec-only" shape — updatedV3_ENGINE_TRAIT_BOUNDARIES.md,FOLLOWUPS.md, and appended a dated closure checkpoint toSTAGE_1_COMPLETION_AUDIT.md. (2) Recorded the deliberateKeyEngineinline-orchestrator-integration deferral as an explicit reversion-clauseFOLLOWUPS.mdentry (reopen criterion: the Stage 2KeyEngineHandleactor migration). (3) Removed obsolete#[allow(dead_code)]annotations on the now-production-dispatchedLedgerEngine::synced_height/snapshotandRefreshEngine::produce_scan_resulttrait methods (thebench-internals-onlyLedgerEngine::balanceallow is retained with an updated rationale). (4) Swept stale in-code narrative across the engine crate: dropped dangling deleted-MockLedgerprecedent references, rewrote the obsoleteMock*test-substrate framing to the actual no-Mock substrate, normalized current-behaviorC5α/C5βstage labels into behavior descriptions (preserving genuinely-historical notes), and fixed stale four-/five-parameterEngine<…>shape references in comments. Docs/comments plus one annotation removal; no behavior, consensus, or wire-format change. -
shard-visual: type-safe renderer dispatch (no panic fallback).
render::rendernow dispatches on a closedAlgorithmenum (AperiodicTile/Phyllotaxis/Truchet/Crystalline) instead of a&strmatch with apanic!("Unknown algorithm: …")fallback. Dispatch is compile-time exhaustive, so an unknown algorithm is unrepresentable rather than a runtime panic.candidate.rscarries the layer identities asAlgorithmconstants and derives the serializedCandidateRecipestrings viaAlgorithm::as_str(), so the frontend-facing recipe JSON is unchanged. -
shard-visual: per-renderer structural SHAKE256 namespaces (
docs/V3_SHARD_VISUALIZATION.md§"Structural entropy"). The fourcandidate.v1renderers (aperiodic_tile,phyllotaxis,truchet,crystalline) now draw their structural parameters from a dedicated per-algorithm namespace (shard.v1.render.<algorithm>) rather than sharing the legacy eight-wordhash_wordspool. Two renderers reading the same word index previously produced structurally-correlated geometry for one shard, and eight 32-bit words did not stretch across four algorithms without reuse; SHAKE256's XOF gives each(shard_hash ‖ 0x01 ‖ namespace)an independent, unbounded stream.aperiodic_tilepreviously consumed zero hash entropy (identical features → identical geometry) and now draws a rosette rotation and palette-spread jitter. The now-deadRenderParameters::hash_unit/hash_wordsaccessor and field are removed from the crate. The Python explorer (shekyl-dev/visualization) is updated byte-identically. Pre-genesis / pre-release: no archived shard images depend on the old layout. -
economics: C2c cutover —
get_block_rewardbase subsidy delegates to Rust (Stage 1 PR 7 §5.8). The duplicated C++ ESF base-subsidy formula ((MONEY_SUPPLY − already_generated) >> esf+ tail floor) incryptonote::get_block_reward(src/cryptonote_basic/cryptonote_basic_impl.cpp) is deleted; the 4-arg path now calls the canonical Rust primitiveshekyl_base_block_reward(7-base) through the newshekyl::base_subsidy_before_penaltythin wrapper insrc/shekyl/economics.h, mirroring thecompute_fee_burn/compute_emission_splitshape. The weight penalty (mul128/div128_64) and the 5-arg release-multiplier path stay in C++ and are behavior-identical to the C2a′ witnesses; production call sites 1–7 are unchanged (signatures untouched), and fix α was already landed in 7-base. This is a consensus-atomic cutover (07-consensus-atomic-cutovers.mdc): no production path computes the ESF curve in C++ afterward, and the C2a′ dual-leg KAT remains bit-identical. Branched off the post–7-basedevtip (7-base mergefed6f594b; C2a′ ancestor by topology). Closes thedocs/FOLLOWUPS.mdbase-emission migration item.
Fixed
-
build: cargo-audit ignore config relocated to the path cargo-audit actually reads. The advisory-ignore config lived at
rust/audit.toml, but cargo-audit only auto-loads.cargo/audit.toml(no--configflag exists), so the file was inert — the existingRUSTSEC-2026-0097(rand) ignore never applied. The config now lives atrust/.cargo/audit.toml, making both ignores live (verified undercargo audit --deny warnings). AddedRUSTSEC-2024-0436(pasteunmaintained) to the ignore list with rationale:pasteis a compile-time-only proc-macro pulled transitively byshekyl-shard-visual'simageprocdependency, emits no runtime code, and is not security- or consensus-load-bearing; reopen ifimageproc/nalgebradroppasteor if the crate replacesimageprocwith hand-rolled rasterization. Pre-existingatomic-polyfill/bincodeunmaintained notices and theaesyanked notice are unrelated to this change and left untouched (plaincargo audit, which CI runs, does not fail on them). -
refresh: async path no longer skips the engine post-pass (FOLLOWUPS P1/P3). The asynchronous refresh path (
Engine::start_refresh→run_refresh_task) merged scan results throughLedgerEngine::apply_scan_result, which discarded the inserted-indexVec<usize>and never ranpopulate_engine_handle_fields, leaving newly-merged transfers withoutoutput_handle/source_ciphertext. The mutator is removed from theLedgerEnginetrait (now read-only:synced_height/snapshot/balance);run_refresh_taskandEngine::start_refreshare specialized toLocalLedgerand merge through the inherentEngine::apply_scan_result, which runs the merge body and the M3b post-pass under oneLocalLedgerwrite guard. This closes P1 (post-pass skip) and P3 (discardedVec<usize>allocation) in a single commit. The deadFaultInjecting<LocalLedger>test wrapper andreplace_ledgertest helper were deleted; hybrid retry tests driveConcurrentMutationproducer-side. Shape (b) perdocs/design/STAGE_1_PR_4_REFRESH_ENGINE.md§8; atomicity rationale perdocs/completed/STAGE_1_PR_3_M3B_PREFLIGHT.md§3.
Documentation
- stake-lifecycle: §5 reorg reconciliation is forward-rebuild only —
drop the
pop_blocknullifier-revert as wallet-tracked work.docs/design/PHASE_2B_STAKE_LIFECYCLE.mdpreviously narrated the consensuspop_blockstake-claim-nullifier revert as a §5 "cross-track carry" / dependency across six sites. This conflated a daemon implementation detail with wallet work. The wallet does no reversal: §5.2 rebuildsclaimed_epochsforward from the canonical post-reorg chain (derive-don't-accumulate, the same pattern asCURVE_TREE_CLIENT.md'sfrom_blocks). Its reliance on the daemon having reverted the nullifier set is baseline daemon correctness — identical to the spend path's reliance on spent-key-image revert when a spend is reorged-then-respent — so it is not a stake-specific dependency at all, and the daemon'spop_blockmechanism (CONFIDENTIAL_STAKING.md§11; inherited-architecture consensus code) is not the wallet's to track. Edits: §10.1 box rewritten to "forward-rebuild only, no reversal work"; §5.2 closing note collapsed to the key-image-parity statement; §9 Round-2 cell + §3 Round-3 row de-special-cased; §8.0 note trimmed to the minimal "part of this gate, not separate"; §7.4 T6/G7 reframed as wallet-side forward-rebuild properties ("doesclaimed_epochsrecompute correctly from the post-reorg chain?") and the nullifier-revert removed from the cross-track-deps list. Kept: the rejected-incremental-reversal decision record (§5.2). No wallet design change; no disposition reversal — §5 wallet design remains signed off. (2A is confirmed not the owner:PHASE_2A_SEND_PATH.md§3.0.4 defers reorg/tree handling to the curve-tree-client phase.) - engine: seven-lens conformance pass on
PersistenceEngine/PendingTxEngine/KeyEnginetrait surfaces. Documentation-only rustdoc work bringing the three trait files to theLedgerEngine/DaemonEnginebar perdocs/V3_ENGINE_TRAIT_CONFORMANCE_LENSES.md. No signatures, behavior, or implementor changes; no#[allow]scope change beyond adding per-attribute reasons.persistence.rs: added the CL-1 owns-vs-deliberately-off-trait framing (§2.6; hydrationopen/createstays onEngineper Q9.11), the CL-2# Supertrait boundssection (Send + Sync + 'staticdriven from the sync lifecycle viadrive_persistence, shared byArc; notClone— single-owner artifact + advisory lock), the full CL-5# Stage-4 swap-insection, the CL-4 C/I/P triad on all six methods (including a newsave_prefsbody), and per-attribute CL-6#[allow(dead_code)]reasons onbase_path/network/capability.save_prefsis documented as not panicking on mutex poisoning — verified against the implementor, which routes throughshekyl_engine_prefs::save_prefsand never acquires theWalletFilemutex.pending_tx.rs: added the CL-4 C/I/P triad tobuild/submit/discard/signal_mempool_evictedand# Panicstooutstanding, plus a module-level note on the deliberate poison-handling asymmetry (mutators mapMutexpoisoning to a domain error;outstandingpanics on it).key.rs: added# Panicsto all four methods (account_public_addresslockless;derive_subaddress/try_claim_outputRwLock-poison;sign_transactionM3a stub does not panic), reconciledsign_transaction's# Cancellationfrom b to a to match the §4 table (with a PR-5 reversion-clause forward note), and rewrote the stale# M3a transitional bridgesection (which referenced the removedTxInputSigningContext::source_secretsfield) to state the M3a stub reality.- Reconciled
docs/V3_ENGINE_TRAIT_BOUNDARIES.md§4:discardasync → sync(cancelb → n/a, idempotencyyes → no), added the missingsignal_mempool_evictedrow, and appended the PR-5 forward trigger to thesign_transactioncancel cell. Flipped thedocs/V3_ENGINE_TRAIT_CONFORMANCE_LENSES.md§2 scorecard (the three surfaces → conformant) with past-tensed footnotes, and tracked the deferred CL-7 forward-compat audit of the off-trait value/error types indocs/FOLLOWUPS.md(V3.0).
- Codify the seven engine-trait conformance lenses. Added
docs/V3_ENGINE_TRAIT_CONFORMANCE_LENSES.md, which enumerates the documentation-as-contract discipline previously implicit in theLedgerEngine/DaemonEnginereference surfaces and scattered acrossV3_ENGINE_TRAIT_BOUNDARIES.md§§1.4–1.6: CL-1 ownership boundary, CL-2 supertrait bounds, CL-3 error landing pad, CL-4 per-method C/I/P triad (= §1.6's documentation discipline), CL-5 Stage-4 swap-in invariance, CL-6 justified#[allow(dead_code)], CL-7 value/error-type forward-compat. The doc disambiguates these conformance lenses (trait documentation) from §8.3.1's design lenses (trait shape), carries a current-devconformance scorecard for all seven traits, and pins the add/remove-a-lens discipline per21-reversion-clause-discipline.mdc. Cross-referenced fromV3_ENGINE_TRAIT_BOUNDARIES.md§1.6 and hooked into the per-trait PR pre-flight viaSTAGE_1_PER_PR_TEMPLATE.md§3.6. Documentation-only; no code or behavior change. - Inter-stage cleanup: tech-debt tracker consolidation + stale-ref fixes.
Post-P2 audit of Stage 1 (trait-extraction) closure. (1) Consolidated open
structural-debt tracking into a single file: the three orphan MSVC/Windows
build items (
libunboundstub, vendored-code warnings, vcpkg manifest-mode) moved fromdocs/STRUCTURAL_TODO.mdintodocs/FOLLOWUPS.md§"V3.2 — Rust cutover and cleanup";STRUCTURAL_TODO.mdrepurposed as a structural-reference / reviewer-rubric doc (retains the 32-bit "bit-width carve-out" security argument and migration-on-touch rubric that other docs/code cite as canonical), not an open-todo list. Inbound refs inrust/shekyl-logging/src/legacy.rsand.cursor/rules/93-legacy-symbol-migration.mdcretargeted toFOLLOWUPS.md. (2) Fixed stalescan_range_start/effective_floor_at_tipreferences inFOLLOWUPS.mdandSTAGE_1_PR_4_REFRESH_ENGINE.md(helpers removed in the P2 TOCTOU fix, commit87264a3a2; the producer now derives its scan start from the anchored snapshot). (3) Added a dated closure checkpoint todocs/completed/STAGE_1_COMPLETION_AUDIT.mdrecording P1/P2/P3 closure and confirming C2c economics cutover remains the sole open V3.0 economics item (standalone 7-cutover PR perSTAGE_1_PR_7_ECONOMICS_ENGINE.md§6.2, not eligible for chore bundling). Docs-only; no code or consensus changes.
Changed
-
Refresh: wallet-birthday scan floor (P2).
LocalRefreshcarriesscan_start_floorfromsync_state.restore_from_heightand sessionskip_to_height/refresh_from_block_heightoverrides. Refresh preflight anchors the ledger atfloor - 1when needed so the merge gate staysstart == synced_height + 1; the producer scans from the floor through tip.Engine::createpersistsrestore_height_hintintosync_state.restore_from_height. -
Workspace MSRV raised 1.85 → 1.88;
kameo = "=0.20.0"pinned (Stage 2 gate). Satisfies the three preconditions in thedocs/FOLLOWUPS.md"kameo dependency pin and MSRV alignment before Stage 2 cuts" entry: (1) exact-patch pin of the actor framework in[workspace.dependencies](declared-only; no consumer yet, so inert in the build graph), (2) MSRV bump to kameo 0.20.0's required 1.88.0 (verified at source via the crates.io index), propagated per-crate viarust-version.workspace = trueacross all first-party members so the gate is enforced workspace-wide rather than declared only on the virtual root, and (3) the workspace bounded-mailbox default (mailbox(64), overrides documented at the actor site). Norust-toolchain.tomladded — CI builds on@stable(≥ 1.88); the gate's intent is the MSRV declaration, not a pinned channel. Stage 2's first commit adds the live consumer and closes the FOLLOWUP. -
Stage 1 PR 6 — PersistenceEngine C7: remove password
save_state.WalletFile::save_statenow takes session-cachedwrap_key_region_2only; password-taking steady-state save deleted.shekyl_wallet_save_stateFFI drops the password parameters. Design:docs/design/STAGE_1_PR_6_PERSISTENCE_ENGINE.md. -
Wallet file format v1: per-region HKDF wrap keys (spec + implementation).
docs/WALLET_FILE_FORMAT_V1.md§2.6 prescribeswrap_key_region_1(label-only HKDF) andwrap_key_region_2(info || addr) via HKDF-SHA-256 Expand from CSPRNGfile_kek, replacing directfile_kekAEAD for regions 1 and 2. On-disk layout andfile_versionunchanged; Tier-3 KAT fixtures regenerated. Seedocs/design/WALLET_FILE_FORMAT_V1_HKDF_REGION_DERIVATION.md.
Documentation
-
Stage 1 PR 6 — external lessons canvass (§5.12).
docs/design/STAGE_1_PR_6_PERSISTENCE_ENGINE.mdrecords historical wallet-disaster lessons, OS discipline for F5(b), Stage 4 actor-model pins (bounded mailbox, save coalescing, supervisorstop), and substrate verification (KDF params in wrap AAD; HKDFaddress= 65-byte classical address). Blocking PR 6: panic-hook redaction test, mlock honest contract, region-2OsRngnonce rustdoc. V3.1/V3.x items indocs/FOLLOWUPS.md. -
Stage 0 PR-A — iai-callgrind symmetry rule (
3d313256c). Backfill.docs/design/STAGE_0_HARNESS.md§4.2 codifies the symmetry rule (setup and fixture teardown excluded from the measured region; criterion amortizesDropviab.iter, iai-callgrind does not) and adds Finding 5 to the §4.4 gap-check inventory. Closes the Drop-contamination capture (synced_heightreported 60,033 instructions vs the expected low-tens, a property-preservation gap), class-level across everyengine_trait_bench_*bench. -
Stage 0 PR-A-extension — iai-callgrind boundary rule (
2e5309ad3). Backfill.docs/design/STAGE_0_HARNESS.md§4.2 adds the boundary rule (iai-callgrind measures function-boundary value movement;Engine<SoloSigner>is 6,296 bytes, so by-value fixture passing cost ~600 instructions of memcpy) and the §4.4 unified(Box<Engine<S>>, TempDir)component-model fixture shape. Closes the memcpy-at-boundary finding. -
Stage 0 PR-C — iai-callgrind hoisting rule (
93d515123). Backfill.docs/design/STAGE_0_HARNESS.md§4.2 adds the hoisting rule (criterion-sideb.iteriter-amortization can elide state-dependent compute the author meant to count) and the §4.4 two-anchor static check (predict criterionmedian_nsfrom iai instructions by workload class). Closes Finding 7 (criterion-vs-iai workload-class disagreement), completing the symmetry/boundary/hoisting rule-triple.
Added
-
Stage 1 PR 6 — PersistenceEngine Phase 0–2c (trait surface + file layer).
docs/V3_ENGINE_TRAIT_BOUNDARIES.md§2.6 amends steady-statesave_state/save_prefsto F5(b) sealing keys;shekyl-engine-coreaddsPersistenceError,OpenError::Persistence,ChangePasswordError,StateWrapKey, and thePersistenceEnginetrait module;shekyl-crypto-pqaddsseal_state_file_with_wrap_key_region_2;shekyl-engine-fileaddsMutex<WalletFileState>,rotate_passwordon&self, additivesave_state_with_wrap_key_region_2, andbase_path().WalletFiletrait impl andEngine<F>wiring follow in C3–C5. Design:docs/design/STAGE_1_PR_6_PERSISTENCE_ENGINE.md. -
RandomX v2 Track A Phase 2h adversarial-corpus methodology landed (
feat/randomx-v2-phase2h-impl, target PR; commits C1–C10 perdocs/completed/RANDOMX_V2_PHASE2H_PLAN.md§8). Closes the Phase 2g R7-D1/R7-D2/R7-D3/R7-D4 deferrals by replacing the V1-shaped class-heaviness grinding methodology (unreachable under V2's PROGRAM_SIZE = 384 σ-gaps) with the V2-substrate-anchored recipe-based corpus per R1-D1's three-category composition (Category 1 audit-anchored spec-silence enumeration; Category 2 coverage-metric attestation; Category 3 substrate-derived boundary values). The methodology ships first-class evaluator + declarative recipe DSL + canonical-output pinning + per-PR M5 mechanical citation-validation. T2/T6 originally inherited the Phase 2g runtime-test#[ignore]gating behind the (then-open) universal-across-inputscompute_hashdivergence FOLLOWUP; that FOLLOWUP closed ondevvia PR #79 (989610cac, 2026-05-26; root cause:RANDOMX_FLAG_V2missing atrandomx_create_vm), and the post-rebase substrate-close commits in this PR (see C11 below) lift the FOLLOWUP-gated attributes and workflow conditions.-
C1 canonical-output substrate + Pass-3 measurement constants. New
rust/shekyl-randomx-differential/src/adversarial_canonical_outputs.rslands the M1 canonical-output discipline for adversarial recipes plus the Pass-3 measurement-bundle constants (RUNNER_NOISE_MARGIN, per-class regression threshold,SAMPLE_BUDGET_PER_RECIPE). The Family-1 array (FAMILY_1_RECIPE_OUTPUTS) is regenerated at C5 alongside the recipe-corpus expansion and pinned viagen_canonical_outputs.rsFamily-1 branch. -
C2
PreparedCache::from_raw_for_testingaccessor. The R1-D2 close cache-level test-internals accessor lands onrust/shekyl-pow-randomx/src/prepared_cache.rsunder the existingtest-internalsfeature gate (R5-D1 carve-out shape; sole consumer isshekyl-randomx-differential). C-side symmetry via the pre-existingrandomx_get_cache_memoryextraction path keeps the production surface unchanged. A round-trip test asserts thatfrom_raw_for_testing(seedhash, bytes)of a freshderiveoutput's bytes reproduces the samePreparedCache(cache-bytes byte-identical; superscalar programs re-derived from the seedhash). -
C3 recipe types + first-class evaluator. New
rust/shekyl-randomx-differential/src/adversarial/module landingtypes.rs(BaseSeedhash,CacheRecipe,EvaluatedRecipe),interpreter.rs(declarative recipe →(seedhash, cache_bytes)evaluator with C-side base-cache derivation amortization),canonical.rs(base-cache bytes derivation helpers), and therecipes/submodule scaffold (spec_silence_anchors.rs,coverage_targets.rs,boundary_values.rs,dataset_item_extrema.rs) per R1-D3 close. -
C4 initial recipe corpus (8 recipes; Cat 1 + 3). The starter corpus lands with two Category 1 spec-silence anchors (
u128-high-half-cache-word-0,shift-mask-boundary-cache-word-1), three Category 3 boundary-value recipes (boundary-cache-first-byte,boundary-cache-last-byte,boundary-dataset-item-stride-first-edge), and three Category 3 dataset-item-extrema recipes (boundary-block-stride-second-block-base,boundary-block-stride-first-block-tail,boundary-line-stride-within-block). Each recipe's rationale field cites the specific V2 substrate (plan-doc section, configuration constant, or cache-implementation line range) per R1-D8's three-evidence-category structure. Coverage-targets module ships empty per R1-D1's coverage-tooling-reproducibility reopen criterion. -
C5
FAMILY_1_RECIPE_OUTPUTS+ Family-1 generator branch. The canonical-output array is regenerated alongside the C4 recipe expansion via thegen_canonical_outputs.rsFamily-1 generator branch; each entry pins the expected(seedhash, hash)against the C reference per M1 canonical-output discipline. -
C6
mode_adversarial_ratiobinary mode. Newrust/shekyl-randomx-differential/src/mode_adversarial_ratio.rsimplements the worst-case-ratio measurement mode replacing the §3.19 R7-D4 diagnostic-only branch atmain.rs's--mode=adversarial-ratiodispatch (renamed from--mode=worst-caseper the methodology shift). Measures Rust-to-C latency ratios over the recipe corpus against R1-D6's Claim 1 (per-recipe bound) + Claim 2 (corpus-median regression-tracking signal); emits structuredT6_OBSERVATION/T6_CLAIM_2_TRACKINGJSON for the regression-tracking dashboard harvester. -
C7 T2 + T6 reactivation with inherited deferral gating. Phase 2g §6 T2 (
adversarial_corpus_byte_equality) and T6 (worst_case_ratio) reactivate as new integration tests underrust/shekyl-randomx-differential/tests/. T2 asserts byte-equality between Rust and C across the recipe corpus; T6 invokesmode_adversarial_ratioand enforces Claim 1 + emits Claim 2 tracking signals. C7 cross-input diagnostics revealed that the Phase 2gcompute_hashFOLLOWUP's "large data sizes" framing was incomplete — the divergence surfaces universally across all tested seedhashes and data inputs, including 32-byte fixed inputs. T2/T6 inherit the same#[ignore]deferral as the Phase 2g runtime tests (T1/T3/T5/T7/T8/T16); the FOLLOWUPS amendment records the revised characterization. -
C8 CI workflow wiring (per-PR T2 + workflow_dispatch T6).
.github/workflows/randomx-v2-differential.ymlgains a per-PRcargo test --ignored T2step gated behindif: falseuntil the divergence FOLLOWUP closes. New.github/workflows/randomx-v2-adversarial-ratio.ymlworkflow_dispatch-only T6 workflow scaffolds the activation surface for measurement-mode runs (heavy enough to warrant a separate workflow gate per R1-D7 Sub-A close); sameif: falsegating mechanism. The activation surface is one-line workflow edits + one-line test-attribute edits when the FOLLOWUP closes. -
C9 M5 mechanical citation-validation script. New
scripts/ci/check_phase2h_citations.shimplements R2-D4's mechanical citation validation: parses reciperationalefields and validates per-category prefix (R1-D8 taxonomy invariant), cited plan-doc existence underdocs/design/, cited source-file existence underrust/shekyl-pow-randomx/src/(for*.rs) orexternal/randomx-v2/src/(for*.{c,cpp,h,hpp}), and cited line-number validity against the file's actual line count. Composes with M3 PR-template discipline (the procedural ceiling for semantic verification) per the T-A15 mitigation chain. Wired into the per-PRstructural-validatejob as a fifth gate step. Sub-second runtime on the C4 starter corpus (8 recipes); scales through R1-D1's 50–200 target. -
FOLLOWUPS reflow. The "Post-2g adversarial-corpus methodology + implementation" entry is annotated as closed by Phase 2h with cross-citations to C1–C9; the "Investigate
shekyl-pow-randomx::compute_hashdivergence" entry is amended to record the Phase 2h cross-input findings (universal-across-inputs scope correction; the 192-bytet16vector continues to pass only because it pins the seedhash + input combination at the known-good point). Both edits land in C10. -
C11 post-rebase substrate-close (V3.0 verifier-divergence FOLLOWUP closed by PR #79; this PR carries the operational close). PR #79 (
989610cac, 2026-05-26) closed the V3.0shekyl-pow-randomx::compute_hash-divergence-from-C-reference FOLLOWUP by passingRANDOMX_FLAG_V2atrandomx_create_vminCOracleSession::new. Following PR #79's merge, this PR rebased onto the post-#79devand landed four commits discharging the activation-surface contract that C7/C8 established:c71ce2413—RANDOMX_FLAG_V2extension toCOracleSession::from_raw_for_testing+ T17 round-trip backstop. Mirrors PR #79's fix at the testing constructor so substrate-overwrite-based session creation (the path T2/T6 exercise) is flag-equivalent toSelf::new. Newrust/shekyl-randomx-differential/tests/c_oracle_session_round_trip.rs(T17) asserts cache-byte SHA + hash parity between the two constructors for a fixed(seedhash, payload)pair; bracket-tested by temporary V1 revert to confirm it catches flag drift.6fc059e1e— lift T2#[ignore]+ workflowif: falsegating. Removes the#[ignore]attribute ont2_adversarial_corpus_byte_equality; rewrites the test module's "C7 close" docstring as past-tense "Active per-PR cadence (post-PR-#79 closure)" naming the substrate-anchored reopening criterion per21-reversion-clause-discipline.mdc; preserves the R1-D6 close Reframe 1 substrate-broken vs ignore-ladder distinction as the discipline's authoritative instance. Therandomx-v2-differential.ymlworkflow'sif: falsegate on the dedicated T2 step is lifted; the preceding defaultcargo teststep gains-- --skip t2_adversarial_corpus_byte_equalityso T2 runs exactly once per CI invocation (release-mode via the dedicated step) within R1-D6 close Reframe 2'sT2_PER_PR_BUDGET_MSbudget.1b1bda7df— lift T6 workflowif: falsegating + reframe T6 docs. Rewrites theworst_case_ratiomodule rustdoc's "C7 close" section as past-tense "Post-PR-#79 substrate note (FOLLOWUP closed)" and lifts therandomx-v2-adversarial-ratio.ymlworkflow step'sif: falsegate. T6 itself retains its test-layer#[ignore]attribute for runtime-cost reasons orthogonal to the FOLLOWUP (~40 s per recipe, outside per-PR cadence per R1-D6 close Reframe 2); the inline comment and step body record that the--ignoredflag persists on this basis.workflow_dispatchcadence is unchanged.72a4a9eed— reframe T16 docs as regression guard. Rewritesdivergence_triagemodule rustdoc from past-tense D1 substrate-triage investigation tool to forward-tense three-way (Rust ↔ C ↔ fixture) byte-equality regression guard at the canonical input. Preserves the D1 historical context (the three-hypothesis enumeration, outcome (A) confirmation, D2 → PR #79 diagnostic terminus); cites the substrate-anchored reopening criterion; cross-references T17 as the lighter-weight per-PR-cadence backstop. T16 stays#[ignore]-gated for runtime-cost reasons (256-MiB Argon2d-512 cache + ~10–30 s wall);#[ignore]reason text updated to surface the runtime-cost-only basis.
-
-
RandomX v2 Track A Phase 2g differential-test harness landed (
feat/randomx-v2-phase2g-impl, PR #75, merge commit33d22a83b, 2026-05-25). Final sub-PR of the Rust pure-software RandomX v2 verifier port perdocs/design/RANDOMX_V2_PLAN.md§"Track A — Phase 2" and the design plandocs/completed/RANDOMX_V2_PHASE2G_PLAN.md. Stack landed across the planned C0–C10 work commits plus follow-ups (two Copilot review rounds, two mechanicalrustfmtabsorptions, the R5-D2 plan-doc soft-fail refinement, and the R7 adversarial-corpus deferral cluster) landing a separate test-only artifact (rust/shekyl-randomx-differential) that links the Rust verifier (shekyl-pow-randomx) and the v2 fork's C reference (via the newrust/randomx-v2-sysbindings crate) and asserts byte equality across a corpus of(seedhash, data)inputs per Phase 0 §7's differential-harness-as-separate-artifact discipline (no dev-dependency edge fromshekyl-pow-randomx; the verifier crate'scargo teststill succeeds without the C library present). Preceded by the R5-D1 substrate-amendment PR #74 (merge93d1155bb) that landed thetest-internalsfeature gate onshekyl-pow-randomxexposingPreparedCache::cache_block_bytes_for_testing(gated bycfg(feature = "test-internals")), resolving the contradiction between R1-D14's cache-equivalence precondition requiring byte-level access to the Rust cache and §5.3.1's "zero new production surfaces" disposition. The feature is enabled exclusively by the harness crate; production builds see no new surface.-
Workspace deps +
randomx-v2-sysskeleton + CMake gate (commits8d8d4a109,013984118,e8d2eaccf,8b67d7775,e8dec6278,432162ddb; C0–C4). Newrust/randomx-v2-syscrate is the sole consumer of the v2 fork's C ABI per §5.3 R1-D14; itsbuild.rsresolvesRANDOMX_V2_INSTALL_DIR(preferred) or falls back to the in-tree install layout under<build-dir>/external/randomx-v2-installper the R5-D2-refined R4-D3 soft-fail discipline (commit429044cf8's plan-doc refinement landed the soft-fail-on-missing-library framing before C3's implementation). The harness craterust/shekyl-randomx-differentialis abinwith hand-rolled argument parsing (noclapdependency) and dispatches--mode=correctness,--mode=latency,--mode=concurrent, plus a deferred--mode=worst-casethat exits with an informative diagnostic pointing at the post-2g design round per §3.19 R7-D4.CMakeLists.txtgains theBUILD_RANDOMX_V2_DIFFERENTIAL_HARNESSoption (default OFF; flipped ON in CI) per Phase 0's miner-only-build-flag pattern. -
C5a corpus + canonical outputs (R6 cluster) (
5e00f457e,12884d945).corpus.rsbuilds the random(seedhash, data)corpus with a bimodal block-template-shaped[200 .. 600·1024)byte distribution per §3.16, andcanonical_outputs.rscarries 1024 pre-computed(seedhash, data) → hashcanonicals generated against the pinned C reference at the workspace-pinned fork SHA (per §5.4 R6 cluster's canonical-pinning discipline). Thegen_canonical_outputs.rstooling binary regenerates the canonicals as a separate operation outside the harness's runtime modes (skip-listed in.cargo/mutants.tomlper cargo-mutants skip-list discipline). -
R7 adversarial-corpus deferral (
c41a6c7f8,5598adea0). Plan-doc Round 7 reopens R1-D5 (adversarial seedhash corpus) and R1-D6 (u128/__int128_tedge-case data corpus) under two independent substrate findings: (i) the verifier-accessor gap (the class-heaviness grinding methodology requires atest-internals-gated opcode-stream accessor whose implementation would duplicatecompute_hash_innerunder a feature gate); (ii) the statistical-infeasibility gap (R1-D5's ≥40% per-class / ≥60% combined acceptance criteria were calibrated against V1's PROGRAM_SIZE = 256 and are unreachable by random grinding against V2's PROGRAM_SIZE = 384 with per-class σ-gaps from 6.8 (CACHE_MISS) to ~125 (CFROUND); fewer than 10⁻⁸ threshold-meeting candidates expected within any realistic compute budget). R7-D3 defers R1-D8 (worst-case timing test T6) by the same reasoning; R7-D4 routes the deferred work to the post-2g design round; R7-D5 carries the §6 T2 deferral. The post-2g round is queued indocs/FOLLOWUPS.md(V3.0 pre-genesis queue) as "Post-2g adversarial-corpus methodology + implementation." -
Cache-precondition + Rust/C oracle wrappers (
558eba59a).cache_precondition.rsderives the Rust and C caches from the same seedhash, compares them block-by-block via thetest-internals-gatedcache_block_bytes_for_testingaccessor, and emits an O(1)-block divergence window when they differ (window construction refactored under Copilot Round 2 below from the naïve O(N) re-iteration to the streaming form).rust_subject.rswrapsshekyl-pow-randomx'sPreparedCache+compute_hashsurface;c_oracle.rswrapsrandomx-v2-sys'srandomx_alloc_cache/randomx_init_cache/randomx_create_vm/randomx_calculate_hashlifecycle. The C oracle is!Send + !Sync(the C library's VM holds a per-thread JIT page); the harness pre-computes C-side reference hashes single-threadedly before spawning workers. -
Correctness + latency + concurrent modes (
71f5077d2,f25e6356f).mode_correctnesswalks the random corpus + canonical-output corpus, asserting Rust hash = C hash = canonical hash at every entry.mode_latencybenchmarks per-hash latency over the random corpus, reporting median/p95/max in(Rust, C)pairs with upper-median statistic for even-length samples (matching standard benchmark convention; doc-comment corrected under Copilot Round 2).mode_concurrentorchestrates multi-worker correctness assertion plus Linux-specific RSS-ceiling enforcement via/proc/self/statmsampling (RSS_CEILING_BYTES,RSS_TOLERANCE,RSS_SAMPLE_INTERVAL,RSS_STEADY_STATE_WARMUP_HASHESconstants documented inline); the smoke test surfaced the known V3.0compute_hashdivergence at large data inputs perdocs/FOLLOWUPS.md's V3.0 pre-genesis queue, which validates the harness's detection capability rather than indicating a Phase 2g regression. -
Failure output schema + invocation banner (
cadacf7a3,b63cd2592).failure_output.rsdefines the 11-field structured-JSON failure schema (M4/T11) emitted to stderr on Rust vs. C divergence; the schema carriesmode,seedhash,data_sha256,rust_hash,c_hash,canonical_hash(optional),rust_subject_version,c_oracle_version,fork_pin_sha,timestamp, plus the divergence-window blob fromcache_precondition.rswhen relevant.invocation_banner.rsemits the M4/T17 banner to stderr before any test output, recording mode, corpus sizes, seedhash count, fork pin, and thetest-internalsfeature- gate citation as the harness's authority-claim line. The rustfmt drift commitb63cd2592absorbed mechanical formatting changes from C8 to keep the C9 commit scope-clean. -
CI wiring + crate-invariant extension + mutants + PR template (
dd984d115). New.github/workflows/randomx-v2-differential.ymlruns thestructural-validatejob per-PR (build cleanliness, unit/integration tests, invariant-script coverage,cargo fmt+clippy) and themutantsjob on a staggered weekly cron; runtime modes (correctness, latency, concurrent end-to-end) are deferred behindcontinue-on-error: truewith a comment pointing at thedocs/FOLLOWUPS.mdV3.0compute_hashdivergence entry, and become merge-blocking once that V3.0 work lands. The workflow builds the C reference library out-of-band by invoking the submodule's own CMakeLists.txt (external/randomx-v2) directly with Ninja + ccache; this avoids pulling the parent project's C++ dependencies into the harness build path..github/workflows/build.ymland.github/workflows/codeql.ymlgain--exclude shekyl-randomx-differentialon their workspacecargo build/cargo testinvocations so the daemon CI matrix does not link against the C library it does not need;build.yml'slint-rust-debug-macrosstep gains a*/src/bin/*exclusion sogen_canonical_outputs.rs'sprintln!(legitimate CLI output) is not flagged.scripts/ci/check_randomx_crate_invariants.shis extended to scanrandomx-v2-sysandshekyl-randomx-differentialfor Pattern A (OnceCell/OnceLock/Lazyimports) and Pattern B (column-0staticdeclarations);shekyl-randomx-differentialis also scanned for Pattern C (FFI exports), whilerandomx-v2-sysis exempt from Pattern C (it is the FFI consumer crate). Newrust/shekyl-randomx-differential/tests/crate_invariants.rsintegration tests T13 (script coverage), T14 (randomx-v2-syssole-consumer property — verified by walkingcargo metadatato confirm no other workspace member depends onrandomx-v2-sys), and T15 (randomx-v2-syssignature-audit pin — matches the bindings file's fork-pin SHA against the submodule HEAD atexternal/randomx-v2). New.cargo/mutants.tomlconfigurescargo-mutantswithtimeout_multiplier = 5.0and skip-globs for tooling binaries (src/bin/**) and canonical outputs (canonical_outputs.rs) per the skip-list discipline. New.github/pull_request_template.mdcarries the three-line discipline checklist for harness / verifier modifications (amendment-cite, audit-line-range cite, harness-pass-as-evidence with audit co-citation). -
Copilot Round 1 — inline review responses (
3ac2d777f). Four findings against the implementation diff: (1)build.rswarning message uses<build-dir>/external/randomx-v2-installas the documented fallback path string; (2)RANDOMX_V2_PHASE2G_PLAN.md§3992's embeddedbuild.rsexample matches the live wording; (3) therandomx-v2-sysbindings file gains the fork-pin signature comment with the exact submodule SHA for T15 to assert against; (4) cross-citation provenance lines for cache-precondition and Rust/C oracle hand-off discipline. -
Copilot Round 2 — post-implementation findings (
d60186fa9,90a536219). Five substantive findings: (1)parse_seedhash_hexnow explicitly rejects uppercase A–F with a load-bearing diagnostic (--seedhash: character {i}: uppercase hex rejected; use lowercase per Seedhash::Display), pinning the lowercase convention shared with the verifier'sSeedhash::Displayvia a newparse_seedhash_hex_rejects_uppercaseregression test; (2)mode_latency::median_p95_max's doc-comment now statessamples[n/2]is the upper-median for even-length samples (the prior wording said "lower median" but the implementation has always been upper-median; fixing the doc rather than the code preserves existing test expectations); (3)RustSubjectSession::seedhash's doc-comment recommends*session.seedhash()(the idiomatic deref for aCopytype) over.clone(); (4)failure_output::timestamp_increases_across_constructionsis replaced bytimestamp_is_nonzero_and_recent, which asserts the timestamp is non-zero and within a plausible epoch window (post-2020 / pre-2200) instead of relying onSystemTime::now()monotonicity through athread::sleep(1s)(the prior test was flaky under clock adjustments); (5)cache_precondition::build_divergence_windowis refactored from O(N) re-iteration of the cache block stream to O(1)-block streaming construction by passing thecurrent_block, buffering theprev_block, and accepting theremainder_iteras a mutable iterator — the doc-comment claim ("at-most-two 1-KiB blocks") was true of the window contents but not of the cost to construct it, and the refactor brings cost in line with the doc with four new unit tests covering interior, crosses-backwards, crosses-forwards, and at-cache-start windows. Stale README +canonical_outputs.rsdoc-comments that referenced C5b / C6 boundaries and a placeholder error (pre-R7-D4 framing) are updated to reflect the post-2g deferral and the completed C4–C10 sequence.
§9 / Phase 2g gate confirmation (HEAD at PR #75 merge =
33d22a83b): Formatcargo fmt --all -- --check✓; Lintcargo clippy -p shekyl-randomx-differential -p randomx-v2-sys --all-targets -- -D warnings✓; Testcargo test -p shekyl-randomx-differential -p randomx-v2-sys --release✓ (unit + integration including T13/T14/T15); Crate-invariant gatescripts/ci/check_randomx_crate_invariants.sh✓ (extended scan scope acrossshekyl-pow-randomx,randomx-v2-sys,shekyl-randomx-differential); FPU unsafe grepscripts/ci/check_randomx_fpu_rounding.sh✓ (inherited from 2d); workspace testcargo test --workspace --exclude shekyl-randomx-differential✓ on the daemon CI matrix (the harness crate's tests are exercised on therandomx-v2-differential.ymlmatrix that has the C library available). Four substantive Copilot review threads on PR #75 resolved with provenance citations to the commits that addressed each finding; 14 outdated threads were auto-greyed by GitHub as the surrounding code shifted.Deferrals named, with reopening criteria per
21-reversion-clause-discipline.mdc:mode_worst_case+ adversarial-corpus methodology (R7-D1/R7-D2/R7-D3/R7-D4). Deferred to a post-2g design round; tracked indocs/FOLLOWUPS.mdV3.0 pre-genesis queue. Reopens via the design round's plan-doc landing (the methodology must be V2-substrate-anchored; class- heaviness grinding is V1-shaped and statistically infeasible against V2 substrate). Phase 2g's--mode=worst-caseflag is reachable but emits the deferral diagnostic referencing the FOLLOWUPS entry.compute_hashdivergence at large data inputs. Surfaced by Phase 2g C7's first end-to-end smoke test against a 387,581-byte data input in R1-D4's bimodal upper-half distribution; cache-equivalence precondition passes (caches byte-identical); divergence is incompute_hash's VM path. Closed 2026-05-26 by substrate-triage onchore/randomx-v2-c-oracle-flag-v2— root cause was the harness's C oracle and canonical-output generator both passingRANDOMX_FLAG_DEFAULT(v1, PROGRAM_SIZE = 256) torandomx_create_vmagainst a Rust verifier implementing v2 (PROGRAM_SIZE = 384). The Rust verifier was correct throughout. Fix: exposeRANDOMX_FLAG_V2 = 128inrandomx-v2-syswith cache- vs-VM flag-split docs (cache memory is V2-flag-invariant perexternal/randomx-v2/src/randomx.cpp:79's(JIT | LARGE_PAGES)mask atrandomx_alloc_cache; onlyrandomx_create_vmhonors the V2 bit); switch the two callsites atc_oracle.rs+gen_canonical_outputs.rs; regenerateCANONICAL_RANDOM_HASHESunder the v2 flag (v1-c5a-nightly-1024→v2-flag-nightly-1024;CANONICAL_CACHE_SHASunchanged, as predicted by the mask). End-to-end--mode=correctnessre-runs the nightly corpus (1024 random pairs / 32 seedhashes) with three-way agreement (Rust ≡ C ≡ canonical, exit 0). Full closure record + lessons-into-substrate dispositions indocs/FOLLOWUPS.md"Recently resolved (audit trail)" section; post-mortem with the missed-altitude finding indocs/completed/RANDOMX_V2_PHASE2G_PLAN.md. The harness's detection of this divergence (and the diagnostic-triage test that bisected it) is end-to-end validation of its M4 detection capability against a real substrate gap.- Per-hash latency CI gate (≤3.0× ratio). Phase 2g
produces the harness binary that the Phase 3a per-PR CI
mechanism (
RANDOMX_V2_RUST.md§8) consumes; the gate activates when Phase 3a's FFI shim lands. Phase 2g itself runsmode_latencyinformationally, not as a CI gate.
-
-
RandomX v2 Track A Phase 2f implementation core landed (
feat/randomx-v2-phase2f-impl, 2026-05-23). Implementsdocs/completed/RANDOMX_V2_PHASE2F_PLAN.mdRound 2 + Round 3 dispositions onrust/shekyl-pow-randomxin five commits versus the §8 Round 3 ceiling of six (commit 5 omitted per Branch A — see prediction-vs-measured reconciliation below).-
Seedhashnewtype +PreparedCachebundle (e687cf68b). Closes the Phase 2c-inherited consensus-correctness footgun wherecompute_hash(&Cache, &[u8; 32], &[u8])carried the cache and the seedhash as separate arguments — a caller passing the wrong cache for a given seedhash got a wrong hash. Newsrc/seedhash.rsintroducespub struct Seedhash([u8; 32])withfrom_bytes/as_bytes/Display(lowercase hex per §1.1 Round 2 + post-closure pin #1) replacing every&[u8; 32]seedhash parameter site. Newsrc/prepared_cache.rsbundlesCache + SeedhashwithPreparedCache::deriveas the single public construction path;Cachetransitionspub → pub(crate)per §1.1 Round 2.compute_hashsignature transitions from(&Cache, &[u8; 32], &[u8])to(&PreparedCache, &[u8]). Atomic codebase sweep updates every in-crate call site (Phase 2c/2d tests,vm.rstests,cache.rstests, benches) per §3.1 Round 2 sweep-discipline. Per16-architectural-inheritance.mdc's pre-genesis discount- cost-benefit-defer-to-later anti-pattern, the substrate correction lands at Phase 2F rather than V3.x.
-
CacheStoretwo-slot type (31aa0ff9d). Newsrc/cache_store.rsimplements §3.1 Round 2's frozen API:lookup,lookup_or_derive,set_canonical. Internal sync-shape per §3.1 Round 2: per-slotRwLock<Option<Arc<PreparedCache>>>(canonical non-evictable + transient displace-on-publish),Mutex<HashMap<Seedhash, Shared<DerivationFuture>>>for in-flight derivation deduplication with cleanup-on-publish per §3.1 Round 2 (closes F3 thundering-herd attack on novel-seedhash + F4 unbounded HashMap growth). Eleven unit tests T-CS-1 through T-CS-11 per §6.1 Round 3 cover the state-transition table (3-seedhash interleave attack; cold-start; advance-promotes-and-demotes), in-flight dedup (T-CS-7), cleanup-on-publish white-box (T-CS-8), concurrent-determinism property (T-CS-9), and type-shape compile-time checks (T-CS-10/11). Caller hand-off Arc-lifetime discipline note in the rustdoc per §4 F2 disposition. -
Crate-invariant grep gate (
68086d99c). Newscripts/ci/check_randomx_crate_invariants.shenforces §3.6 R1-E1 patterns A/B/C: pattern A bans imports ofonce_cell/lazy_static/OnceLock/LazyLock(stricter than module-level-static-only by rejecting at the import); pattern B bans column-0staticdeclarations (function-local statics live inside fn bodies and are indented;constitems are a different keyword); pattern C bans#[no_mangle],#[unsafe(no_mangle)],#[export_name,#[unsafe(export_name, andextern "C" fndefinition form anchored at column 0 modulo attribute indentation (so thelib.rsrustdoc citation of the discipline does not collide with the gate). The rustfmt-rely-chain note per §3.6 Round 3 records that the column-0 anchor is robust against function-local statics if and only ifcargo fmt --checkis a CI gate (which it is per Phase 2c R0-D6). New.github/workflows/build.ymlstep sibling to the FPU-rounding step. Newtests/crate_invariants.rscargo-test wrapper makes the gate runnable viacargo testfor local pre-PR checks. Verification at HEAD: zero hits acrossrust/shekyl-pow-randomx/src/. -
Cfg-gated
VmStatePool+ four-bench A/B harness (3121b726d). Newsrc/vm_pool.rsgated by#[cfg(any(test, feature = "internal-pool-bench"))]per §3.3 Round 3.VmStatePool::new(capacity: usize)is a runtime parameter per §3.5 R1-D5 Round 3 (panics in non-test builds without the feature flag, enforcing explicit configuration);Mutex<Vec<VmState>>storage with capacity cap;acquire()returns aVmStateGuardwhoseDropreturns the instance to the pool if capacity allows.vm.rsfactorscompute_hashinto a thin wrapper overpub(crate) compute_hash_inner(&mut VmState, ...)so the production no-pool path (VmState::new()per call) and the cfg-gated pool path share one implementation;compute_hash_innerzerosstate.fprcon entry (the onlyVmStatefield with observable carry-over across pooled reuse, since CFROUND writesfprcduringexecute_programbut does not reset it at boundaries). Seven unit tests T-PL-1 through T-PL-7 cover acquire/release, capacity bounds, and equivalence to the no-pool path. Newbenches/per_call_alloc.rsmeasures B-2 (scratchpad zero-init) + B-3 (register-file/program alloc);compute_hash_alloc.rsextends with B-pool-off (always on) + B-pool-on (under--features internal-pool-bench). The cfg-gated approach closes the Round 1 circular-sequencing problem ("can't bench the pool without implementing the pool"). -
Phase 2f A/B bench measurement — Branch A (
a37aac054).BENCH_RESULTS.mdrecords the §3.4 R1-D4 Round 3 disposition empirically: B-pool-off 304.44 ms median (CI [303.14, 305.96]), B-pool-on 303.72 ms median (CI [302.71, 304.88]), B-2 48.6 µs, B-3 81.7 ns. Component-floor sum (B-2 + B-3) ≈ 48.7 µs caps the achievable pool savings; the point-estimate A/B delta of 720 µs is statistically indistinguishable from zero (95% CIs overlap heavily) and structurally bounded above by the component-floor cap (so the 720 µs is run-to-run measurement noise, not pool benefit). Disposition: Branch A — achievable savings is below the §3.4 Round 3 50 µs threshold; pooling produces no production-relevant benefit on this hardware class. The cfg-gatedVmStatePoolstays in source as a bench-only artifact; §8 commit 5 (cfg-gate flip to default-on) is omitted. Phase 3a's FFI shim sees the unchanged productioncompute_hashbody.Prediction-vs-measured reconciliation per §8 Round 3 discipline: prediction A held. The §8 plan-doc recorded two competing predictions (Branch C plausible per PR-66's hundreds-of-µs full-pipeline alloc cost; Branch A plausible per modern allocators amortizing 2 MiB zero-init to tens of µs). B-2 measured at 48.6 µs on this hardware (mmap-backed glibc on kernel 6.12, large-page-aware allocator) is consistent with the Branch A framing; PR-66's per-call full-pipeline cost (~300 ms) is dispatch-loop dominated (2048 iterations × 8 chains × per-iter AES + scratchpad RW + dataset reads), not allocation-specific. Pooling can amortize only allocation cost; the component-floor cap is structurally below Branch B/C thresholds. Reopening criterion per
21-reversion-clause-discipline.mdc: a hardware class with substantially different allocator behavior, a Phase 3a FFI fanout pattern not captured by the single-thread bench, or a Phase 2g per-hash-latency surface on production-target hardware that yields A/B delta ≥ 100 µs reopens the disposition via a fresh §3.4 pin in the relevant plan-doc.
§9 gate confirmation (HEAD =
a37aac054): Formatcargo fmt -p shekyl-pow-randomx -- --check✓; Lintcargo clippy -p shekyl-pow-randomx --all-targets -- -D warnings✓ (feature off); Lintcargo clippy -p shekyl-pow-randomx --all-targets --features internal-pool-bench -- -D warnings✓ (feature on); Testcargo test -p shekyl-pow-randomx --release -- --test-threads=1✓ (117 passed, 2 ignored — T6/T7 superseded by Phase 2d's T16; 4 crate_invariants integration tests passed; 1 perf placeholder ignored — T17 per-hash latency Phase 2g deliverable); Doccargo doc -p shekyl-pow-randomx --no-deps✓; FPU unsafe grepscripts/ci/check_randomx_fpu_rounding.sh✓ (inherited from 2d); Crate-invariant grepscripts/ci/check_randomx_crate_invariants.sh✓ (new gate landed in commit 3); Bench delta informational —compute_hash_alloc::per_call307.42 ms vs. Phase 2d baseline 303.60 ms (+1.27%; under §9's ±10% regression-trigger threshold). -
-
RandomX v2 Track A Phase 2f — review-cycle fix (PR #72 Copilot finding NF8, fifth pass) (
feat/randomx-v2-phase2f-impl, 2026-05-24). One finding surfaced by the fifth Copilot review pass against84d5ba72aand addressed in-place. NF8 is a documentation-vs-implementation discrepancy in the cargo-test wrapper's rustdoc claim about its regression-detection role; the architectural disposition is unchanged, and the fix tightens the substrate by expanding scan scope rather than weakening the documented claim.-
NF8 —
tests/crate_invariants.rsrustdoc claimed an active regression-detection mechanism the scan scope did not realize. The cargo-test wrapper preamble described "would-match" comments insidetests/crate_invariants.rsas a positive regression-detection surface — if a future patch un-anchored one of the patterns, the in-comment citations would start matching and the gate would fire. The mechanism is real only if the file is in scan scope; pre-NF8, the script'sCRATE_SRC="rust/shekyl-pow-randomx/src"constant excluded the test directory entirely, so the regression-detection claim was a fiction.Fix. Expanded
CRATE_SRCfrom a single path to an array("rust/shekyl-pow-randomx/src", "rust/shekyl-pow-randomx/tests", "rust/shekyl-pow-randomx/benches")inscripts/ci/check_randomx_crate_invariants.sh. The recursivegreparm gains--include='*.rs'to skip C/C++ reference-vector generators attests/vectors/reference/<primitive>/_generator/*.{c,cpp}(legitimate column-0staticdeclarations under C/C++ semantics; out of scope for a Rust-targeted invariant gate). The per-fileawkmulti-line scanner already iterated viafind ... -name '*.rs'and picked up the change automatically. The verifier crate'stests/andbenches/directories carry zero column-0 banned shapes today, so the scope expansion is mechanical with no false-positive surface.Verification. Plant-revert positive-side tests across both new scope arms:
use std::sync::OnceLock;plant intests/→ gate FAILS; multi-line bypass plant inbenches/→ gate FAILS; column-0staticplant intests/→ gate FAILS;pub extern "C" fnplant inbenches/→ gate FAILS; baseline → gate PASSES. Regression-detection reality check: simulated un-anchoring of Pattern A (drop the^from the regex) fires the gate from multiple in-scope sources: (1) thetests/crate_invariants.rs:146-147would-match comments, exactly as the rustdoc claim describes; (2) legitimate function-local indenteduse std::sync::OnceLock;statements inside#[cfg(test)] mod tests { }blocks atsrc/cache_store.rs:596,src/vm.rs:2767, andsrc/vm.rs:3342, which the column-0 anchor was protecting and would un-protect under regression. The mechanism is doubly real with the expanded scope.Documentation. Updated the
tests/crate_invariants.rspreamble to explicitly cite the NF8 fix and the now-real regression-detection mechanism, naming the scope expansion (src/→src/+tests/+benches/) as the substrate change.
-
-
RandomX v2 Track A Phase 2f — review-cycle fix (PR #72 Copilot finding NF7, fourth pass) (
feat/randomx-v2-phase2f-impl, 2026-05-24). One finding surfaced by the fourth Copilot review pass against321b89edband addressed in-place. NF7 is a CI-gate completeness defect against §3.6 R1-E1 Pattern A; the architectural disposition is unchanged. Plan-doc round history records the fix as audit trail per21-reversion-clause-discipline.mdc's post-closure-pin discipline.-
NF7 —
PATTERN_RUNTIME_STATEregex bypassed by rustfmt-default multi-line grouped imports. §3.6 Round 3 froze Pattern A as a column-0-anchoredgrep -Eregex matching banned identifiers (once_cell/lazy_static/OnceLock/LazyLock) anywhere on the same line as ausestatement. The single-line grouped formuse std::sync::{Arc, OnceLock};is correctly caught (OnceLockappears on the same line as the column-0use); the rustfmt-default multi-line grouped form, where theuseopener carries no banned identifier and the indented identifier lines fail the column-0 anchor, bypasses entirely:use std::sync::{\n Arc,\n OnceLock,\n};matches none of the per-line patterns. rustfmt's defaultimports_granularity = "Preserve"accepts the multi-line form and acargo fmt-mediated rewrite from the single-line form is a one-max_width-overflow away (or a futureimports_granularity = "Crate"config change), so the bypass is reachable in production-discipline workflows. The Round 3 R1-E1 Pattern A invariant is "no module-level imports of these types," not "no module-level imports in a specific formatting style"; the gate's stated property and its mechanical coverage diverged.Fix. Added a per-file POSIX
awkscanner that complements the single-linegrepregex inscripts/ci/check_randomx_crate_invariants.sh. The scanner triggers on any column-0usestatement opening an unclosed brace block, accumulates subsequent lines tracking nested-brace depth via balanced{/}counts (souse foo::{bar::{baz, OnceLock}}spread across lines is handled correctly), and on depth-zero closure scans the accumulated buffer against the same banned-token alternation(once_cell|lazy_static|OnceLock|LazyLock). The two arms (single-linegrep, multi-lineawk) jointly enforce Pattern A regardless of rustfmt grouping style.Verification. Plant-revert positive-side tests: synthesized multi-line bypass file → gate FAILS (exit 1) with the banned token cited; nested-brace multi-line bypass → gate FAILS; clean multi-line
use(no banned tokens) → gate PASSES; baseline crate state → gate PASSES. The cargo-test wrappertests/crate_invariants.rsinvokes the unmodified bash entry point so the test surface continues to assert exit-zero discipline; the documentation comment was extended with a multi-line bypass would-match example mirroring the single-line / Pattern B / Pattern C citations so the regression-detection mechanism is auditable for the new arm too.
-
-
RandomX v2 Track A Phase 2f — review-cycle fixes (PR #72 Copilot findings NF3 + NF4 + NF5 + NF6, second pass) (
feat/randomx-v2-phase2f-impl, 2026-05-24). Four findings surfaced by the second Copilot review pass againstd4d88bdc1and addressed in-place. Three (NF3, NF4, NF5) are documentation drift inherited from Phase 2c phrasing or from PR #72 NF2's prior commit; one (NF6) is an implementation defect on the in-flight-derivation rendezvous architecturally specified in Round 2 but under-specified at the panic-unwind boundary. None reopen Round 2 / Round 3 / post-closure-pin architectural dispositions. Plan-doc round history records the fixes as audit trail per21-reversion-clause-discipline.mdc's post-closure-pin discipline.-
NF3 —
lib.rscrate-level rustdoc still frameddispatch_instructionas having a NOP body ("Phase 2d replaces the dispatch body in-place per §5.1.1 of the plan doc"). The bullet was correct as of Phase 2c's PR landing; Phase 2d (PR #70 →dev) replaced the body in-place with the real table-driven per-opcode dispatch and added the T16 reference vector for end-to-end real-dispatch parity, but the rustdoc was not updated to record the now-landed state. Fix: rephrased the dispatch bullet to reflect both the Phase-2c-landed NOP and the Phase-2d-landed real dispatch, named T16 as the current end-to-end consensus-parity gate, and reframed "Subsequent sub-PRs" to "Sub-PR ladder" with explicit(landed)/(planned)markers on 2d / 2f / 2g. Also updated the bench bullet'scompute_hash_allocdescription to record the post-2d baseline alongside the 2c stub-NOP number. -
NF4 —
benches/compute_hash_alloc.rsrustdoc framed the per-call cost composition under the stub-NOP body ("Under the stub-NOPdispatch_instructionbody, the per-call cost is dominated by …"; "8 × 2048 stub-NOP iteration-loop bodies … no per-instruction work since dispatch is NOP"). Same drift as NF3. Fix: rephrased the cost-composition section to describe the pipeline neutrally (per-iteration dispatch is a step in the iteration body; Phase 2c measured under stub-NOP, Phase 2d adds per- instruction work at that step), and updated the file-header summary + thePER_CALL_SAMPLE_SIZErationale to record the post-2d baseline. -
NF5 —
Cargo.tomlinternal-pool-benchfeature comment claimedVmStatePoolis apub(crate)type whoseDefaultpanics in non-test builds. First half is wrong post-PR-72 F2 (the type is#[doc(hidden)] pubso the criterion bench inbenches/compute_hash_alloc.rs, a separate cargo target, can nameVmStatePool::newandcompute_hash_with_poolacross the crate boundary;pub(crate)would forbid that). Second half is incomplete (the panic is gated specifically bycfg(all(not(test), feature = "internal-pool-bench")); the no-feature production build never compilesvm_poolat all). Fix: rewrote the comment to record the actual visibility (#[doc(hidden)] pub), the actual gating shape (whole module behind#[cfg(any(test, feature = "internal-pool-bench"))]), and the actual panic discipline (panic only whenDefault::default()is called outside#[cfg(test)], enforcing §3.5 R1-D5 explicit-capacity at Phase 3a). -
NF6 —
DerivationSlot::wait_for_resultdeadlocks on leader thread panic inrust/shekyl-pow-randomx/src/cache_store.rs. Round 2 §3.1 pinned the in-flight-derivation rendezvous asMutex<HashMap<Seedhash, Shared<DerivationFuture>>>at the architecture level; the implementation used aMutex<Option<Arc<PreparedCache>>>per-slot rendezvous with aCondvarfor follower wake-up. Pre-fix, if the leader thread panicked insidePreparedCache::derive(e.g., allocation failure during the 256 MiB Argon2d-512 fill),slot.publishnever ran, theinnermutex stayed atNone, theCondvarwas never broadcast, and (a) every follower already parked oncv.waitblocked forever; (b) thein_flightHashMap entry was never removed, so subsequent callers for the same seedhash acquiredin_flight.lock(), found the orphaned slot, became followers of the dead leader, and joined the deadlock cascade. Production builds setpanic = "abort"fordev/release(process aborts before any of this matters), butcargo testalways builds withpanic = "unwind"per the test-harness contract — so a test exercising the failure path would hang rather than fail with a diagnostic message. Fix: replacedMutex<Option<Arc<PreparedCache>>>withMutex<DerivationOutcome>(Pending/Published(Arc<PreparedCache>)/LeaderAborted); addedLeaderGuard<'cs>that owns the leader's slot Arc + a borrow of the in-flight mutex + asuccess: boolflag, withDropthat always removes the in-flight entry (cleanup-on-publish + cleanup-on-panic in one path) and conditionally broadcastsLeaderAbortedviapublish_aborted_if_pendingwhenmark_successwas never called;wait_for_resultnow panics with a diagnostic message onLeaderAbortedinstead of looping on the condvar. Thelookup_or_deriveleader branch wraps itsPreparedCache::derive+slot.publish+transient.writesequence in the guard's scope with amark_successflag flip after the transient write — on success the guard's drop is a no-op for the abort broadcast and the in-flight removal becomes the cleanup-on-publish step that previously lived inline. Test added — T-CS-13cachestore_leader_abort_wakes_followers_and_cleans_in_flight: white-box test using a standaloneMutex<HashMap<Seedhash, Arc<DerivationSlot>>>mock so the test runs without paying the ~150–200 msPreparedCache::derivecost; spawns a follower thread onslot.wait_for_result(), drops aLeaderGuardwithoutmark_success, and asserts (1) in-flight entry removed; (2) slot inLeaderAborted; (3) follower panic-propagated rather than hanging. With the pre-fix slot type the test would hang indefinitely on assertion (3); with the fix it passes deterministically.
-
-
RandomX v2 Track A Phase 2f — review-cycle fixes (PR #72 Copilot findings NF1 + NF2) (
feat/randomx-v2-phase2f-impl, 2026-05-24). Two implementation defects surfaced by the post-fix Copilot review against7b5302ee9and addressed in-place. Both are localized refinements at the implementation layer; the Round 2 / Round 3 / post-closure-pin architectural dispositions remain unchanged. Plan-doc round history records the fixes as audit trail per21-reversion-clause-discipline.mdc's post-closure-pin discipline.-
NF1 —
PATTERN_FFI_EXPORTblind spot inscripts/ci/check_randomx_crate_invariants.sh. The Round 3 §3.6 R1-E1 pattern Cextern "C" fnarm anchoredexternas the first non-whitespace token;pub extern "C" fn,pub(crate) extern "C" fn,unsafe extern "C" fn, andpub unsafe extern "C" fnall bypassed the gate. Without#[no_mangle]they are not C-callable today, but the gate's stated purpose is to forbid the export-intent shape independent of#[no_mangle]so that stepwise FFI-export drift (addpub extern "C" fnfirst, attach#[no_mangle]later) fires the gate at the first commit rather than only the second. Fix: extend the regex to allow optionalpub/pub(crate)/pub(super)/pub(in path)visibility prefix and optionalunsafekeyword beforeextern, mirroring pattern A's prefix coverage (closes the same shape of blind spot the F1 fix closed for pattern A). Verified against eleven positive shape variants (all match) and eight negative shapes (extern "C" { fn bar(); }import blocks, rustdoc citations,use std::ffi::CStr;,fn extern_c() {}, etc. — all skip). -
NF2 —
CacheStore::lookuplinearizability race on transient→canonical promotion inrust/shekyl-pow-randomx/src/cache_store.rs. The Round 2 §3.1 per-slotRwLock<Option<Arc<PreparedCache>>>shape was specified at the architecture level; the implementation acquired and released each slot's read guard sequentially across the comparison sequence. A concurrentset_canonicalcould promote an entry from transient to canonical betweenlookup's two slot inspections, causinglookup(&S)to observe canonical=Some(prior) → released → transient=Some(prior_canonical) → returnNonedespite the requested entry being live in the canonical slot the entire time. Soft consequence: alookup_or_deriveconsumer falls through to a ~150–200 ms Argon2d-512 re-derivation that should have been a slot hit; violates the documented "few hundred nanoseconds" cost-model. Fix: acquire both slot read guards before the comparison sequence and hold them across both inspections. The canonical-then-transient acquisition order matchesset_canonical's canonical-write-then-transient- write order, so there is no deadlock cycle. Updatedlookuprustdoc with explicit linearizability + lock-ordering discussion; updated theCacheStorestruct's# Synchronization shaperustdoc to record the global lock-ordering invariant ("every method acquiring both slot locks acquires them canonical-then-transient, regardless of read-vs-write mode"). New test T-CS-12cachestore_lookup_linearizable_under_canonical_swap: seeds the store with two pre-derived prepared caches in distinct slots, runs alternatingset_canonical(p_a) / set_canonical(p_b)calls in one thread while a second thread tightly pollslookup(&seedhash_a) / lookup(&seedhash_b)for 2,000 iterations and asserts both never returnNone(both entries are live in some slot at every observable moment, so a linearizablelookupmust always find them). With the buggy implementation the test fails probabilistically; with the fix it passes deterministically because a concurrentset_canonicalcannot interleave between the two slot reads.
-
-
RandomX v2 Track A Phase 2f — plan-doc front-matter staleness corrections (
chore/randomx-v2-phase2f-plan, 2026-05-23). Addresses PR #71 review findings (4 items, all header drift between the scaffold-as-of-Round-0 framing and the post-Round-3- post-closure-pin actual state). Per
91-documentation-after-plans.mdcaudit-trail discipline, the scaffold-original framing is preserved in place and the superseding state is marked inline; this preserves the discipline's evolution as auditable rather than flattening history.
(1) §Status block reframed. The opening paragraph previously described the doc as a "Round-0 substrate capture" with Round 1 as the next deliverable. Reframed to "Round 3 closed + post-closure pins + post-closure pin refinements" with a brief inventory of what each round / post-closure amendment landed; readers wanting current state read the status block, readers wanting evolution read §11 Round history. Two new front-matter paragraphs (Reading order + Original scaffold framing preserved) make the audit-trail discipline explicit.
(2) §front-matter "No 2c or 2d public surface changes in 2f" claim flagged as superseded. The original claim was correct as of the scaffold but is false post-Round-2 (which intentionally amends the inherited 2d public surface to close the consensus-correctness footgun the 2d signature carried). The original claim is preserved as audit trail; a Round-2-supersedes paragraph immediately follows it citing
16-architectural-inheritance.mdc's pre-genesis discount rationale for landing the substrate correction in plan-doc Round 2 rather than V3.x.(3) §Scope envelope
≤600 net-new-linestarget flagged as superseded. Round 2'sSeedhashnewtype +PreparedCache+ atomic Seedhash sweep added ~200 net lines per §5.2's line-count table (~800 net-new lines total; ~50–150 additional if the R1-D3 cfg-gated pool flips to production). The scaffold-original ≤600 figure is preserved as audit trail; the load-bearing budget is §5.2's per-item table.(4) CHANGELOG post-closure-pin-refinements entry honestly framed. The previous closing paragraph said "No structural changes to Round 2 / Round 3 / post-closure-pin dispositions; only narrower specifications" — but item (1) in the same bullet is a narrow structural change to a post-closure pin (reversing the pin-#2 framing on
cache_ref()in favor of an explicit accessor). Reworded to "No changes to Round 2 / Round 3 dispositions. Item (1) above is a narrow structural change to a post-closure pin …; the other five items are narrower specifications of pre-existing pins." This is the same shape as the prediction-vs-measured discipline added in commitcb9dc0dd2's §8 framing — make the divergence visible rather than letting it slip past.No changes to plan-doc dispositions (Round 1 / Round 2 / Round 3) or to post-closure pins (item-#1 reversal already landed in commit
cb9dc0dd2); these are framing-staleness corrections only. No CI / code changes; PR #71 remains doc-only. - post-closure-pin actual state). Per
-
RandomX v2 Track A Phase 2f — post-closure pin refinements (
chore/randomx-v2-phase2f-plan, 2026-05-23). Companion commit to the post-closure substrate-completeness pins (docs/completed/RANDOMX_V2_PHASE2F_PLAN.md). Six narrow refinements, each tightening a post-closure pin against a substrate observation. Per21-reversion-clause-discipline.mdc's post-closure-pin discipline; not a Round 4.(1) §1.1 pin #2 reversed: explicit
pub(crate) cache_ref()accessor onPreparedCache. The original post-closure-pin disposition ("no accessor;compute_hashprivate-field-extracts internally") is replaced. The explicit accessor:impl PreparedCache { pub(crate) fn cache_ref(&self) -> &Cache; }documents the established reach-through shape from
&PreparedCacheto&Cachefor the dispatch loop's in-crate consumption, and prevents a future contributor from re-exposingCache's API onPreparedCache(e.g., addingprepared.derive_item(...)as a convenience). Per05-system-thinking.mdc's "specification first, code second" discipline, the explicit accessor is the documented contract. Tests continue to usepub(crate) Cache::from_rawper Phase 2c R0-D6.(2) §4 Round 4 placeholder explicit close. F1–F7 is the threat-model close for Phase 2F. The §4 "Round 4 placeholder" is preserved per
91-documentation-after-plans.mdcaudit-trail discipline so the Round 1 framing remains visible; it is not a queued deliverable. Future findings (impl-PR pre-flight; Phase 2g differential-harness surface) reopen the threat model via substrate-change criteria, not via sequential numbering — there is no Round 4 hanging on this plan-doc.(3) §8 commit-5 prediction-vs-measured discipline. The impl-PR description must include both the predicted branch (from §8 — Branch C plausible per PR-66's hundreds-of-µs; Branch A plausible per modern-allocator tens-of-µs) and the measured branch (from commit 4's
BENCH_RESULTS.md) with explicit reconciliation: "prediction held" or "prediction wrong because <substrate-anchored reason>." Mirrors the mp-correction discipline (Phase 2c PR-65); makes the divergence visible rather than letting it slip past as an undocumented surprise.(4) §10.3 layering note: shim absorbs (g)-style discipline the verifier rejected. The shim-side scoped-closure discipline ("borrow for the duration of one hash computation" rather than "store the handle in async state") absorbs the API constraint Round 2 §3.1 rejected at the verifier layer. The (g)-option scoped-closure pattern was rejected at the verifier's Rust-side API for being too constraining on consumers; the same pattern is acceptable on the shim side because the shim's consumers are FFI callers who already navigate explicit allocate/use/destroy lifecycle. The responsibility moved layers (verifier → shim) rather than disappeared. Future readers see that the (g)-rejection at the verifier and the (g)-style absorption at the shim are the same discipline, applied at the layer where it doesn't constrain the wrong consumers.
(5) §10.4 cfg-gated-additions principle +
TraceSinkscope. Cfg-gated test-infrastructure additions are not "tweaks to upstream RandomX" — they are Rust-language affordances for tooling. The "don't tweak upstream unless we need to" discipline applies to consensus-affecting behavior (production-build code paths influencing hash output, cache derivation, dispatch loop, validation rules), not to bisection convenience (test-only paths gated by#[cfg(any(test, feature = ...))]). The line is consensus-affecting, not Shekyl-specific.TraceSinktrait scope pinned: the trait's surface design lives with Phase 2g's plan-doc, not with the verifier's public API; the trait stays scoped to the differential harness's consumption. Do not promoteTraceSinkto a public surface — apub trait TraceSinkexposed from the verifier crate would create an API contract that constrains future verifier-internal refactors.(6) §10.5 Phase 2g audit posture against the C reference. Three-leg framing for the "Shekyl's verifier is canonical RandomX v2" claim: (1) spec-faithful implementation discipline (Phases 2b/2c/2d/2f); (2) C-reference audit where the spec is silent (Argon2d salt; SuperscalarHash program-generation seed; JIT-vs-interpreter dispatch; etc.); (3) differential-harness corpus testing (Phase 2g). The load-bearing claim is leg 1; leg 3 is the backstop. Corpus testing on a finite set of inputs does not establish behavior on the unbounded set of all inputs, but it does increase confidence that leg 1's discipline was applied correctly. For an external auditor asking "how do you know this is right?", the answer is "we implemented to spec, audited against the C reference where the spec is silent, and test against the C reference's outputs as a backstop" — not "we test against the C reference" alone. Phase 2g's plan-doc inherits this framing.
No changes to Round 2 / Round 3 dispositions. Item (1) above is a narrow structural change to a post-closure pin (the pin-#2 framing on
cache_ref()is reversed in favor of an explicitpub(crate) fn cache_ref(&self) -> &Cacheaccessor); the other five items are narrower specifications of pre-existing pins. Reopen criteria are substrate-anchored per the named items; none anticipated. The chore branch holds Round 2 + Round 3 + post-closure pins + post-closure pin refinements; no push without separate authorization. -
RandomX v2 Track A Phase 2f — post-closure substrate- completeness pins (
chore/randomx-v2-phase2f-plan, 2026-05-23). Companion commit to Round 2 + Round 3 ofdocs/completed/RANDOMX_V2_PHASE2F_PLAN.md. Per21-reversion-clause-discipline.mdc("an under-specification surfaced post-closure does not reopen the round it belonged to but is named explicitly as a post-closure pin"; not a Round 4). Six items, all narrow specifications of what Round 2 / Round 3 already pinned at the architectural level.(1) §1.1
Displayimpl framing corrected. Round 2's framing claimed "lowercase hex for logging consistency with Phase 2c's existing seedhash-formatting conventions"; verification at HEAD (rg -i seedhashacrossrust/shekyl-pow-randomx/src/) found zerotracing::/log::/format!/ hex-rendering sites. Phase 2c does not establish a seedhash-formatting convention because Phase 2c does not log seedhashes. The lowercase-hex disposition stands (matcheshex::encodeand the cryptographic-output convention); the framing is corrected to cite the convention directly rather than the unsupported "Phase 2c consistency" claim. TheDisplayimpl is for downstream consumers (FFI shim, daemon-side logging, test diagnostics) — the verifier crate itself does not log.(2) §1.1 dispatch-loop /
Cachevisibility pin. The Round 2Cache: pub → pub(crate)transition does not affect the dispatch loop'svm.rs::execute_onesignature (in-cratecache: &Cache— the visibility transition is crate-boundary-only).compute_hashextracts&prepared.cachevia private-field access internally (both in the same crate); nocache_ref()accessor onPreparedCacheis added. Tests that need directCacheconstruction continue to usepub(crate) Cache::from_rawper the Phase 2c R0-D6 tests-use-the-actual-API discipline.(3) §1.1
PreparedCacheequality pin.PreparedCachedoes not derivePartialEq/Eq. Two equality semantics are needed; each is served by a more specific primitive thanPartialEqonPreparedCache: seedhash equality (slot.seedhash() == lookup_keyviaSeedhash's derivedPartialEq) for CacheStore slot indexing, andArc::ptr_eqfor identity comparisons in tests T-CS-5/7/9. DerivingPartialEqwould either be structural value-equality (compare 256 MiB cache bytes; no caller wants this) or delegating equality (compare seedhash only; conflates "same seedhash" with "samePreparedCacheinstance"). Both shapes are wrong; the absence of the impl forces consumers to use the right primitive at the call site.(4) §8 commit-5 empirical-conditional + branch-prediction pin. Commit 5 (cfg-gate flip per §3.4 Round 3) is conditional on the §6.3 A/B bench delta measured at commit 4. The empirical answer does not exist at plan-doc-close time. Branch C (≥ 100 µs delta) plausible per PR-66's hundreds-of-µs per-call alloc cost; Branch A (< 50 µs) plausible per
Box::<[u8]>::new_zeroed_slice(2 MiB)typical tens-of-µs cost on modern allocators. Both predictions are consistent with the §3.4 Round 3 disposition; the impl-PR's commit-4 bench result resolves the prediction with substrate-anchored data, and the impl-PR description names the branch taken so reviewers can spot surprises against the prediction.(5) §10.3 Phase 3a FFI shim discipline. The verifier crate provides the Rust-side type system (
Seedhash,PreparedCache,Arc<PreparedCache>,CacheStore); the Phase 3a FFI shim owns the C-side opaque-handle shape,Seedhash::from_bytes(*ptr)construction at the boundary,Arc<PreparedCache>lifecycle across the boundary (daemon-facing API discourages long-lived holds per the caller hand-off Arc-lifetime discipline), andVmStatePool::new(capacity)runtime-parameter derivation fromdev-tip daemon threadpool source at Phase 3a wire-up time. Phase 3a's plan inherits the disposition rather than re-litigating it.(6) §10.4 Phase 2g
compute_hash_with_tracepre-pin. Pre-pinned the option for a#[cfg(any(test, feature = "differential-trace"))] pub fn compute_hash_with_trace(prepared, data, trace_sink) -> [u8; 32]test-infrastructure entry point for differential- harness bisection (per-iteration register-file snapshots; not a public-API addition; production build pays no overhead). The C reference does not expose this; the Rust verifier exposes it under#[cfg(...)]so the verifier's "stay minimal; don't add Shekyl-specific divergence" discipline is preserved. Phase 2g's plan inherits the option; uses it iff bisection workflow requires per-iteration trace visibility (otherwise the API is not added).No structural changes to Round 2 / Round 3 dispositions; only narrower specifications. The reopen criteria for the post-closure pins are substrate-anchored per the named items; none are anticipated. The chore branch holds Round 2
- Round 3 + post-closure pins; no push without separate authorization.
-
RandomX v2 Track A Phase 2f — Round 3 design (refinement bundle) (
chore/randomx-v2-phase2f-plan, 2026-05-23). Companion commit to Round 2's architectural keystone fordocs/completed/RANDOMX_V2_PHASE2F_PLAN.md. Round 3 hardens the dispositions Round 2 left for follow-up. §3.3 R1-D3 reframed to cfg-gated A/B approach — pool body implemented behind#[cfg(any(test, feature = "internal-pool-bench"))]regardless of R1-D4 outcome; bench harness measures both paths directly (B-pool-offalways;B-pool-onwhen feature enabled). Closes the Round 1 circular-sequencing problem. §3.4 R1-D4 dissolved into R1-D3 — the threshold (Decision #7's 100 µs) is a binding source; the Round 1 task is mechanical application of the threshold to the A/B delta. The cfg-gated pool stays in source as the bench-only artifact on Branch A; flips to default-on on Branch C. §3.5 R1-D5 refined to runtime-configurable capacity —VmStatePool::new(capacity: usize)constructed from the Phase 3a FFI shim's threadpool-source-derived value; the default constructor panics in non-test builds to enforce explicit configuration. The Round 1 R1-D5 survey methodology stands; the substrate-anchored value flows in at runtime rather than baking into apub(crate) constat compile time. Closes the Round 1 staleness footgun where a Phase-2F-baked-in capacity could mismatch the Phase 3a daemon configuration. §3.6 R1-E1 rustfmt-rely-chain note added — the column-0 anchor is robust against function-local statics if and only ifcargo fmt --checkis a CI gate (which it is, per Phase 2c R0-D6); the rely-chain is named explicitly in the §3.6 Round 3 sub-block. §4 threat-model F1–F7 enumeration (Round 4 placeholder retained as audit trail; Round 3 supersedes inline): F1 cache-derivation DoS amplification (closed by canonical non-eviction); F2 Arc-holding memory exhaustion (bounded by capacity-2 + caller-side discipline note); F3 thundering herd on novel-seedhash (closed by in-flight dedup); F4 unbounded HashMap growth (closed by cleanup-on-publish); F5 concurrent-derivation race (covered by determinism property + dedup); F6 mutex contention amplification (addressed byRwLock-per-slot + capacity-2-no-sharding); F7 cache-derivation cost asymmetry (out of scope; upstream daemon-side validation discipline). Caller hand-off Arc-lifetime discipline note added to theCacheStorerustdoc (consumers should holdArc<PreparedCache>only for the duration of the immediate hash computation; long-lived holds extend cache memory residency beyondCacheStore's bound; daemon-side discipline, not aCacheStoreenforcement). §6.1 test plan reshaped to Round-2-typed pre/post table (T-CS-1..11; in-flight-dedup test T-CS-7; cleanup-on-publish white-box test T-CS-8; concurrent-determinism property test T-CS-9; type-shape compile-time checks T-CS-10/11). §6.3 bench harness reshaped toB-pool-off/B-pool-onA/B per §3.3 Round 3; component-floor benches retained as cross-check;BENCH_RESULTS.mdrecords the A/B delta + Branch disposition. §8 commit table reshaped to 6-commit Round-2-+-Round-3 shape:Seedhash+PreparedCachetype sweep (1);CacheStore(2); invariant grep gate (3); cfg-gated pool + A/B bench (4 — always); cfg-gate flip (5 — Branch C only); plan close +CHANGELOG(6). The Round 1 5-commit shape's Branch-A-omits-commit-4 / Branch-B-defers-commit-4 / Branch-C-includes-commit-4 trichotomy collapses to "is commit 5 included" rather than "does commit 4 exist." §3.1 (g) rejection inline atCacheStorerustdoc — the Arc-holding memory exhaustion finding is named in the public rustdoc so future readers asking "wouldn't this be simpler without two slots?" find the adversarial finding rather than re-proposing the shape. Adversarial-pass-precedent named — the (g) → (b) Round 2 reversal is the second documented instance of "adversarial pass reverses an aesthetically-preferred choice" (first: LWMA-1 time-source local-time-only over peer-time- derived). The recurrence justifies promotion to26-sub-pr-design-discipline.mdcas a sibling discipline- promotion PR (chore/sub-pr-design-discipline-adversarial-pass). No outstanding Round-N+1 follow-ups queued from Round 3; Round 4 (if any future round opens) reopens via the §3.1 / §3.3 / §3.5 substrate-change reopening criteria, not via sequential numbering. -
RandomX v2 Track A Phase 2f — Round 2 design (architectural reframe) (
chore/randomx-v2-phase2f-plan, 2026-05-23). Architectural keystone fordocs/completed/RANDOMX_V2_PHASE2F_PLAN.mdRound 2; supersedes the Round 1 dispositions where the Phase 2c freeze inherited a consensus-correctness footgun the type system can close at zero cost. Round 1 dispositions stand as audit trail. Round 3 follow-up commit (queued; same chore branch) refines the dispositions Round 2 doesn't directly touch (R1-D3 / R1-D4 / R1-D5 / R1-E1 refinements; threat-model F1–F7 enumeration; commit-table reshape). The "no push" framing is the right shape: both Round 2 and Round 3 commits stay on the chore branch until both are ready, then push together. Merging the keystone alone would create a transient state where the architecture is reframed but the synchronization shape isn't pinned, the in-flight deduplication isn't specified, etc.- §1.1 substrate correction (load-bearing). Phase 2c's
compute_hash(&Cache, &[u8; 32], &[u8]) -> [u8; 32]shape carries the cache and the seedhash as separate arguments; a caller passing the wrong cache for a given seedhash gets a wrong hash, which is fine for chain integrity (network rejects) but is a footgun the type system can close at zero cost. Round 2 introduces:pub struct Seedhash(/* private [u8; 32] */)— newtype replacing[u8; 32]aliases for seedhashes. DerivesCopy / Clone / Debug / Eq / Hash / PartialEqplus aDisplayimpl (hex). Representation is private (accessor- mediatedfrom_bytes/as_bytes); pre-genesis the representation is fixed but post-genesis the accessor shape lets the representation evolve without churning every call site. Future-proofs typed-provenance refinements (e.g.,ValidatedSeedhash(Seedhash)for "this seedhash came from a validated block header" vs arbitrary user input).pub struct PreparedCachebundlingCache + Seedhash. The bundle is constructed viapub fn PreparedCache::derive(Seedhash) -> PreparedCache;pub fn PreparedCache::seedhash(&self) -> &Seedhash. Wrong-cache-for-seedhash is unrepresentable: there is no public path to construct aPreparedCachewhosecachewasn't derived from itsseedhash.compute_hashsignature amended:pub fn compute_hash(&PreparedCache, &[u8]) -> [u8; 32]. No separate seedhash parameter. The bundling propagates through to the FFI surface (per §10.2) — the C++ caller passes an opaquePreparedCache*and never sees a seedhash on the compute path.Cachetransitionspub → pub(crate). Thepub(crate)Cache::derive(&Seedhash) -> Cacheis the implementation primitive but not the public API. Test access is preserved viasrc/*.rs#mod testsdiscipline (Phase 2c R0-D6);Cacherustdoc carries a pointer toPreparedCacheas the public construction path.- Two-layer derivation discipline.
Cache::derive(&Seedhash) -> Cacheis the pure transform (testable in isolation, preserves Phase 2c's T1 spec-vector test infrastructure);PreparedCache::derive(Seedhash) -> PreparedCacheis the bundling wrapper (three lines of body; callsCache::deriveand pairs the result with the seedhash). Two-layer chosen over one-layer for test-infrastructure continuity (T1 spec vectors assertCache::deriveagainst canonical reference output; reusing those tests against the internalCache::deriveis cheaper than rewiring them throughPreparedCache.cache()). - Seedhash-newtype sweep is atomic with introduction.
Every site that currently passes
&[u8; 32]for a seedhash (Cache::derive, PreparedCache::derive, CacheStore::*, FFI shim's seedhash constructor, every test that constructs a literal seedhash) updates to&Seedhash/Seedhashin the same impl-PR commit as the newtype's landing. Not as a follow-up. Otherwise the codebase has a transitional period where some sites use&[u8; 32]and some use&Seedhash, which is exactly the drift the newtype prevents.
- §3.1 R1-D1 + R1-D2 merged disposition. Round 1's
single-axis question (CacheStore API shape) is layered into
a three-axis question post-
PreparedCache:- Axis 1 (consensus-correctness): where the cache+seedhash
binding lives. (i) separate parameters [Phase 2c freeze;
Round 2 rejects]; (ii)
PreparedCachebundle [Round 2 picks]. Type-enforces the binding. - Axis 2 (QoS): canonical-protection shape. (a)
transparent memo with
pin/unpin[Round 1 rejected]; (b) explicit two-slot type [Round 1 picked, Round 2 reaffirms; now operates onArc<PreparedCache>]; (c) type-stratified composition [Round 1 rejected as over-provisioning at capacity 2]. Once Axis 1 type-enforces consensus correctness, Axis 2's stakes drop from "structurally enforce consensus correctness" to "structurally enforce QoS sticky property" — (b) is still right but the argument is lower-stakes. - Axis 3 (whether CacheStore exists): (d) no-CacheStore
[Round 2 rejects]; (e) thin amortizing layer [Round 2
partial pick]; (f) full canonical-protection structure
[Round 1 (b) shape, pre-PreparedCache; Round 2 partial
pick]. Round 2 picks (e)/(f) hybrid: thin amortizing
layer with explicit two-slot canonical-protection on top
of
Arc<PreparedCache>. Rejects (d) and its (g) refinement (no-CacheStore + per-consumer in-flight deduplication map) for the Arc-holding memory exhaustion attack: without a capacity-N cap at a CacheStore layer, an attacker who induces concurrent novel-seedhash lookups gets the daemon to hold manyArc<PreparedCache>clones whose total memory footprint scales with seedhashes-seen- in-attack-window. Capacity-N at the CacheStore layer bounds this; consumer-side discipline does not.
- Axis 1 (consensus-correctness): where the cache+seedhash
binding lives. (i) separate parameters [Phase 2c freeze;
Round 2 rejects]; (ii)
- Frozen
CacheStorepublic surface (Round 2 supersedes Round 1's code-block).pub fn new() -> CacheStore,pub fn lookup(&self, seedhash: &Seedhash) -> Option<Arc<PreparedCache>>,pub fn lookup_or_derive(&self, seedhash: &Seedhash) -> Arc<PreparedCache>,pub fn set_canonical(&self, prepared: Arc<PreparedCache>). The Round 1insertmethod is removed; its function (publish a derived cache into the transient slot) is subsumed bylookup_or_derive's on-completion publication. No caller-driven insert path remains. Separating fast-path (lookup, no derivation) from slow-path (lookup_or_derive, may derive) lets a hot-path validator that knows it should hit canonical calllookupand treatNoneas an error signal rather than transparently paying ~150 ms of unexpected derivation cost. - In-flight derivation deduplication shape pinned (Round 2
new vs. Round 1 surface).
Mutex<HashMap<Seedhash, Shared<DerivationFuture>>>insideCacheStore. Concurrentlookup_or_derivecalls for the same novel seedhash share one in-flight derivation; only one Argon2d fill runs. Cleanup-on-publish drops the in-flight-map entry immediately on derivation completion — load-bearing for memory-boundedness (without it, the in-flightHashMapgrows unboundedly under sustained novel-seedhash attack). Closes the thundering-herd attack surface that applies regardless of Axis 2/3 selection. TheShared<DerivationFuture>is thefutures::future::Sharedadapter (or a sync alternative built onstd::sync::Arc<std::sync::Mutex<DerivationState>>+ condvar; the choice is a Round 3 sub-detail per dependency discipline). - Synchronization shape pinned at Round 2. Per-slot
RwLock<Option<Arc<PreparedCache>>>for canonical and transient (lookups are hot path; writes are rare; concurrent readers proceed; canonical reads don't block transient writes and vice versa).Mutex<HashMap>for in-flight (writes/reads balanced; short critical section;RwLockwould not buy meaningful concurrency). Sharding rejected at capacity-2 (no contention to reduce when there are only two slots). Pre-genesis discount makes synchronization changes bounded; reopen via Round X+1 if Phase 3a profiling surfacesRwLock-not-helping orMutex-contention. - 11-row state-transition table refreshed. Pre/post states
typed against
Arc<PreparedCache>(rather than(seedhash, Arc<Cache>)pairs);insert→lookup_or_derivesubstitution; in-flight-dedup concurrent row added. The Round 1 table is preserved as audit trail in §3.2 and superseded by §3.1 Round 2 disposition. Substantive transitions are unchanged from Round 1 (canonical non-evictable; transient displace-on-publish; advance promotes-and-demotes); the table refresh is the typing. - Capacity-2 reopen criterion sharpened. Round 1: "a
second Rust caller of
CacheStorelands that needs concurrent canonicality across multiple chains." Round 2: "a named real consumer surfaces a sustained operational pattern where 2 caches isn't sufficient and the operator demonstrably has to choose between paying re-derivation cost or extending theCacheStore." Substrate-anchored event = consumer's call-site grep evidence + measurement showing the cost. - Transparent-memo framing retired. Parent-plan
RANDOMX_V2_PLAN.mdDecision #6 wording ("transparent memo with capacity-2 LRU andpin()API") belonged to the rejected Option (a). Option (b) is honest about the two-slot structure. Wording amendment queued as precursor PRchore/randomx-v2-plan-decision6-amendmentthat lands before the Phase 2F implementation PR opens. Bounded scope; one-file change. Precedent: Phase 2c F4-absorbed parent-plan rescope. - Reversion clause expanded to three independent axes.
Axis 1 (PreparedCache bundling): reopens if a deserialization
use case surfaces, FFI-shim audit reveals C-ABI cost, or V4
PQC architectural choice requires PQC-authenticated cache
metadata. Axis 2 (canonical-protection-in-(b)): carries
forward Round 1's three reopening criteria unchanged. Axis
3 (CacheStore exists): reopens if Phase 3a profiling shows
Mutex<HashMap>is the bottleneck or if architectural- inheritance audit reveals consumer-side discipline can be made structural (e.g., scoped handle pattern). - §5 implementation hand-off contract updated. Frozen items per Round 2 (compute_hash signature, Seedhash newtype, PreparedCache, Cache visibility, CacheStore API, eviction-policy table, sweep-atomic-with-introduction) plus Round 1 freeze items unchanged where Round 2 doesn't apply. In-scope artifacts table grows by 5 rows (Seedhash newtype, PreparedCache, Cache visibility transition, compute_hash signature update, atomic Seedhash sweep within the crate). Out-of-scope re-emphasized: FFI shim updates land at Phase 3a; parent-plan Decision #6 wording lands at the precursor PR. Total ~800 lines net-new (was ~600 pre-Round-2; +~200 from PreparedCache + Seedhash + sweep + larger CacheStore + larger test matrix).
- §10 forward path updated. 2g and 3a inherit the
compute_hash(&PreparedCache, &[u8]) -> [u8; 32]shape (not the Phase 2c-frozencompute_hash(&Cache, &[u8; 32], &[u8])). Phase 3a's FFI shim constructsSeedhash::from_bytes(*ptr)from the C-ABI's*const [u8; 32]and passesArc<PreparedCache>as opaque pointers. §10.2 PQC migration space note: the verifier crate's API is PQC-orthogonal by construction (Seedhash is 32 bytes; PreparedCache uses classical Argon2d-derived state; compute_hash produces 32 bytes). PQC architectural choices (V4-lattice signatures, hybrid- PQC verification pipeline shape) land at Phase 3a's shim layer, not in the verifier. Future contributors should not attempt to "PQC-prepare" the verifier crate's API. - Plan-doc edits (this commit): §1.1 rewritten with
scaffold-and-Round-1 freeze preserved as audit trail and
Round 2 amendment as load-bearing supersession; §3.1 Round 2
disposition added (covers merged R1-D1+R1-D2, three-axis
options matrix, frozen API code-block, in-flight dedup
shape, synchronization shape, 11-row state-transition table,
capacity-2 reopen criterion, transparent-memo retirement,
three-axis reversion clause); §3.2 Round 2 marker added
(R1-D2 merged into R1-D1); §5.1 frozen-by-this-doc list
updated with Round 2 supersession markers; §5.2 in-scope
artifacts grows by 5 rows; §5.3 out-of-scope additions
(FFI shim updates; parent-plan Decision #6 wording); §10
forward path updated for
PreparedCacheshape + §10.1 precursor PR queue + §10.2 PQC migration space note; §11 Round history gains Round 2 row.
- §1.1 substrate correction (load-bearing). Phase 2c's
-
RandomX v2 Track A Phase 2f — Round 1 design closure (
chore/randomx-v2-phase2f-plan, 2026-05-23). Closes Round 1 ofdocs/completed/RANDOMX_V2_PHASE2F_PLAN.mdsix decision points after the 2026-05-23 scaffold (f3da9f093):- R1-D1 (CacheStore API surface): picks option (b) explicit
two-slot type with
new/lookup/insert/set_canonical. Rejects (a) transparent memo on the basis that the F1 sticky- canonical defense depends on the caller never letting the canonical seedhash be evicted by routinelookupordering; folding the canonical-vs-transient distinction into the type structurally enforces what (a) would push to caller discipline. Rejects (c) type-stratified composition as over-provisioning at capacity 2. Internal sync viastd::sync::Mutexonly —lruis not a workspace dependency (verified atrust/Cargo.toml),parking_lotis transitive-only via the existingcriterion/tokiopaths, and a 2-slot store does not justify pulling either into the direct dep set per17-dependency-discipline.mdc. Frozen API code-block pinned inRANDOMX_V2_PHASE2F_PLAN.md§3.1 Round 1 disposition; revert criteria (substrate-anchored): a second Rust caller emerges needing concurrent canonicality across multiple chains; Decision #5 (FFI-locality) reverses; Phase 3a FFI shim survey surfaces concurrency requirements incompatible with the two- slot shape. - R1-D2 (eviction policy + interleave matrix): policy falls
out of (b) — canonical slot is non-evictable, transient slot is
displace-on-insert,
set_canonicaladvance promotes-from- transient + demotes-prior. Cold-start window (noset_canonicalyet called) leaves both slots subject to attacker churn; bounded to daemon startup and handled by the FFI shim's discipline (no fallback policy inCacheStoreitself). 11-row pre/post state- transition table coveringRANDOMX_V2_PHASE2C_PLAN.md§5.11.7 #1 3-seedhash interleave attack, the 2-seedhash cold-start degenerate case, the canonical-advance demotion, and the no-op cases. Reversion criteria tied to R1-D1. - R1-D3 (bench methodology for per-call
VmStateisolation): picks option (b) Component method. Rejects (a) Diff method because the natural amortization shape requires either promotingVmStatetopubor adding apub fn compute_hash_with_statehelper — both contradictRANDOMX_V2_PHASE2F_PLAN.md§1.1 (VmStateispub(crate)) and Decision #7 (no publicVmPool). Rejects (c) Population method per the scaffold's sequencing- cycle note. Component sum:Box::<[u8]>::new_zeroed_slice(2 MiB)median + synthetic register-file zero-init median (the bench does not consume the productionVmStatenewtype to keep visibility clean); the sum is a floor on per-call alloc cost. Bench code-block pinned in §3.3 Round 1 disposition. Revert criteria: floor lands in [50, 100) µs ambiguity band per R1-D4 (would require option (a)'s tighter measurement); Decision #7 reverses; empirical evidence shows the component decomposition systematically underestimates by > 30%. - R1-D4 (pool decision threshold + reversion clause): confirms
the 100 µs threshold per
RANDOMX_V2_PLAN.mdline 240. Three- band decision rule on the R1-D3 component-floor median: < 50 µs → no pool (Branch A); [50, 100) µs → escalate to impl-PR pre- flight per R1-D3 reversion-clause #1 (Branch B); ≥ 100 µs → pool insidecompute_hash, no publicVmPool, capacity from R1-D5 (Branch C). Reversion clauses for the no-pool path enumerate substrate-anchored triggers (allocator regression; scratchpad-size change at consensus level; runtime-architecture mismatch). - R1-D5 (daemon parallel-verification fanout survey
methodology): audit-against-actual-code per
16-architectural-inheritance.mdcagainstsrc/cryptonote_core/blockchain.cpp,src/cryptonote_core/tx_pool.cpp,src/cryptonote_core/cryptonote_tx_utils.cpp,src/cryptonote_core/tx_pqc_verify.cpp, andsrc/common/threadpool.{h,cpp}atdevtip =fb21909ff. Substrate correction vs. prompt: only one parallelcompute_hashcall site exists at HEAD — alt-chain branch validation'sblock_longhash_workerviatools::threadpool::getInstanceForCompute(), capped bym_max_prepare_blocks_threads(default 4). Mempool tx verification does not callcompute_hashin parallel; the prompt's two-source assumption was incorrect. Pool capacity formula:min(threadpool::getInstanceForCompute().get_max_concurrency(), m_max_prepare_blocks_threads) + 1reserve. Reversion criteria: a future PR introducestools::threadpool+compute_hashintx_pool.cpp/cryptonote_tx_utils.cpp/tx_pqc_verify.cpp;m_max_prepare_blocks_threadsdefault change; Phase 3a FFI shim survey reveals new concurrent consumer. - R1-E1 (CI grep pattern set + permitted exceptions): three
patterns. Pattern A bans imports of
once_cell/lazy_static/OnceLock/LazyLock(stricter than module-level-static-only — eliminates the disambiguation between module-level and function- local usage by rejecting the import; the crate provably needs none of these). Pattern B bans column-0staticdeclarations (function-local statics are inside fn bodies and indented per rustfmt;constitems are a different keyword and not matched). Pattern C bans#[no_mangle],#[unsafe(no_mangle)],#[export_name,#[unsafe(export_name, andextern "C" fndefinition form (extern "C" { fn foo(); }import blocks consuming external FFI surfaces are not matched since they requirefninside the brace block, not after"C"). New scriptscripts/ci/check_randomx_crate_invariants.shmodeled onscripts/ci/check_randomx_fpu_rounding.shfrom Phase 2d. CI integration: new.github/workflows/build.ymlstepenforce RandomX crate-level isolation invariantssibling to the FPU step. Substrate finding at verification: pattern C's first draft (anywhere-on-line match) collided with the existinglib.rsrustdoc at lines 31–32, which legitimately cites the forbidden tokens as part of the documented discipline. Disposition: anchor pattern C at column 0 with optional leading whitespace (matches code attributes indented inside fn bodies; excludes rustdoc lines, which start with//!). Verified clean baseline at HEAD post-fix acrossrust/shekyl-pow-randomx/src/. Per-pattern reversion criteria: stdlib evolution producing pattern A successor primitives; genuine large-immutable-shared- state need motivating pattern B relaxation; Decision #5 (FFI- locality) reversal motivating pattern C reopen. - Plan-doc edits: §3.1–§3.6 each gain a Round 1 disposition
sub-block (preserving the scaffold's framing as audit-trail per
91-documentation-after-plans.mdc); §5 superseded by frozen- surface contract (5.1 frozen items + 5.2 in-scope artifact table + 5.3 out-of-scope re-emphasis); §6 superseded by the 7-row CacheStore unit-test table + 4-row CI-invariant table + 4-row bench-harness table; §8 superseded by the 5-commit table with R1-D4 three-branch (A/B/C) conditional on commit 4; §11 Round history gains Round 1 row. - Implementation deferred to
feat/randomx-v2-phase2f-implafter Round-N closure (target 4–6 rounds, matching Phase 2c/2d cadence). Round 4 specifically does the threat-model addenda pass against scaffold §4. Branch policy per06-branching.mdc— chore branch is short-lived; commit not pushed pending user authorization.
- R1-D1 (CacheStore API surface): picks option (b) explicit
two-slot type with
-
RandomX v2 Track A Phase 2d implementation core landed (
feat/randomx-v2-phase2d, 2026-05-22). Implementsdocs/completed/RANDOMX_V2_PHASE2D_PLAN.md§3.5/§3.7 R1–R6 decisions onrust/shekyl-pow-randomx:-
F128 newtype + integer helpers (
bd7cea464). Promotes the Phase 2ctype F128 = [f64; 2]alias to a#[derive(Copy)]struct F128([f64; 2])carryingadd/sub/mul/div/sqrt/swap- FSCAL XOR-mask methods per §3.2 R1-D2. Adds private
sign_extend_i32_to_i64,load64/store64,rotr/rotlhelpers matchinginstructions_portable.cppsemantics. Quarantines the FPU rounding-mode write in a newfpu_roundingmodule (x86_64_mm_setcsr, aarch64mrs/msr fpcrinline asm) per §3.1 R1-D1 / §3.7 R6-D1. Replaces the Phase 2c stub-NOPdispatch_instructionbody with a densematchondecode_instruction_type(opcode)covering all 28 executable opcodes plus CFROUND + CBRANCH, driven by a PC loop withProgram.cbranch_tablestatic metadata populated duringinit_program(§3.6 R2-D1/R2-D2/R2-D3). Promotessuperscalar::{mulh, smulh_u64, randomx_reciprocal}topub(crate)for IMULH/IMUL_RCP dispatch.
- FSCAL XOR-mask methods per §3.2 R1-D2. Adds private
-
T16 real-dispatch hash vector + CI grep + FPU reset (
26fc49d6c). Addstests/vectors/reference/vm/t16_vm_compute_hash_real.binemitted by the pinned fork's interpreted-light VM underRANDOMX_FLAG_V2(§6.2 T16 / §8 commit 5b); Phase 2c's stub-NOP T6/T7/T8 vectors are marked#[ignore]because real dispatch mutates the register file before later iterations, so the end-to-end T16 byte-equality supersedes them. Addsscripts/ci/check_randomx_fpu_rounding.shwired into the existing Lint job to enforce the §9 FPU primitive scope (_mm_setcsrexactly once on x86_64, twoasm!(calls on aarch64, nofesetround). Restores FPU rounding mode to round-to-nearest atcompute_hashentry/exit and around theexecute_programdeterminism tests so the process-wide MXCSR/FPCR mutation from CFROUND does not leak across tests. Phase 2d post-dispatchcompute_hash_alloc::per_callmeasures 303.60 ms median (+2.6% vs. Phase 2c stub-NOP baseline of 296.00 ms), under the §9 ±10% regression-trigger threshold (rust/shekyl-pow-randomx/BENCH_RESULTS.md). -
T9–T15 single-opcode reference vectors (Phase 2d §6.2 / §8 commit 5a). Adds a new
tests/vectors/reference/_generator/phase2d/generator (gen.cpp + Makefile + README) driving the pinned fork'srandomx::BytecodeMachine::compileInstruction+executeInstructionagainst fabricated single instructions over a canonicalNativeRegisterFile+ scratchpad fixture, and emits seven new.bin+.meta.txtreference vectors undertests/vectors/reference/vm/: T9 integer smoke (IADD_RS / IMULH_R / IROR_R / ISTORE), T10 FP smoke under RN (FADD_R / FMUL_R / FDIV_M / FSQRT_R), T11–T14 the 9-FP-opcode matrix under MXCSR modes 0..3, and T15 CFROUND throttle (throttled + 2 unthrottled cases, paired withrx_get_rounding_mode()u32). Rust spec-vector tests insrc/vm.rs#mod testsdrivedispatch_instructionagainst the same canonical fixture and assert byte-equality ({t9,t10,t11,t12,t13,t14,t15}_vm_..._matches_fork_reference). Phase 2d's implementation core (PRs landed prior) plus T9–T15 per-opcode coverage and T16 end-to-end hash now exhaust theexecuteInstructiondispatch surface against the v2 fork pinaaafe71byte-for-byte. -
Phase 2d post-gate fmt cleanup (
4fc0606d1). Six mechanicalcargo fmt --checkdivergences accumulated across the four Phase 2d substantive commits surfaced together when the §9 Format gate re-ran post-T9-T15 land: four indispatch_instruction's integer arms (IAddRs / IMulRcp / IRorR / IRolR) frombd7cea464, plusCANONICAL_E_MASK_PDfrom043076f18. Addressed in a single fmt-only commit per15-deletion-and-debt.mdc's "fix mechanical formatting errors in a file already being modified" carve-out; no semantic change. §8 commit-table reconciliation (five landed commits vs. seven planned slots) is documented indocs/completed/RANDOMX_V2_PHASE2D_PLAN.md§11's Implementation row with SHA → §8 mapping and §9 gate confirmation at HEAD4fc0606d1.
-
-
Sub-PR design discipline rule (PR #67, 2026-05-22). Promotes fourteen Phase 2c-emergent process disciplines from
docs/completed/RANDOMX_V2_PHASE2C_PLAN.md/RANDOMX_V2_PHASE2C_AUDIT.mdinto.cursor/rules/26-sub-pr-design-discipline.mdc(Option A; opt-in — cite when scoping multi-round per-trait PRs). Closesdocs/FOLLOWUPS.mdV3.0 discipline-promotion item. Applies to RandomX v2 sub-PRs, LWMA-1 Phase 4, and other multi-round consensus-critical design work. -
RandomX v2 Track A Phase 2d — Rounds 1–6 design closure (PR #68). Expands
docs/completed/RANDOMX_V2_PHASE2D_PLAN.mdthrough Round 6 after PR #66 ondev(e9917097f): Round 1 (FPU/F128/frequency dispatch/u128 audit); Round 2 (PC-driven loop,Program.cbranch_table,VmState.branch_pc); Round 3 (threat-model addenda); Round 4 (phase2d generator CLI for T9–T16); Round 5 (closure + §10 FPU grep patterns). Round 6 closes two Round-6-blocking findings against the Round-5 state: (R6-D1) aarch64 FPU primitive resolves the R1-D1/R5-D1 inconsistency by reopening R1-D1 option (b) for aarch64 only — stable inline asmmrs/msr fpcrwrite — with substrate justification (no stablecore::arch::aarch64FPCR-write intrinsic exists); (R6-D2) out-of-range opcode disposition changes from R1-D3'sdebug_assert!/no-op pair topanic!in both profiles, removing the debug-vs-release behavior divergence the §10 equivalence gate would surface. Plan-doc edits ride along: R1-D4 IMUL_RCP unreachability citation (R6-D3), §8 commit-5 split into 5a (T9–T16 additions) + 5b (T8 expectation flip) keeping the consensus-affecting flip independently bisectable (R6-D4),exec_pcinvariant- documentation note + sentinel reset for implementation-PR rustdoc (R6-D5). Implementation authorized onfeat/randomx-v2-phase2d. -
RandomX v2 Track A Phase 2c — Cache derivation + VM substrate + T1-T8 spec-vector parity + bench baselines (
feat/randomx-v2-phase2c-impl, PR #66, 2026-05-22). Third sub-PR of the Rust pure-software RandomX v2 verifier port perdocs/design/RANDOMX_V2_PLAN.md§"Track A — Phase 2" and the design plandocs/completed/RANDOMX_V2_PHASE2C_PLAN.md. Eight-commit stack landing the cache + VM substrate end-to-end with byte-for-byte parity against therandomx-v2fork at pinaaafe71(v2.0.1) for all eight reference vectors (T1-T8), plus the bench baseline + CI cross-profile gate that Phase 2d/2f/2g inherit:- Commit 1 —
Cachetype skeleton + size constants +Drop(39eda3164).src/cache.rspub Cachestruct +CACHE_SIZE/DATASET_ITEM_SIZE/DATASET_ITEM_COUNTconstants + emptyDrop(review-surface hook per §5.11.4). Per the §3 module layout + §2 surface 1 framing. - Commit 2 —
Cache::derive+ T1' determinism + unsafe carve-out #1 (48e7df633). Argon2d 256 MiB fill (delegating to Phase 2a'spub(crate) fill_cache) + 8 ×Blake2Generator-seededgenerateSuperscalarprograms (delegating to Phase 2b'sBlake2Generator+generateSuperscalarfromsrc/superscalar.rs)RANDOMX_CACHE_ACCESSESconstant + the cache-memory allocation unsafe carve-out (the only#![deny(unsafe_code)]exception this commit introduces, per the §1 covenant 7 enumeration) + cache-sitedebug_assert!s per §5.11.2. T1' (Cache::derivedeterminism property test, ~100 invocations) + theprogramsfield landing onCache. Plan-doc errata86f058c3b/431a54b38/3e6bb2734(impl-time pre-flight R0-D5/R0-D6/R0-D7: dropCache::from_raw, relocate T1-T8 to unit tests, withdrawrandomx_reciprocalpub(crate)promotion).
- Commit 3 —
Cache::derive_item+item_bytes+ T2' invariance (9ab584596).pub(crate) Cache::derive_item(the per-iteration dataset-item read consumed byVmState's 2048-iteration loop)pub(crate) Cache::item_bytes(the byte-level indexing accessor) + the dataset-item spec constants (SUPERSCALAR_MUL_0,SUPERSCALAR_ADD_1..SUPERSCALAR_ADD_7). T2' (invariance under item-number permutation) property test. Dissolves the#[allow(dead_code)]onsuperscalar::execute_superscalar.
- Commit 4 —
VmStateskeleton + scratchpad alloc +Drop(c63555a5e, with186a8cfdffix-up for thePROGRAM_SIZE/PROGRAM_ITERATIONSdistinction caught at R0-D9 pre-flight).src/vm.rspub(crate) VmStateskeleton with the frozen §5.1.1 field set (per §5.5 F5 v2-only simplification),pub(crate)type definitions (F128,Instruction,Program), thePROGRAM_SIZE(384) /PROGRAM_ITERATIONS(2048) /RANDOMX_SCRATCHPAD_L3(2 MiB) spec constants, thealloc_zeroed_scratchpadcarve-out (the second and final#![deny(unsafe_code)]exception this PR introduces per §1 covenant 7), the scratchpad-allocationdebug_assert!per §5.11.2, the emptyDrop(review-surface hook per §5.11.4), and the threat-model disposition rustdoc per §5.11.4 (public-input-only scope note). - Commit 5 —
init_scratchpad+init_program+ T3'-T5' determinism (76cf9a5ae).VmState::init_scratchpadviacrate::aes::fill_aes_1r_x4;VmState::init_program(stack- allocate the 3 200-byte program buffer per spec §4.5's128 + 8 × PROGRAM_SIZEbudget, fill viacrate::aes::fill_aes_4r_x4, parseentropy[0..128]into the register-init field set, parseinstructions[128..3200]intoself.program.instructions); plus the IEEE-754 / dataset helpers the parser consumes (get_small_positive_float_bits,get_float_mask,CACHE_LINE_ALIGN_MASK,DATASET_EXTRA_ITEMS,CACHE_LINE_SIZE). T3' / T4' / T5' fixture-free determinism property tests inline per §5.11.1- §14 Round 0 R0-D6 (test placement inside
src/*.rs#mod tests).
- §14 Round 0 R0-D6 (test placement inside
- Commit 6 —
compute_hash+execute_program+ T6'-T8' determinism (4b182292b).pub fn compute_hash(&Cache, &[u8; 32], &[u8]) -> [u8; 32](the crate's single hash- producing entry point) +VmState::execute_program(the spec §4.6 /vm_interpreted.cpp::execute()2048-iteration loop — the single per-iteration body that the stub-NOPdispatch_instructiondispatches into per spec §4.6.5) + the privatedispatch_instructionNOP-body stub (the §5.1 function-body replacement contract Phase 2d fills in per §5.1.1 frozen surfaces 1-3); plus the supporting helpers (SCRATCHPAD_L3_MASK_64,DYNAMIC_MANTISSA_MASK,RANDOMX_PROGRAM_COUNT,cvt_packed_int_to_f128,mask_register_exponent_mantissa). T6' / T7' / T8' fixture- free determinism property tests inline per §5.11.1. - Commit 7 — T1-T8 spec-vector parity vs. randomx-v2 fork
(
4ba995469). Reviewer-runnable C++ reference generator attests/vectors/reference/_generator/phase2c/(Makefile +gen.cpp+ README +.gitignore) compiled against the vendored fork at pinaaafe71. Eight reference vectors pre-computed and committed undertests/vectors/reference/cache/(T1: cache fingerprint Blake2b-256 over the entire derived cache + the 8 superscalar programs; T2: 8-item dataset batch) andtests/vectors/reference/vm/(T3: scratchpad init; T4: register init from entropy; T5: program parse from entropy; T6:spAddr0/spAddr1snapshot across 4 stub-NOP iterations; T7: post-AES-mix register snapshot across 4 stub-NOP iterations; T8: end-to-endcompute_hashoutput under stub-NOP dispatch). Ten Rust spec- vector tests (T1-T8) inline insrc/cache.rsandsrc/vm.rspass byte-equality against the committed fixtures. Refactor:VmState::execute_programsplit intoexecute_program(the outer per-chain orchestration) +execute_iteration(the per-iter body) to enable T6/T7 intermediate-state snapshotting. Two implementation-time substrate-divergence findings landed in this commit per16-architectural-inheritance.mdc's cross-language-port discipline:- R0-D10 — chain-boundary integer-register reset
(cross-language-port-implicit-state-loss discipline). The
C reference's per-chain
NativeRegisterFile nreg;construction (vm_interpreted.cpp:59) implicitly zero- initializes the integer-register array via the struct definition atbytecode_machine.hpp:40. The Rust port fusesreg+nreginto a singleVmState.rper30-cryptography.mdcsecret-locality framing; the per-chain reset that fell out of C's two-struct shape is re-asserted explicitly asself.r = [0; 8];at the top ofVmState::execute_program. T8 (the end-to-end vector that runs all 8 chains) surfaced the missing reset; T6/T7 (single chain) don't. Disposition under §14 Round 0 R0-D10. - R0-D11 —
IMUL_RCP::imm32storage divergence (cross- language-port-storage-divergence discipline). The C reference'sinitCache(dataset.cpp:131-138) post-processes the 8SuperscalarPrograms aftergenerateSuperscalarby replacing eachIMUL_RCPinstruction'simm32in-place with an index into a reciprocal-cache side table; the reciprocal value is later resolved at execution time via that index. The Rust port keeps the originalimm32and computes the reciprocal on-the-fly inexecute_superscalar(result-equivalent, byte-divergent for serialization). T1 (the cache fingerprint vector that hashes the serialized programs alongside the derived cache) surfaced the divergence; T2 (which only consumes thederive_itemoutput, not the program serialization) didn't. Resolution lives in the C++ generator (emit_t1re-runsBlake2Generator+generateSuperscalardirectly rather than reading the C reference'scache->programsto hash the pre-modification programs); the Rust port's storage shape is the consensus-relevant one. Verified result- equivalence via T2's dataset-item parity (which exercises the sameIMUL_RCParm at execution time via Rust's on-the-fly reciprocal calculation). Disposition under §14 Round 0 R0-D11.
- R0-D10 — chain-boundary integer-register reset
(cross-language-port-implicit-state-loss discipline). The
C reference's per-chain
- **Commit 8 — Phase 2c benches + per_hash_latency placeholder
- debug-vs-release CI gate + scope-bounding doc-comment +
BENCH_RESULTS + CHANGELOG** (this commit). Two criterion
benches at
benches/cache_derive.rs benches/compute_hash_alloc.rslanding the §5.8 PR-gate baseline measurement infrastructure.tests/perf/per_hash_latency.rsplaceholder (#[ignore]+unimplemented!()cross-referencing F8 + §13 forward-path 2g inheritance) at the canonical 2g deliverable path per R3-minor-2 — structural code out-survives prose discipline per21-reversion-clause-discipline.mdc; 2g's author finds the placeholder by grep against its own deliverable name and replaces the body in-place. Workflow line addition in.github/workflows/build.ymlGate 2:cargo test --release -p shekyl-pow-randomxper §5.11.3 R4 (Rust integer-overflow semantics differ between debug-panic and release-wrap; T1-T8 byte-equality assertions catch any silent drift). Crate-level scope-bounding doc-comment insrc/lib.rsper §5.11.4 R4 (public-input-only scope with substrate-anchored reopening criterion). Baseline measurements recorded inBENCH_RESULTS.md(i9-11950H, Debian 13, kernel 6.12.88):Cache::derivemedian 341.45 ms;compute_hashend-to-end (stub-NOP) median 296.00 ms. Both measurements exceed the §5.8 plan-author budgets (200 ms and 100 µs respectively); the threshold-vs-actual gap is documented inBENCH_RESULTS.md§"Threshold reconciliation" with the diagnosis (single-thread Argon2d on this hardware class is fundamentally a ~300 ms operation; thecompute_hash_allocbudget framing was internally inconsistent with what the bench measures) and named reopening criteria per21-reversion-clause-discipline.mdc. Plan-doc errata R0-D12 (separate commit landing alongside this one) records the gap in §14 Round 0; Phase 2c does not block on the reconciliation per the §5.8 explicit "implementation-PR-time decision" authority.
- debug-vs-release CI gate + scope-bounding doc-comment +
BENCH_RESULTS + CHANGELOG** (this commit). Two criterion
benches at
Phase 2c retains the Phase 2a/2b forward-compatibility posture: no
#[no_mangle], noextern "C" fn, no#[export_name], no module-level runtime-mutable state.#![deny(unsafe_code)]at the crate level with the two carve-outs above per §1 covenant 7 (cache::CACHE_MEMORY_ALLOCandvm::alloc_zeroed_scratchpad).cargo test -p shekyl-pow-randomxsucceeds withoutexternal/randomx-v2/initialized and without a C++ toolchain — the spec-vector reproducibility check (make vectorsin the generator directory) opts in to the fork submodule + C++ build. The live differential harness remains Phase 2g's separate artifact. Phase 2d builds on the Phase 2cdispatch_instructionNOP stub via function-body replacement (no trait wiring, no impl swap, no signature change tocompute_hashper §5.1.1's frozen surfaces 1-3); Phase 2f wrapsCache::derivein aCacheStoreLRU +VmStatepool; Phase 2g lands the C-side differential harness as a separate test-only artifact; Phase 3 then exposes the verifier throughshekyl-ffiand rewires the C++ daemon to it; Phase 4 deletes the C++ verifier path. - Commit 1 —
Documentation
-
RandomX v2 Track A Phase 2c plan + 2d skeleton scaffold + parent-plan alignment (
chore/randomx-v2-phase2c-plan, PR #65, 2026-05-21). Doc-only branch landing the design substrate for Phase 2c implementation (Cache::derive+VmState+compute_hash+ NOP-bodydispatch_instruction), the Phase 2d skeleton scaffold (function-body replacement ofdispatch_instruction), and the parent-plan alignment commits that absorb the cross-cutting decisions. Thirteen commits across five design rounds. Implementation cut authorized post-PR-#65 merge per the §14 closure entries.-
Round 1 (2026-05-21). F1–F9 interactive walk closed nine findings via gap-analysis; ShekylU128 audit verified the v2-only simplification surface. F4 absorption (
Cache::derivemoves from originally-scoped Phase 2e into 2c) lands as a parent-plan precursor commit. PerRANDOMX_V2_PHASE2C_PLAN.md§14 Round 1 entry. -
Round 2 (2026-05-21). Substrate-finding pass tightens the type-and-module shape within Round 1's locked dispositions. Three structural restructurings: (R2-D1)
BytecodeDispatchtrait plusStubNopDispatchimpl →dispatch_instructionfree function with NOP body replaced in 2d, eliminating the mock-X anti-pattern; (R2-D2)Vm<'a>public type →compute_hashpublic transform withVmStateprivate (module layout collapses 5 files → 2); (R2-D3)Cache::from_rawvisibility correction (pub→pub(crate); test-time only, not FFI surface). Parent-plan alignment commit follows (Decision #7 substrate-shift:VmStatepooling becomes internal tocompute_hash, not a publicVmPooltype). -
Round 3 (2026-05-21). Substrate-completeness pass closes out before implementation. (R3-D1) §5.1.1 "Function-body replacement contract" pins the 2c → 2d hand-off: frozen
dispatch_instructionsignature, frozenInstructionfield set, andVmStatefield set populated empirically from an audit againstbytecode_machine.hpp's 29 opcode handlers +vm_interpreted.cpp::execute(). Audit produced one correction-from-prompted-list finding:mpis a v2-only local-variable alias formem.ma, not a struct field; §5.5 F5 entry updated to match (the audit-against-actual-code precedent that §5.11.8 formalizes in Rounds 4–5). (R3-D3) Sibling commit landsRANDOMX_V2_PHASE2D_PLAN.mdskeleton scaffold: §5.1.1 contract carry-forward, VmState field-set reference, forward-actions from F1/F2/F3/F5/F7, decision points for 2d Round 1 (FPU rounding-mode mechanism; F128 newtype shape; per-opcode dispatch shape). -
Round 4 (2026-05-21). Threat-model addenda pass against priority-1 surface (per
.cursor/rules/00-mission.mdc's security-and-quantum-resilience commitment) enumerating six attack objectives: mining-faster differential; cache poisoning; FFI exploitation; resource DoS; Rust safety boundary gaps; consensus split via implementation divergence. New §5.11 records eight findings + dispositions. In-scope 2c-implementation additions: T1' (Cache::derivedeterminism) + T2' (derive_iteminvariance) property tests (~60 LoC per §5.11.1's per-sub-test estimate; T1'a/b/c ~10 LoC each + T2'a ~30 LoC);debug_assert!discipline at the two unsafeBox::new_zeroed_slicesites (~10 LoC); debug-vs-release equivalence as PR gate (1 line in CI workflow); public-input-only scope note. Forward-actions to downstream phases: 2g adversarial seedhash corpus + pathological-program worst-case timing bound (≤5.0×); 3a FFI null-pointer + length-validation +seedhash: *const [u8; 32]typed-array pointer +ERR_NULL_PTR/ERR_DATA_TOO_LARGEtaxonomy +RANDOMX_BLOCK_TEMPLATE_MAX_SIZEpinned at 2 MiB; 2f CacheStore canonical-seedhash slot eviction-protection +VmStatepool capacity sized against daemon parallel-verification fanout. Discipline note: §5.11.8 audit-against-actual-code validation (the discipline that produced R3-D1'smpcorrection is the discipline 2d/2g inherit). Parent plan alignment + 2d skeleton addenda ship as sibling commits. -
Round 5 (2026-05-21). Closure-only refinement pass against the Round 4 plan-doc; substantive review surface closed at Round 4, four discipline-enforcement edges tightened:
- (R5-D1)
RANDOMX_V2_PHASE2C_PLAN.md§5.11.8 framing amendment: "reading-the-source vs. producing-a-table-from-intuition" named as the load-bearing audit step (the table is the audit's output; the audit's substance is the line-by-line reading that produces the table). "Show your work" enforcement formalized: every audit table cites line ranges at the pinned fork commit; reviewer spot-checks by opening the cited file and reading the named lines. The R3-D1mpcorrection is reframed from "we caught one bug" to the precedent that proves the discipline (a prompted-list table without a reading-the-source pass IS the failure mode.cursor/rules/16-architectural-inheritance.mdc's "audits-are-clean-so-compress" anti-pattern names). - (R5-D2)
RANDOMX_V2_PLAN.mdPhase 0 §5 FFI hardening refinements: C-side header formconst uint8_t (*seedhash)[32](not decayedconst uint8_t *seedhash); C++ call-site declaration discipline (uint8_t seedhash_buffer[32]; ...&seedhash_buffer), documented at each call site not just at the signature;RANDOMX_BLOCK_TEMPLATE_MAX_SIZErationale-sentence cross-check (generous ceiling above any realistic Shekyl block template; the 2 MiB == scratchpad-size coincidence is explicitly named non-load-bearing; reversion-clause per.cursor/rules/21-reversion-clause-discipline.mdc). - (R5-D3)
RANDOMX_V2_PHASE2D_PLAN.md§3.1 CI-time grep mechanical-enforcement addendum: the unsafe-block scope-check discipline (Scaffold-R4 prose-form) is promoted to a §10 hard-gate CI grep modeled on theRANDOMX_V2_PLAN.md§7.7shekyl-pow-randomxnever uses#[no_mangle]invariant pattern. The grep asserts the rounding-mode-setter function body contains exactly one of the chosen-option primitives (_mm_setcsr/__set_fpcr/asm!/chosen-crate) and nothing else (no other intrinsic calls, no pointer dereferences, no allocator calls, no function calls beyond the primitive). Catches the "future contributor adds a reasonable-seeming improvement that silently expands the unsafe surface" failure mode that prose-as-discipline depends on reviewer attention to catch. - (R5-D4) New
docs/FOLLOWUPS.mdV3.0 entry: sibling PR (opens post-PR-#65 merge todev; parallel-eligible with the Phase 2c implementation PR, not gated on it) to promote five 2c-emergent disciplines to project-level documentation (likely.cursor/rules/26-sub-pr-design-discipline.mdcwith substantial prose, similar shape to16-architectural-inheritance.mdc). The disciplines: function-body replacement contract; audit-against-actual-code; threat-model addenda framing; reversion-clause for sub-PR boundary changes; forward-action propagation convention. Scoped as a short-lived sibling per.cursor/rules/06-branching.mdcrule 2; opens within 5 working days of PR #65 merging; lands before Phase 2d Round 1's design doc cuts. Explicitly not a Round 5 deliverable per.cursor/rules/15-deletion-and-debt.mdc"while we're here is the enemy."
- (R5-D1)
-
Posture-shift note (recorded for downstream sub-PRs). Round 4's threat-model framing converted "design closure" into design closure plus active defense against named attacker objectives. The shift is named in the §14 Round 5 entry so 2d Round 1, 2f Round 1, 2g Round 1, and LWMA-1 Phase 4's design rounds inherit the shape rather than revert to per-finding review — the threat-model-objective frame surfaces findings (
mp, eviction interleave, FPU rounding-mode escape, u128 edge cases) that per-finding review wouldn't catch because no individual finding suggests the next one; the attacker-objective frame does. -
Touched files.
docs/completed/RANDOMX_V2_PHASE2C_PLAN.md(new; Rounds 1–5);docs/completed/RANDOMX_V2_PHASE2D_PLAN.md(new; Scaffold + Scaffold-R4 + Scaffold-R5);docs/design/RANDOMX_V2_PLAN.md(parent-plan alignment commits for F4 absorption, Decision #7 substrate-shift, Round 4 FFI/perf/risk carries, Round 5 FFI refinements);docs/FOLLOWUPS.md(Round 5 V3.0 entry).
-
-
Phase 2a PR #62 — address Copilot review (post-Round-1 follow-up cycle). Doc-only commits on
feat/randomx-v2-phase2a. Addresses five Copilot review findings surfaced against the Phase 2a initial commits (107d6f8ce,9f854e0ce,f0d648fb2). The fixes cluster on two distinct substantive concerns in the_generator/directory; the Rust test source and the productionargon2d.rsprimitive are unaffected.-
Findings C1–C4 — "architecture-independent" wording is materially wrong (4 sites). The committed
_generator/gen.c,_generator/Makefile,m8_t3_p1_shekyl_test_key.meta.txt, andm64_t3_p1_shekyl_test_key.meta.txtall asserted that theargon2_ref.creference Argon2 implementation "keeps the produced bytes architecture-independent." That is wrong on endianness:gen.c'swrite_rawdoes a rawfwriteofblock { uint64_t v[128] }memory, which serializes eachuint64_tin the host's native byte order. The committed.binfiles happen to be little-endian u64 streams because they were generated on a little-endian host (x86_64 Linux); regenerating on a big-endian host would silently produce different bytes and break the byte-for-byte test.Reworded each site to the accurate framing: the reference impl is instruction-set-independent (no AVX2/SSSE3 codegen variance across hosts that share a byte order), and the on-disk vector format is a little-endian u64 stream — the same format
argon2d.rs'sblocks_to_le_bytesproduces in Rust viau64::to_le_bytes(perargon2d.rs:230-241).gen.c's comment additionally records the big-endian-host disposition: the fix if a future maintainer regenerates on big-endian hardware is to add ahtole64-style serialization step inwrite_raw, not to redefine the on-disk format.Important asymmetry preserved: the Rust test side is already architecturally portable —
blocks_to_le_bytespins the LE convention on both sides regardless ofBlock's in-memory layout, so thecargo testpath works correctly on big-endian targets. The bug was only on the C generator side and only in the comment claims, not in the bytes themselves (which are correct for the all-little-endian maintainer/CI fleet the project ships against). -
Finding C5 —
_generator/README.mdprovenance check command is broken (1 site). The "Reviewing the vectors" section documenteddiff -r . ..as the verification command. That compares the_generator/directory's file set (gen.c,Makefile,README.md) against the parent directory's file set (*.bin,*.meta.txt); the file sets do not overlap, sodiff -ralways reports "only in" entries instead of the intended check (do the regenerated.binfiles match the committed bytes?).Replaced with
git diff --stat -- ../*.binissued from inside_generator/.make vectorsoverwrites the committed bytes in-place;git diff --statthen asks git whether the working tree has drifted fromHEADon the specific.binpaths. A clean exit (no output) is the affirmative attestation that the committed bytes match the named fork pin. Added a paragraph explaining why the priordiff -r . ..command did not work so a future reviewer doesn't reintroduce the same shape.
Gates. Doc/comment-only changes; no production Rust touched.
cargo fmt --check,cargo clippy -p shekyl-pow-randomx --all-targets -- -D warnings,cargo test -p shekyl-pow-randomx, andcargo doc -p shekyl-pow-randomx --no-depsall clean. The Phase 2f forward-compatibility greps fromRANDOMX_V2_RUST.md§7.2 (#[no_mangle],extern "C" fn,#[export_name], module-level runtime-mutable state) still return zero hits on the crate.Scope discipline note. C1–C4 land as a single commit because they are the same finding instance applied at four sites with identical content fixes (per
90-commits.mdcscope-per-commit rule — "scope" is the substantive change class, not the file count); C5 lands as a separate commit because it is a distinct finding (functional bug in a procedure command vs. wording precision). The Lean-A disposition (reword the comments to describe the constraint accurately) is preferred over Lean-B (addhtole64portability shims togen.c) because the generator is a developer-machine-only artifact that runs on the project's all-little-endian maintainer fleet, and bundling portability shims for a host architecture nobody runs on is the cost-benefit-defer-to-later anti-pattern's mirror image per16-architectural-inheritance.mdc. The big-endian-host disposition is recorded ingen.c's comment so a future maintainer who needs it knows the prescribed shape. -
-
Post-PR-4
docs/FOLLOWUPS.mdcleanup (chore/post-pr4-followups-cleanup, 2026-05-21). Two scope-respecting doc-only commits closing the cleanup work that became actionable once PR 4 (commitfd6005e2a, merged 2026-05-21) landed. Scoped per the post-PR-4 FOLLOWUPS triage to Class A relocations (closed-but- mislocated entries) and D-item 481 substrate corrections, with Class B (P1 / P2 / P3) and Class C (F11-S Windows-midrange, refresh bandwidth under α) deliberately untouched to avoid the cost-benefit-defer-to-later anti-pattern of reflexive re-anchoring under16-architectural-inheritance.mdc— their existing trigger language remains accurate and the merge SHA will be cited at each focused PR's open date.-
Commit 1 — Relocate three
[CLOSED 2026-05-20]PR 4 entries to Recently resolved (38a599fc6):docs/FOLLOWUPS.mdgains three audit-trail entries relocated from the live queues, prose-verbatim with the[CLOSED 2026-05-20]prefix folded into the standard "(closed YYYY-MM-DD, merged todevYYYY-MM-DD at SHA)" parenthetical the section uses, plus the dev-merge SHAfd6005e2aadded for traceability:- "Stage 1 retroactive Mock-X cleanup:
MockLedger→LocalLedger::from_test_blocks(...)+FaultInjecting<LocalLedger>" (was V3.0 queue L697; PR 4 §7.X commit C6β), - "Stage 1 retroactive Mock-X cleanup:
MockDaemon→TestDaemonrename" (was V3.0 queue L744; PR 4 §7.X commit C6γ), - "Stage 1 PR 4 Phase 0d —
RefreshEnginecheckpoint 3 mid-scan- reorg-abort extension: struck, not deferred" (was V3.x staker- archival queue L4116; PR 4 §7.X commit C8). The Phase-0d entry had one substrate-anchored cross-reference that became wrong in its new location (referencedReorgAmplificationDetectoras "below" — true in V3.x queue context, false in Recently-resolved context); rephrased to "(above, in the V3.x staker-archival queue)". Net diff: 117 insertions / 118 deletions (relocation-shape).
- "Stage 1 retroactive Mock-X cleanup:
-
Commit 2 — Sharpen PR 3 engine-property test re-location entry (
95ece3760):docs/FOLLOWUPS.mdV3.0-queue entry "Stage 1 PR 3 engine-property test re-location" (~L481) gains two substrate-correctness fixes surfaced during the post-PR-4 cleanup triage that confirmed why the work is not completable in the current cleanup PR:- Trigger anchor corrected from
STAGE_1_PR_3_KEY_ENGINE.md§4.4 (which does not exist — §4 is the "Post-amendment §2.1 trait surface" and has no §4.4) to §7.7 ("V3.x full-PQC trait churn acknowledgement") + §3.4 Decision 4. Trigger reworded to name the V3.2 unifiedKeyEngine/LedgerEngine/DaemonEnginepub(crate) → pubvisibility-promotion bundle explicitly, with an inline "unilateralKeyEnginewidening does not satisfy this trigger" clause that prevents future maintainers from acting on a single-trait widening — re-introducing the trust-model incoherence the per-traitpub(crate)lock prevents per Stage 1's Trust-class A classification (PR 3 §2.1 trust-class table row forKeyEngine). - M3b D5 peer-test name removed (the
..._subaddresspeer test the pre-flight estimated was consolidated into the primary test's inner loop during M3b implementation;local_keys.rs:1278-1322). Added one-sentence pre-flight-vs-implementation note for cross-reference traceability toSTAGE_1_PR_3_M3B_PREFLIGHT.md§D5. File:line anchors added for both tests (local_keys.rs:1258,:1554) and the inline test-docstring deviation notes (:1243-1251,:1538-1541). Disposition unchanged: entry remains open with the same V3.2 trigger and one-PR-covers-both-tests bundling guidance.
- Trigger anchor corrected from
Gates. Doc-only changes; no code touched.
git diffshape verified to be relocation + substrate-correctness only; no live disposition re-anchoring. No CHANGELOG drift relative to the as- landed FOLLOWUPS state.Scope discipline note. The cleanup PR's scope was bounded explicitly against the user-named anti-pattern of continual re-anchoring without completion: items where the work cannot be completed at this time (P1 / P2 / P3, F11-S Windows-midrange, refresh bandwidth under α, the PR 3 engine-property test re-location itself) get their existing dispositions preserved verbatim, and the merge SHA
fd6005e2awill be cited at each focused PR's open date per21-reversion-clause-discipline.mdc's named-criteria principle. Only completable work (relocations + substrate-correctness fixes) lands here. -
Added
-
RandomX v2 Track A Phase 2b — AES + Blake2Generator + SuperscalarHash primitives + spec-vector parity tests (
feat/randomx-v2-phase2b, 2026-05-21). Second sub-PR of the Rust pure-software RandomX v2 verifier port perdocs/design/RANDOMX_V2_PLAN.md§"Track A — Phase 2" and the design plandocs/completed/RANDOMX_V2_PHASE2B_PLAN.md. 7-commit stack (6 designed + 1 rustfmt cleanup interleaved between commits 5 and 6 to absorb residue an editor save reintroduced intosuperscalar.rsbetween gate-runs) landing the remaining v2 primitives the verifier needs:- Commit 1 — AES round primitives + Blake2Generator + MSRV
bump.
cipher_round/equiv_inv_cipher_roundatsrc/aes.rswrappingaes-0.9.0::hazmat(_mm_aesenc_si128/_mm_aesdec_si128equivalent-inverse semantics, matching RandomXsoft_aesenc/soft_aesdec).Blake2GeneratorPRNG atsrc/blake2_generator.rsper spec §3.5. Workspacerust-version = "1.85"pin foraes-0.9.0's edition-2024 MSRV, verified against the Guix substrate before pinning. F1 convergence onsrc/argon2d.rs's#[allow(dead_code)]from module-level to per-item attributes. - Commit 2 — AES composites.
AesGenerator1R(state writeback),AesGenerator4R(no writeback),AesHash1Rper spec §3.2–3.4. Initial state, generator keys, and extra- round keys ported asconst [u8; 16]arrays viapack_le_u32x4reproducing_mm_set_epi32(i3, i2, i1, i0)'s little-endian memory layout exactly. - Commit 3 — SuperscalarHash generator + executor.
src/superscalar.rsimplementing spec §6 + §7.2. Pure function call surface (no module-level mutable state) per permanent decision #6;#![deny(unsafe_code)]survives. Includes the §5.5 spec- silence audit table in the module rustdoc with 8 documented spec-silent decisions matching the C reference verbatim. - Commit 4 — AES spec-vector parity tests. 8 reference
vectors at
tests/vectors/reference/aes/covering round primitives, the F6 chained-pair multi-round supplement,AesGenerator1R/4Routputs, andAesHash1Ron uniform + empty inputs. Reviewer-runnable C++ generator at_generator/instantiates<softAes=true>templates to keep emitted bytes SIMD-codegen-independent. - Commit 5 — SuperscalarHash spec-vector parity tests. 7
reference vectors at
tests/vectors/reference/superscalar/per the F4 structured 3-vector decomposition: 3 Layer A program serializations (baseline / nonce-mixing / seed- derivation isolation), 3 Layer B executions against fixedr=[0..8], and 1 combined end-to-end attestation tuple. Test names encode the failure-mode attribution (vector_2_tests_nonce_mixing_only, etc.). Layer B decouples generation parity (Layer A) from execution parity; combined tests the full generate→execute pipeline without intermediate serialization. Wire format documented in_generator/README.md"Wire format" and inserialize_program/deserialize_programhelpers. - Commit 6 — CHANGELOG + FOLLOWUPS entry (this commit).
F7 AES symbol-surface handoff to V3.0 / Phase 3c recorded at
docs/FOLLOWUPS.md; the live runnable check (cargo build --release && nm shekyld | grep -iE '(aes|randomx)') and its expected disposition (norandomx_*matches perRANDOMX_V2_RUST.md§7.1; aes-crate Rust-mangled symbols expected and benign) are recorded at the pre-genesis queue so the Phase 3c PR closes the item. - Phase 2b retains the Phase 2a forward-compatibility posture:
no
#[no_mangle], noextern "C" fn, no#[export_name], no module-level runtime-mutable state.#![deny(unsafe_code)]at the crate level.cargo test -p shekyl-pow-randomxsucceeds withoutexternal/randomx-v2/initialized and without a C++ toolchain — the live differential harness remains Phase 2g's separate artifact. Phase 2c–2e build on Phase 2b's primitives to deliverVm, bytecode dispatch, andCache::derive; Phase 3 then wires the verifier throughshekyl-ffi; Phase 4 deletes the C++ verifier path.
- Commit 1 — AES round primitives + Blake2Generator + MSRV
bump.
-
RandomX v2 Track A Phase 2a —
shekyl-pow-randomxcrate scaffold + Argon2d primitive (feat/randomx-v2-phase2a, 2026-05-21). First sub-PR of the Rust pure-software RandomX v2 verifier port perdocs/design/RANDOMX_V2_PLAN.md§"Track A — Phase 2" anddocs/design/RANDOMX_V2_RUST.md.- New workspace crate
rust/shekyl-pow-randomx/with crate-level rustdoc citing the Phase 0 decision substrate (spec-first perRANDOMX_V2_RUST.md§3; derived-first per §4; isolation invariants per §7). pub(crate) fn fill_cache(key: &[u8], blocks: &mut [argon2::Block])atsrc/argon2d.rsimplementing the Cache Argon2d "memory fill" perexternal/randomx-v2/doc/specs.md§7.1 + Table 7.1.1 (parallelism = 1,memory = 262144KiB = 256 MiB,iterations = 3,Argon2d,salt = "RandomX\x03"). Built onargon2 = "0.5.3"'sArgon2::fill_memoryafter verifying at source that the omit-finalizer path matches RandomX's spec-required surface (recorded in the module rustdoc). Constants andParamsare compile-time validated.- Argon2d spec-vector parity tests at
tests/vectors/reference/argon2d/: two derived vectors (m=8boundary case;m=64multi-segment) fromargon2_ref.cat fork pinaaafe71(v2.0.1), with per-file.meta.txtprovenance headers and a reviewer-runnable_generator/reproducer (gen.c,Makefile,README.md). The Rust tests consume pre-committed bytes viainclude_bytes!; nocargo testdev-dep on the C library (Phase 2g owns the live differential harness at full RandomX parameters). - Forward-compatible with Phase 2f's CI isolation invariants:
no
#[no_mangle], noextern "C" fn, no#[export_name], no module-level runtime-mutable state (noMutex/RwLock/OnceCell/OnceLock/Lazy/static mut/atomics-at-module-scope).#![deny(unsafe_code)]at the crate level. - Phase 2a scope is purely additive: no FFI surface, no C++
caller rewire, no deletion of
src/crypto/rx-slow-hash.c(those are Phase 3a/3b/3c/4). Phase 2b lands AES round + SuperScalarHash next.
- New workspace crate
-
Stage 1 closeout audit tracking (2026-05-27, post–PR #81; updated 2026-05-29 post–PR #88). Records Stage 1 trait-extraction status in
FOLLOWUPS.mdV3.0 queue and cross-refsV3_ENGINE_TRAIT_BOUNDARIES.md§8.1 / §1 banner plusWALLET_REWRITE_PLAN.md. Dedicated audit markdown now landed:docs/completed/STAGE_1_COMPLETION_AUDIT.md. -
Stage 1 PR 5 —
PendingTxEnginetrait surface and Phase 1 substrate (feat/stage-1-pr5-pending-tx-engine, 2026-05-27). Lands the Round-3-closedPendingTxEnginetrait, the (γ) lean three-collection reservation model, secondary-engine trait seams, andEngine<S, D, L, R, P>orchestration dispatch perdocs/design/STAGE_1_PR_5_PENDING_TX_ENGINE.md§4 / §5.0 / §7.X (C0 =4466d153e… C7 =ca7622558; C8 doc commit follows).pub trait PendingTxEngine: Send + Sync + 'staticatengine/traits/pending_tx.rswithbuild/submit/discard/outstanding/ optionalsignal_mempool_evicted(C5α/β).pub struct LocalPendingTx<S, O, F>atengine/local_pending_tx.rsas the V3.0 productionPparameter default forEngine<S, D, L, R, P>(C5β).pub struct SnapshotId([u8; 16])and domain-separatedderive_snapshot_id(&LedgerSnapshot)(C1).- Submit-path error vocabulary:
SubmitError,TerminalErrorKind,AmbiguousErrorKind,DiscardReason,ReservationExtension,PendingTxErroraugmentations (C2). pub enum PendingTxDiagnostic+emit_pending_txhelper onDiagnosticSink(C3);AssertionSink/PanickingSinkpending-event recording for tests (C7).- Secondary-engine traits:
Signer+LocalSigner(C4α),OutputSelector+WalletGreedyOutputSelector(C4β),FeeEstimator+DaemonFeeEstimator(C4γ). FaultInjecting<P: PendingTxEngine>FIFO fault-injection wrapper under#[cfg(any(test, feature = "test-helpers"))](C7);Engine::replace_pending_txtest hook (C6).
-
Stage 1 PR 4 —
RefreshEnginetrait surface (feat/stage-1-pr4-refresh-engine, 2026-05-15 → 2026-05-20). Lands the Phase-0a-bindingRefreshEnginetrait and theViewMaterialadjacent type perdocs/design/STAGE_1_PR_4_REFRESH_ENGINE.md§4 Phase 0a + Phase 0c + Phase 0e anddocs/V3_ENGINE_TRAIT_BOUNDARIES.md§2.3 (PR 4 C0 =322677261; C1 =d3edc1abb).pub trait RefreshEngine: Send + Sync + 'staticatengine/traits/refresh.rswith one async methodproduce_scan_result(snapshot: LedgerSnapshot, daemon: &D, opts: &RefreshOptions, cancel: &CancellationToken, progress: &watch::Sender<RefreshProgress>, diagnostics: &dyn DiagnosticSink) -> Result<ScanResult, Self::Error>andtype Error: Into<RefreshError>.- Five-checkpoint cancellation discipline (1 / 4 on the orchestrator; 2 / 3 / 5 on the trait body; checkpoint 5 is the per-transaction inner check per §5.4.9 F2 + F11 + F11-S safe-point pins).
Self::Erroris unit-variant-only at the trait surface per §5.4.7 R6 reframe: rich structured diagnostic information flows through the&dyn DiagnosticSinksecond channel; the synchronous return is a structural- branch signal only. OfRefreshError's six variants, three are reachable from aRefreshEngineimpl'sSelf::ErrorviaInto(Cancelledunit,Io(IoError),InternalInvariantViolation { context: &'static str }); three are orchestrator-constructed only (MalformedScanResultat the merge layer;ConcurrentMutationat the merge gate;AlreadyRunningat binary-layer single-flight).ScanResultatomicity-under-cancellation contract:produce_scan_resultreturns either aScanResultcovering the full span it scanned orRefreshError::Cancelled— no partial-span result is ever returned (R7 disposition).LedgerSnapshotis passed by value (R5 + §5.4.5): the orchestrator constructs under the engine read-guard, drops the guard, and hands the snapshot to the producer by move; the snapshot carries reorg-window descriptors only and is cheap to clone.&Ddaemon-handle borrow with the §2.5Clone + Send + Sync + 'staticbound onD, so implementors can clone internally if they need an owned handle to spawn work (e.g., parallel block-fetch refinements); implementors MUST NOT borrow&Dacross atokio::spawnboundary.pub struct ViewMaterial { spend_pub: EdwardsPoint; view_scalar: Zeroizing<Scalar>; x25519_sk: Zeroizing<[u8; 32]>; ml_kem_dk: Zeroizing<Vec<u8>>; spend_secret: Zeroizing<[u8; 32]> }atengine/view_material.rswithZeroize + ZeroizeOnDropderived; capturing the view-and-spend material atLocalRefresh::newso theScannerbuilds once and is held for the instance lifetime (no per-attempt scanner construction; no per-attempt secret duplication; R4 a-instance-scoped).- The
LocalRefreshimplementor atengine/local_refresh.rs(PR 4 C4 =ac100e1ab) is the V3.0 productionRparameter forEngine<S, D, L, R>; future implementors (Stage 4 actor-meshRefreshActor; any future producer variant) implement the same trait surface.
-
Stage 1 PR 4 —
RefreshDiagnosticenum +DiagnosticSinktrait + Stage 1 sink implementations (PR 4 C2 =8fc207051;SuppressedRateLimitvariant per Round 4 review pass F6 = same commit). Lands the second channel of the two-channel error / diagnostic actor-mesh seam perdocs/design/STAGE_1_PR_4_REFRESH_ENGINE.md§5.4.7 R6 reframe + §5.4.8 attack-surface dispositions.pub enum RefreshDiagnosticatengine/diagnostics.rswith#[non_exhaustive]and the Round-4-audit-confirmed Stage 1 variant set:DaemonMalformed { kind: MalformedKind },DaemonTimeout { op: DaemonOp, elapsed: Duration },DaemonProtocolError { kind: ProtocolErrorKind },ReorgObserved { fork_height: u64, depth: u32 },ScanProgress { height: u64, candidates: u32 }, and the Round-4-F6-addedSuppressedRateLimit { class: SuppressedClass }.- Supporting bounded enums (
MalformedKind,DaemonOp,ProtocolErrorKind,SuppressedClass), all#[non_exhaustive];SuppressedClasscarries one arm per rate-limited event class (DaemonMalformed,DaemonTimeout,DaemonProtocolError,ReorgObserved,ScanProgress). TheSuppressedRateLimitvariant carries onlyclass: SuppressedClass— no count, no timing, no original-event payload — per the §5.4.8 #5 F13-pin closing the suppressed-event-count covert channel back from the producer's internal state. pub trait DiagnosticSink: Send + Sync + 'staticwith one methodfn emit(&self, event: RefreshDiagnostic). Trait-level contract pins (rustdoc): emission is non-blocking (extends to non-blocking under concurrent emission, foreclosingMutex<VecDeque<_>>- style implementations that re-introduce the producer- liveness hazard at scale); emission/return coherence (every non-CancelledErrreturn is preceded by at least one correspondingRefreshDiagnosticemission before the error returns, withAssertionSink-driven property tests at C7 as the canonical reference per19-validation-surface-discipline.mdc); per-emitter FIFO ordering preserved (the seventh contract pin added by Round 4 review pass F4 = §5.4.6; cross-emitter ordering is undefined); and the in-process-only trust-boundary contract per §5.4.6 / §5.4.8 #4 (full-fidelityRefreshDiagnosticconsumers MUST live inside the wallet trust boundary recursively; cross-process / network-bound consumers receive only projection types sanitized at the boundary).pub struct NoopDiagnosticSink+pub struct TracingDiagnosticSinkship as the Stage 1 sink implementations;TracingDiagnosticSink::emitroutes per-class projections totracing::event!per the Round-4-review-pass F9 audit (variant tag only forDaemonMalformed/DaemonProtocolError/SuppressedRateLimit; bucketedelapsedforDaemonTimeout; bucketeddepthforReorgObserved; bucketedcandidatesforScanProgresswithheightelided), not the fullRefreshDiagnosticDebugimpl.- All trait + enum surface re-exported flat at the
shekyl_engine_corecrate root per the R3 pattern.
-
Stage 1 PR 4 — C6 no-Mock substrate pass (
RefreshEngine/LedgerEnginefailure-injection wrappers) (feat/stage-1-pr4-refresh-engine, 2026-05-20). Lands the C6α + C6β sub-commits of PR 4's substrate pass per the Round 5 amendment (commit8484e669a) and sub-pin extension (commit29cb7e138, F-Mock-1 through F-Mock-8). The pass closes thedocs/FOLLOWUPS.md"Stage 1 retroactive Mock-X cleanup:MockLedger→LocalLedger::from_test_blocks(...)FaultInjecting<LocalLedger>" entry and applies the no-Mock pattern PR 3 established (production-only implementors + composable trait-levelFaultInjecting<T>wrappers) to PR 2's inheritedMockLedgerparallel-implementation.
C6α —
FaultInjecting<R: RefreshEngine>wrapper +test-helpersfeature (commite9310542a):- Adds
test-helpers = []Cargo feature torust/shekyl-engine-core/Cargo.toml(mirrors thebench-internalsprecedent) gating the C6 test-helper surfaces with#[cfg(any(test, feature = "test-helpers"))]per the F-Mock-1 symmetry pin. - Adds
engine/fault_injecting_refresh.rsimplementingFaultInjecting<R: RefreshEngine>with the Option (i) wrapper API (type Error = RefreshError; FIFOMutex<VecDeque<RefreshError>>queue;queue_failure(err)general injector;queued_failures()drain inspector;debug_assert!-on-Drop queue-drain contract per F-Mock-2). - Adds
Engine::replace_refreshtest-only setter onengine/lifecycle.rsmirroring the existingreplace_daemon/replace_ledgerhelpers. - Adds Class 1 trait-surface smoke tests covering empty-queue
passthrough, single-injection-then-delegation, multi-injection
FIFO ordering, and
#[should_panic]queue-drain-on-teardown.
*C6β —
FaultInjecting<L: LedgerEngine>+LocalLedger::from_test_blocksMockLedgerretirement*:
- Adds
engine/fault_injecting_ledger.rsimplementingFaultInjecting<L: LedgerEngine>with the same Option (i) wrapper shape (queue-of-RefreshError,queue_failure/queue_concurrent_mutation/queued_failures,debug_assert!-on-Drop). NotCloneby design — the priorMockLedger'sArc<Mutex<…>>aliasing shape (inherited from CryptoNote test patterns) does not survive the no-Mock transition. - Adds test-only
LocalLedger::from_test_blocks(Vec<Block>)constructor atengine/local_ledger.rs. The V3.0 substrate supports the empty-Veccase only (the sole shape every existingMockLedger-replaced caller needs); non-emptyVecpanics with a forward-pointer to the V3.1TestLedgerBuildersubstrate-design FOLLOWUPS entry. TheVec<Block>signature is load-bearing — V3.1's substrate consumes the body without a signature change per the rationale recorded in the constructor's rustdoc. - Migrates the §5.2 hybrid retry integration test
hybrid_apply_scan_result_retries_on_concurrent_mutation(inengine/refresh.rs) fromMockLedger::with_seed(...)+queue_concurrent_mutation()toFaultInjecting::new(LocalLedger::from_test_blocks(Vec::new()))queue_concurrent_mutation(). The wrapper's non-Cloneposture required restructuring the assertion sites from a cloned handle to read-guard access through the engine'sArc<RwLock<Engine<…>>>; this is the structurally-correct shape (single owner per the no-Mock substrate-inheritance discipline).
- Deletes
MockLedger+MockLedgerState+ROLE_LEDGER+ associated rustdoc + contract tests +derive_seed_pinned_fixture_for_role_ledgertest fromengine/test_support.rs(ROLE_LEDGERbecomes dead weight becauseLocalLedger'sfrom_test_blocksis deterministic and consumes no seed; theROLE_DAEMONHKDF-derivation pinned-fixture test in the same module covers the underlying derivation mechanism). - Updates the stale
MockLedgerreference indocs/V3_ENGINE_TRAIT_BOUNDARIES.md§1.2 (the only active-doc factual claim that namedMockLedgeras the current substrate; the broader historical references in §§4+ and the PR 2 / PR 3 design docs remain as historical-record prose per the15-deletion-and-debt.mdc"while we're here" discipline).
C6γ —
MockDaemon→TestDaemonrename:- Mechanical rename of the test-substitute type and every call
site across
engine/test_support.rs(struct,impl Rpc,impl DaemonEngine, module docstrings),engine/refresh.rs,engine/lifecycle.rs,engine/mod.rs,benches/common/engine_fixture.rs(forward-pointer comment), andCargo.toml(ChaCha20Rngrationale comment). - Structural shape unchanged — the type is still an alternative
real implementation that serves canned / cached test responses
without network connectivity (per PR 3 §2.1.2's distinction
between "alternative real implementation" and "parallel-
implementation fake"). Only the naming changed:
TestDaemonsignals the role correctly per the no-Mock substrate- inheritance discipline. - Active-doc trajectory updates in
docs/V3_ENGINE_TRAIT_BOUNDARIES.md§1.2 (GenericDaemonClienttrajectory row), §1.4 rename-chain note, §6.1 hybrid-test discussion, §6.2 RNG-seed pin, §3.5Rpc-impl rationale, and the §"Linked file paths" inventory entry (rename chain extended:MockRpc→MockDaemon→TestDaemon).
Test gates (post-C6).
cargo fmt --all -- --checkclean;cargo clippy -p shekyl-engine-core --all-targets --features test-helpers -- -D warningsclean;cargo clippy -p shekyl-engine-core --all-targets -- -D warningsclean (default features);cargo test -p shekyl-engine-core --lib152/152 pass including the migrated hybrid retry test;cargo check -p shekyl-engine-core(default +--features test-helpers+--tests+--benches+--workspace --tests) all green.C7 — hybrid retry test + property tests (
AssertionSink/PanickingSink) (commitc9e65bbc6):- Refactors
Engine::replace_refreshatengine/mod.rsfrom a&mut selfsetter into a consume-and-rebuild constructor (fn replace_refresh<R2: RefreshEngine>(self, refresh: R2) -> Engine<S, D, L, R2>) mirroring the existingreplace_daemon/replace_ledgershape atengine/lifecycle.rs. The refactor lets the genericRtype parameter change between construction and replacement so test orchestration can build anEngine<…, LocalRefresh>at assemble time and rewire it toEngine<…, FaultInjecting<LocalRefresh>>for failure-injection scenarios without going through adyn-erased trait object. - Adds
AssertionSink,PanickingSink, and thePanickingSinkTriggerconfiguration enum toengine/diagnostics.rs, all gated#[cfg(any(test, feature = "test-helpers"))]per the F-Mock-1 cfg-symmetry pin.AssertionSinkrecords emittedRefreshDiagnosticevents for post-hoc coherence assertions;PanickingSinkpanics on configured trigger events to exercise producer panic-safety. - Adds
proptest = "1"as adev-dependencyinrust/shekyl-engine-core/Cargo.tomlpowering the new producer property tests below. - Adds the hybrid retry test
hybrid_refresh_engine_orchestrator_cancellation_retriesatengine/refresh.rsthat exercises the producer-trait / orchestrator cancellation-checkpoint split end-to-end against the fully-composedEngine<SoloSigner, TestDaemon, FaultInjecting<LocalLedger>, FaultInjecting<LocalRefresh>>stack, verifying the orchestrator retries onConcurrentMutation(driven byFaultInjecting<LocalLedger>::queue_concurrent_mutation) and surfaces cancellation cleanly whenFaultInjecting<LocalRefresh>injectsRefreshError::Cancelled. - Adds the
producer_property_testsmodule atengine/local_refresh.rswith five parametric coherence tests, oneproptest!-driven fuzz test (coherence_proptest_fuzz_chain_and_injection) exercising randomized chain length + failure-injection scenarios, four panic-safety tests verifying clean unwind throughPanickingSinkpanics acrossDaemonMalformed/DaemonProtocolError/ScanProgress/Anytriggers plus a recovery test, and a classifier sanity test. The coherence tests exercise the §5.4.6 emission/return coherence pin: every non-CancelledRefreshErroris preceded by a correspondingRefreshDiagnosticemission. The panic-safety tests verify the §5.4.6 producer-side robustness property:Scannerzeroizes cleanly viaDropacross a panickingemit, cancellation-token state remains well-defined, and the refresh attempt fails predictably without corrupting interior state. Tests are deterministic via a compile-time-generatedPROPERTY_TEST_MASTER_SEEDand#[tokio::test(start_paused = true)]for fake-time async scheduling.
Test gates (post-C7).
cargo fmt --all -- --checkclean;cargo clippy -p shekyl-engine-core --all-targets --features test-helpers -- -D warningsclean; default-feature clippy clean;cargo test -p shekyl-engine-core --features test-helpers --lib170/170 pass (152 → 170: +18 C7 tests);cargo doc -p shekyl-engine-core --features test-helpers --no-depsgreen with no new doc warnings (pre-existing intra-doc-link warnings to private items are baseline and unrelated to C7 changes).C8 — docs propagation (this commit):
- This CHANGELOG entry extended with the C7 sub-section above and the C8 sub-section here.
docs/design/STAGE_1_PR_4_REFRESH_ENGINE.mdgains the Phase-1-landed Status-banner closure paragraph enumerating C0–C8 landing SHAs; §7.X gains per-Commit CnLanded:lines anchoring each commit's SHA inline next to the design-time prose.docs/V3_ENGINE_TRAIT_BOUNDARIES.md§2.3 past-tenses the "Stage 1 surface" header and cross- references the as-landed implementation locators (engine/traits/refresh.rs,engine/diagnostics.rs,engine/local_refresh.rs,engine/mod.rs,engine/fault_injecting_refresh.rs,engine/fault_injecting_ledger.rs) with their commit SHAs (C1 / C2 / C4 / C5a / C6α / C6β).docs/FOLLOWUPS.mdgains a Phase 0d explicit retirement note ("struck, not deferred") at the top of the V3.x section, distinguishing the Round 2 composition reframe's struck-candidate from the live R5 / R6 / R4 (c) V3.x consumer-actor deferrals that remain open per Round 3's prior amendments. The pre- existing closed-entries for Mock-X cleanup (MockLedger→FaultInjecting<LocalLedger>+LocalLedger::from_test_blocksandMockDaemon→TestDaemon) carry the[CLOSED 2026-05-20]marker from C6β / C6γ landing and are unchanged in C8.
C9 — FOLLOWUPS P1 / P2 / P3 re-anchor post-Phase-1 landing (this commit):
- Doc-only follow-up commit; not in the original Round 4
C0–C8 decomposition but added post-PR-open per the
user-directed "correct known document errors within the
current PR" trigger (per
.cursor/rules/91-documentation-after-plans.mdc's stale-doc detection discipline and.cursor/rules/15-deletion-and-debt.mdc's "deferred without a named home is the failure mode" framing). Surfaced during a post-C8 review ofdocs/FOLLOWUPS.mdagainst the actual code state inengine/local_ledger.rs:356–367(trait-methodapply_scan_resultdiscardsVec<usize>and short- circuitspopulate_engine_handle_fields) andengine/merge.rs:181–215(inherentEngine::apply_scan_resultruns the post-pass against the capturedinsertedindices) — the two paths diverge by construction in the post-Phase-1 substrate. docs/FOLLOWUPS.mdP1 / P2 / P3 entries rewritten with Post-PR-4-Phase-1 substrate subsections- substrate-anchored reopening criteria per
.cursor/rules/21-reversion-clause-discipline.mdc. The pre-Phase-1 "defer to PR 4" dispositions all assumed α/β/γ Round 1 would reshape the producer/consumer pattern and theLedgerEngine::apply_scan_resulttrait surface, absorbing P1 / P2 / P3 as a side effect. Phase 1 settled on α (preserved current shape; trait surface unchanged) perSTAGE_1_PR_4_REFRESH_ENGINE.md§5.4 Round 1, and did not absorb the three items. P1's hard precondition ("PR 4 lands before any binary integratesRefreshHandle") survives intact and is restated as "P1 closes before any binary integratesRefreshHandle". Each entry's re-anchored disposition names a focused follow-up PR landing V3.0 pre-genesis: P1 →refresh/p1-async-path-post-pass(two candidate closing shapes both feasible against the post-Phase-1 substrate — shape (b)RefreshEngineowns the merge post-pass is newly available because PR 4 landed theRefreshEnginetrait at C1 / C4); P2 →refresh/p2-wallet-birthday-plumbing(substrate well-defined:LocalRefresh::newis the V3.0 production implementor per C4 =ac100e1ab); P3 → downstream of P1, closes alongside P1 in the same focused PR (both candidate P1-closing shapes close P3 as a side effect; P3 stays catalogued separately to preserve the Copilot PR #37 audit trail).
- substrate-anchored reopening criteria per
docs/design/STAGE_1_PR_4_REFRESH_ENGINE.md§5.5 named-home table rows P1 / P2 / P3 updated with a bold Phase 1 landed without absorption marker plus one-sentence cross-refs to the re-anchored FOLLOWUPS dispositions, preserving the §5.5 audit-trail discipline per15-deletion-and-debt.mdc.STAGE_1_PR_4_REFRESH_ENGINE.md§7.X Status banner extended to enumerate C9 alongside C0–C8; the same §7.X gains a new**Commit C9 — FOLLOWUPS P1 / P2 / P3 re-anchor post-Phase-1 landing**block documenting design-time intent and landing SHA, mirroring the per-commit documentation pattern from C0–C8.- Gate inheritance from C8: C9 is doc-only, so
cargo fmt --check,cargo clippy -- -D warnings,cargo test --lib, andcargo doc --no-depsall inherit C8's results unchanged (170 / 170 lib tests pass; fmt clean; clippy clean under both default andtest-helpersfeatures; 48 doc warnings unchanged at the C7 baseline).
C10 – C13 — Copilot post-PR-open review responses:
- Four small post-PR-open commits closing the nine
line-anchored findings the GitHub Copilot review raised
against
95affda61(C8 head before C9 push) on PR #60. Each commit is scoped to a single concern (file + correction class) per.cursor/rules/90-commits.mdc's scope-per-commit discipline; each commit cites its Copilot finding IDs in the commit message body. Doc-only / harness-only; no API surface, no trait body, and no production code-path touched. - C10
60f401e77— scanner rustdoc fn-name corrections inrust/shekyl-scanner/src/scan.rs. Six sites updated from pre-C4scan_transactionto C4-landedscan_transaction_with_cancel, plus the gate-test rustdoc return-type updated fromOk(Timelocked::empty())toOk(ScanOutcome::Completed(Timelocked(empty)))to match the actualScanOutcomevariant the gate returns. Closes Copilot finding IDs 3278232594 / 3278232649 / 3278232666 / 3278232686 plus two same-class adjacent sites discovered during the audit. - C11
949e42bd8—bench_fixturesrustdoc fact-fix inrust/shekyl-scanner/src/bench_fixtures.rs. Themake_bench_walletspend-secret comment cited the on-chain spend point as the basepoint whenfake_spend_key_bytes()actually returns2 * G. Thefake_spend_key_bytes()rustdoc opening was internally contradictory and is rewritten as a clean three-property justification (torsion-free; non-default; distinct fromG). Behaviour unchanged —fake_spend_key_bytes()body still returns(2 * G).compress().to_bytes()byte-identically; F11-S cold-cache audit-trail unaffected. Closes Copilot finding IDs 3278232628 / 3278232770. - C12
20b082a38— refresh-trait checkpoint-list temporal-firing-order explanation inrust/shekyl-engine-core/src/engine/traits/refresh.rs. TheRefreshEnginetrait rustdoc lists checkpoints in temporal-firing order (1 → 2 → 3 → 5 → 4) rather than numeric order. Copilot read this as out-of-order, but the numbering is repo-wide audit-trail convention preserving "checkpoint 5 added per PR 4 Round 4 F2". Synchronized renumbering would touch 12+ cross-reference sites and dissolve the F2-audit-trail provenance; rejected per.cursor/rules/21-reversion-clause-discipline.mdc's substrate-anchored disposition. Fix applied: add an explanatory paragraph to the trait rustdoc that names the temporal-firing-order convention explicitly so the question isn't re-litigated. Closes Copilot finding ID 3278232791. - C13
262ece667— scan-transaction warm-cache bench harness clone-out-of-timed-region fix inrust/shekyl-scanner/benches/scan_transaction.rs. Both warm-cache benchmark variants usediter_batched_refwith an in-routinemem::replace(b, block.clone()), placingScannableBlock::cloneinside the timed region. Switched toiter_batched(|| block.clone(), |block| scanner.scan(block), ..)so the clone is in the setup closure and onlyScanner::scanis measured. F11-S audit-trail impact: ZERO — the F11-S binding measurement (perdocs/design/STAGE_1_PR_4_REFRESH_ENGINE.md§3.1 / §5.4.9 / §7.Y) is anchored on the cold-cache N=16 worst-case p99 (12.95 ms per-tx / 819 µs per-output), and the cold variant was already methodologically correct (all setup outside the timed region). Captured F11-S numbers ata4da2212aand the C4 per-output safe-point disposition stand without revision. Closes Copilot finding IDs 3278232713 / 3278232736. - Gates per commit: each commit ran its scoped bisection-
discipline gates against the affected crate
(
shekyl-scannerfor C10 / C11 / C13;shekyl-engine-corefor C12). Test counts and doc- warning baselines unchanged: 57 / 57 scanner lib tests pass; 170 / 170 engine-core lib tests pass; scanner doc warnings = 2 (C8 baseline); engine-core doc warnings = 49 (C9 baseline). C13 additionally rancargo check --benchesto confirm the bench targets compile under the newiter_batchedshape.
C14 —
[Unreleased]doc-after-plans propagation for C10 – C13 (this commit):- Doc-only follow-up commit per
.cursor/rules/91-documentation-after-plans.mdc's final-task-always rule. After C10 / C11 / C12 / C13 landed locally with green gates, the design doc §7.X Status banner (line ~478) was extended to enumerate C10 – C13 alongside C0 – C9 with landing SHAs and per-commit one-paragraph summaries, and the §7.X commit-block section gained a new**Commits C10 – C13 — Copilot post-PR-open review responses.**block with the same per-commit prose + F11-S impact statement + gate evidence. The C9 block's placeholder**Landed: this commit**was replaced with the landed SHA839c4bbfd. This*C10 – C13 — Copilot post-PR-open review responses*subsection above is the matching CHANGELOG entry; the doc-after-plans propagation also updates the closing C0–C13 paragraph below. - Gate inheritance from C13: C14 is doc-only, so the
cargo fmt --check,cargo clippy --all-targets -- -D warnings,cargo test --lib, andcargo doc --no- depsgates all inherit C13's results unchanged (no rust files touched in C14).
C15 – C16 — Copilot second-round review responses:
- Two small post-PR-open commits closing the four
additional line-anchored findings the GitHub Copilot
reviewer raised against
30798d783(the C14 push head) on PR #60. Both batches touchengine/-side rustdoc and harness surfaces only; doc-only / harness-only; no API surface, no trait body, and no production code-path touched. Each commit cites its Copilot finding IDs in the commit message body per.cursor/rules/90-commits.mdc. - C15
bafb9c548— refresh-trait[LocalRefresh]rustdoc link target fix inrust/shekyl-engine-core/src/engine/traits/refresh.rs. Two[LocalRefresh]reference-link aliases (lines 204 + 258 of theRefreshEnginetrait file) pointed atsuper::super::Engineinstead ofsuper::super::LocalRefresh. The misroute was silent (the alias target is a valid path; rustdoc accepts it) but the rendered docs at the two body sites (lines 48 and 244) linked "LocalRefresh" to theEnginestruct rather thanLocalRefresh. Correct target verified at source:LocalRefreshlives atengine/local_refresh.rs:250, re-exported atengine/mod.rs:187; fromengine::traits::refresh,super::super::LocalRefreshresolves through the re-export (matching the working precedent atengine/fault_injecting_refresh.rs:105). Closes Copilot finding IDs 3278391428, 3278391456. - C16
376e1e821—FaultInjecting<R>+FaultInjecting<L>Drop-timedebug_assert!message fix inrust/shekyl-engine-core/src/engine/fault_injecting_refresh.rsandrust/shekyl-engine-core/src/engine/fault_injecting_ledger.rs. Both Drop messages told test authors to "drain viaqueued_failures()andconsume_or_inject". Neither instruction was usable:consume_or_injectdoes not exist anywhere in the workspace (rg -nF 'consume_or_inject'returned only the two message-body sites — leftover prose from an earlier API draft), andqueued_failures()is ausizeinspector, not a drain. Rewritten to direct readers at the real drain mechanism —produce_scan_result(..)for the refresh wrapper,apply_scan_result(..)for the ledger wrapper — withqueued_failures()cited explicitly as the inspector. The two#[should_panic(expected = ...)]test attributes (fault_injecting_ledger.rs:528,fault_injecting_refresh.rs:590) re-pinned to the new substring shape in the same commit per scope-per- commit discipline (mechanical follow-on of the production-message edit). The refresh-sideshould_panichad also been pinned on the older "FaultInjecting" (without<R>) spelling; both ledger and refresh assertions now consistently include the generic-parameter suffix. Closes Copilot finding IDs 3278391467, 3278391479. - Gates per commit: each ran its scoped bisection-
discipline gates against
shekyl-engine-core(fmt --check, clippy --all-targets -- -D warnings, test --lib, doc --no-deps). C15 doc-only (rustdoc-target); C16 production message + same-scopeshould_panicre-pin. Test counts unchanged at 170 / 170 lib tests pass; doc warnings unchanged at 49 (C9 baseline). The two re-pinnedshould_panictests both confirm the new substrings.
C17 —
[Unreleased]doc-after-plans propagation for C15 – C16:- Doc-only follow-up per
.cursor/rules/91-documentation-after-plans.mdc's final-task-always rule. After C15 / C16 landed locally with green gates, the design doc §7.X status banner was extended to enumerate C14 / C15 / C16 alongside C0 – C13 with landing SHAs and per-commit one-paragraph summaries, and the §7.X commit-block section gained a new**Commits C15 – C16 — Copilot post-PR-open second-round review responses.**block with per-commit prose + Copilot finding IDs + gate evidence. This*C15 – C16 — Copilot second-round review responses*subsection above is the matching CHANGELOG entry; the doc-after-plans propagation also updates the closing C0–C20 paragraph below. - Gate inheritance from C16: C17 is doc-only, so the
cargo fmt --check,cargo clippy --all-targets -- -D warnings,cargo test --lib, andcargo doc --no-depsgates all inherit C16's results unchanged (no rust files touched in C17).
C18 – C20 — Copilot third-round review responses:
- Three small post-PR-open commits closing the three
additional line-anchored findings the GitHub Copilot
reviewer raised against
966154d27(the C17 push head) on PR #60. The findings clustered on substantive discipline questions rather than rustdoc cosmetics: F11-S cancellation safe-point completeness, dead-arm invariant enforcement, and cryptographic-decoding constant-time-or-explicit-rejection discipline. Each commit cites its Copilot finding ID in the commit message body per.cursor/rules/90-commits.mdc. - C18
6cc22965f—Scanner::scan_with_cancelper-tx safe-point cancellation check inrust/shekyl-scanner/src/scan.rs. The F11-S binding's between-tx safe-point perRefreshEnginetrait rustdoc checkpoint 5 was delivered only via the inner per-output iter-0 check insidescan_transaction_with_cancel. For transactions whose per-output loop never runs (zero-output txs;tx.version() != 2; malformedextra; oversized per the defense-in-depth size gate) the inner check is bypassed and the outer per-tx loop delegated straight back without cancellation opportunity. Worst case: a block ofNsuch transactions deferred cancellation byN × O(1)-per-tx-skipcost rather than bounded at a single tx-entry's cost. Fix addsif is_cancelled() { return Cancelled }at the outer per-tx loop entry, rewrites the misleading "subsumed by per-output check at iter 0" comment to describe the new two-checkpoint shape, and adds theouter_per_tx_loop_cancellation_fires_for_zero_output_txregression test (V2 miner-only block viaInput::Gen(0)+ empty outputs/extra). Thecancel_testsmodule rustdoc was simultaneously updated from a three-axis to a four-axis taxonomy naming the outer-loop per-tx boundary explicitly. F11-S benchmark impact: zero — added check is one closure invocation per tx, a few nanoseconds amortized acrossN_outputsper tx and well below the F11-S worst-case per-output cost. Closes Copilot finding ID 3278452877. - C19
5749f444c— deadScanOutcome::Cancelledarmdebug_assert!inInternalScanner::scaninrust/shekyl-scanner/src/scan.rs. The function delegates toscan_with_cancelwith a never-cancelling closure (|| false); under the closure-invariant, theCancelledvariant is unreachable. The previous code mapped the unreachable variant toOk(Timelocked(Vec::new()))for production-panic-free behavior — but the empty-result fallback would silently mask future logic-dispatch regressions. Fix addsdebug_assert!(false, …)naming the closure-invariant before the empty-result fallback, so debug-mode tests catch the violation immediately while production behavior is unchanged. Discipline (preferringdebug_assert!overunreachable!()) named in the same arm's comment so a future refactor preserves the rationale. Closes Copilot finding ID 3278452893. - C20
3331fb82e—ViewMaterial::try_from_keysview_scalar canonical-bytes decoding inrust/shekyl-engine-core/src/engine/view_material.rs. The previous reconstruction viaScalar::from_bytes_mod_order(*keys.view_sk .as_canonical_bytes())silently reduces non-canonical / corrupted input to a canonical scalar — masking in-memory corruption of view-key state and producing a scalar that is NOT the wallet's actual view secret on bad input. The same construction site (lines 211–222) validateskeys.spend_pkwith explicitIoError::Scanneron non-canonical bytes; the asymmetric treatment of view-scalar vs. spend-public-key was not justified by the threat model. Fix switches toOption::<Scalar>::from(Scalar::from_canonical_bytes(...)) .ok_or_else(|| RefreshError::Io(IoError::Scanner { detail: ... }))?. On canonical input the resulting scalar is bit-identical to the pre-fix output; on non-canonical input the conversion returnsNoneand maps toRefreshError::Io(IoError::Scanner)with an operator-actionable detail string. The rustdoc's field-derivation summary and# Errorsblock were both updated to describe the new shape and cite30-cryptography.mdc's constant-time-or-explicit- rejection discipline as the anchor. Closes Copilot finding ID 3278452905. - Gates per commit: each ran its scoped
bisection-discipline gates against the touched crate
(C18 / C19:
shekyl-scanner; C20:shekyl-engine-core) plus downstreamshekyl-engine-coreregression for the scanner-side changes (fmt --check, clippy --all-targets -- -D warnings, test --lib, doc --no-deps). Scanner test count: 57 → 58 (C18 added regression test; C19 unchanged). Engine-core test count unchanged at 170 / 170 lib tests pass. Scanner doc warnings unchanged at 2 (C8 baseline). Engine-core doc warnings unchanged at 49 (C9 baseline).
C21 —
[Unreleased]doc-after-plans propagation for C18 – C20 (this commit):- Doc-only follow-up per
.cursor/rules/91-documentation-after-plans.mdc's final-task-always rule. After C18 / C19 / C20 landed locally with green gates, the design doc §7.X status banner was extended to enumerate C18 / C19 / C20 alongside C0 – C17 with landing SHAs and per-commit one-paragraph summaries, and the §7.X commit-block section gained a new**Commits C18 – C20 — Copilot post-PR-open third-round review responses.**block with per-commit prose + Copilot finding IDs + gate evidence. This*C18 – C20 — Copilot third-round review responses*subsection above is the matching CHANGELOG entry; the doc-after-plans propagation also updates the closing C0–C21 paragraph below. - Gate inheritance from C20: C21 is doc-only, so the
cargo fmt --check,cargo clippy --all-targets -- -D warnings,cargo test --lib, andcargo doc --no-depsgates all inherit C20's results unchanged (no rust files touched in C21).
C22 – C23 — Copilot fourth-round review responses:
- Two small post-PR-open commits closing the five
additional line-anchored findings the GitHub Copilot
reviewer raised against
5557b3192(the C21 push head) on PR #60. Four of the five findings clustered on a single class (staleexpect()panic-message references in the bench harness) and bundle into a single mechanical commit; the fifth is a substantive test-discipline refinement and lands separately. Each commit cites its Copilot finding ID(s) in the commit message body per.cursor/rules/90-commits.mdc. - C22
168ff0e22— stalescan_transaction_with_cancelexpect()strings inrust/shekyl-scanner/benches/scan_transaction.rs. Four.expect("scan_transaction_with_cancel must not error on well-formed fixture")sites (warm + cold variants of the worst-case and typical-case bench groups) referenced the private inner helper but the call sites themselves invoke the public surfaceScanner::scan(..). The mismatch is the same class as the C10 commit (60f401e77) that rewrote six rustdoc fn-name references inscan.rspost the C4 rename + split (ac100e1ab); C22 closes the bench- file residue C10's review-attention scope didn't cover. Fix updates all four sites to"Scanner::scan must not error on well-formed fixture"; rustfmt collapsed the now-shorter message to single-line form. No semantic change (panic messages only fire onErr, and the bench fixtures'Scanner::scaninvocations never produceErrby construction). Operator-facing diagnostic discipline (audit-trail clarity when a bench panics in CI). Closes Copilot finding IDs 3278543704, 3278543738, 3278543753, 3278543764. - C23
a2f173c73— replace Debug-substring with structuralCryptoError::DecapsulationFailedmatch inrust/shekyl-scanner/src/bench_fixtures.rs. Thetypical_case_first_output_exits_via_view_tag_mismatchsanity-check test assertedformat!("{err:?}") .contains("X25519 view tag mismatch")to verify the fast-path-rejection error class — brittle to Debug- format changes (re-derivation, additional context fields, terse-vs-verbose variants) per Copilot's test-discipline finding. Validation at source confirmsscan_output_recoverconstructs multipleDecapsulationFailed(String)instances along distinct early-exit paths (view-tag mismatch, invalid ML-KEM ciphertext length, invalid decap key, ML-KEM decap rejection); a pure variant-only check would not distinguish the typical-case fixture's intended path from sibling reasons, so the substring check on the inner message IS load-bearing. Fix uses a let-else binding both the variant AND the innerStringfield followed by a separate inner-messageassert!— the two-class pinning (variant + reason within variant) is preserved; only the FORM changes (binding the innerStringdirectly via pattern-match rather than going throughformat!("{err:?}")). Comment rewritten to enumerate the two drift classes the new shape catches explicitly.CryptoErrorimported via the existingshekyl_crypto_pq::errorpublic path. Closes Copilot finding ID 3278543725. - Gates per commit: C22 ran
cargo fmt -p shekyl- scanner -- --check(auto-format applied to collapse the shorter message to single-line; second --check clean) +cargo clippy -p shekyl-scanner --all- targets -- -D warnings(clean) +cargo build -p shekyl-scanner --benches(clean) +cargo test -p shekyl-scanner --lib(58 / 58 pass; unchanged from C19). C23 ran the same scoped gates plus a targetedcargo test ... typical_case_first_output_exits_via_view_tag_mismatch -- --nocaptureto confirm the new structural form classifies the fixture's view-tag-mismatch error correctly (1 / 1 pass). Scanner doc warnings unchanged at 2 (C8 baseline).
C24 —
[Unreleased]doc-after-plans propagation for C22 – C23 (this commit):- Doc-only follow-up per
.cursor/rules/91-documentation-after-plans.mdc's final-task-always rule. After C22 / C23 landed locally with green gates, the design doc §7.X status banner was extended to enumerate C22 / C23 alongside C0 – C21 with landing SHAs and per-commit one- paragraph summaries, and the §7.X commit-block section gained a new**Commits C22 – C23 — Copilot post-PR-open fourth-round review responses.**block with per-commit prose + Copilot finding IDs + gate evidence. This*C22 – C23 — Copilot fourth-round review responses*subsection above is the matching CHANGELOG entry; the doc-after-plans propagation also updates the closing C0–C24 paragraph below. - Gate inheritance from C23: C24 is doc-only, so the
cargo fmt --check,cargo clippy --all-targets -- -D warnings,cargo test --lib, andcargo doc --no-depsgates all inherit C23's results unchanged (no rust files touched in C24).
C25 – C28 — Copilot fifth-round review responses:
- Four small post-PR-open commits closing the five
additional line-anchored findings the GitHub Copilot
reviewer raised against
3f4460a59(the C24 push head) on PR #60. All five findings are substantive doc/code-hygiene issues (none nitpicky): three are stale-doc references to deleted symbols / abandoned test substrates (per.cursor/rules/91-documentation-after-plans.mdc's "Stale-doc detection ... the doc update is not optional — the doc is wrong and will mislead readers" rule); one is a dead lint-allow attribute (per.cursor/rules/15-deletion-and-debt.mdc's "Default: delete"); one is a Cargo feature description that claimed re-exports the feature doesn't actually perform. - C25
543fffe23— stalebuild_scanner_from_keysrustdoc / comment references inrust/shekyl-engine-core/src/engine/mod.rs(comment abovepub(crate) fn keys()) andrust/shekyl-engine-core/src/engine/view_material.rs(module rustdoc § "Field shape"). The free functionbuild_scanner_from_keyswas deleted in C5β (b6a1274de— legacy producer-scaffolding deletion inengine/refresh.rs) and replaced byViewMaterial::try_from_keys(&AllKeysBlob)(engine assembly time, per C5a =553d70139) +LocalRefresh::build_scanner(per-attempt scanner construction, per C4 =ac100e1ab). Both LIVE Rust sites updated to name the actual current derivation path; the surviving live consumer ofEngine::keys()(Engine::replace_refresh's test- substrate re-derivation per C6α =e9310542a) named explicitly; reopening-criterion clause added per.cursor/rules/21-reversion-clause-discipline.mdcnaming Phase 2'ssign_transfer/tx_proof/reserve_proofsurfaces as the substrate-change that would reopen#[allow(dead_code)]deletion. Initial rewrite introduced an[Engine::replace_refresh](super::Engine::replace_refresh)intra-doc link that triggered a new rustdoc privacy warning (replace_refreshispub(crate), link frompubview_materialmodule's rustdoc unresolves); reverted to a plain backtick reference per the C18 cross-crate-link mitigation pattern; doc-warning count back to baseline 49. Closes Copilot finding IDs 3278677182, 3278677211. - C26
1cdcd6e52— dead#[allow(unused_imports)]onpub(crate) use refresh::RefreshEnginere-export inrust/shekyl-engine-core/src/engine/traits/mod.rs. The suppression was load-bearing at C1's introduction commit (d3edc1abb) when the re-export landed ahead of consumers; C5 (7140f726a—Engine<S, D, L, R>four-parameter type slot + retry-loop migration to trait dispatch) introduced multiple production consumers making the import live. The suppression has not been load-bearing since C5 and now masks future regressions where the import becomes dead again. Removed per.cursor/rules/15-deletion-and-debt.mdc's "Default: delete"; accompanying comment rewritten to anchor C1 / C5 / C26 and explain the masking-future- regressions failure mode the removal prevents. Symmetric form to C25's update ofEngine::keys()'s#[allow(dead_code)](same discipline check, different disposition because that suppression's live-consumer audit surfaced an ongoing default-feature production justification). Closes Copilot finding ID 3278677226. - C27
15c76a73e— rewordtest-helpersCargo feature description inrust/shekyl-engine-core/Cargo.tomlto reflect that the feature gates compilation only, NOT public re-exports. The previous description claimed the feature "re-exports otherwise-pub(crate)failure-injection wrappers ... for downstream integration test crates", but the four named surfaces (FaultInjecting<R: RefreshEngine>,FaultInjecting<L: LedgerEngine>,Engine::replace_refresh,LocalLedger::from_test_blocks) remainpub(crate)with the feature enabled — no__test_helpersre-export module exists at the crate root (verified at source vs. the siblingbench-internalsfeature which DOES have a__bench_internalsre-export atlib.rs:46-56). Per.cursor/rules/21-reversion-clause-discipline.mdcchose option (b) of Copilot's two options: reword to reflect actual shape, NOT speculatively add re- exports for hypothetical downstream consumers that don't yet exist (the "pre-provisioning for hypothetical consumers" anti-pattern). The rewritten comment names: what the feature actually does (compile-gate the fourpub(crate)surfaces); what it does NOT do (no public re-exports; compare-and- contrast withbench-internalsmakes the asymmetry explicit); why no re-exports yet (pre-genesis no-consumer state); reopening criteria (when the first downstream consumer emerges, add__test_helpersmodule under the__bench_internalsprecedent + V3.0-targeted FOLLOWUPS item +AUDIT_SCOPE.mdamendment if needed); load-bearing production-build safety property (the#[cfg(any(test, feature = "test-helpers"))]gating at the definition site keeps the four failure-injection surfaces out of default-feature production builds). Closes Copilot finding ID 3278677251. - C28
1879baf73— Post-PR-4 retirement note added todocs/V3_ENGINE_TRAIT_BOUNDARIES.md§6 "Test boundary" / §6.1 "Pinned commitments for Stage 1". The §6 framing still asserted a "fully- mockedEngine<SoloSigner, MockKey, MockLedger, MockDaemon, …>" Stage-1 test direction and the §6.1 Round-3 commitment list still enumerated all seven Mock-X types, but three of the seven have retired:MockKeyin PR 3 (perSTAGE_1_PR_3_KEY_ENGINE.md§6.4 no-Mock substrate — already acknowledged in §6.1 Round-4b's(Post-M3 note)but NOT in the §6 framing paragraph);MockLedgerin PR 4 C6β (replaced byFaultInjecting<L: LedgerEngine>);MockRefreshin PR 4 C6α (replaced byFaultInjecting<R: RefreshEngine>). Three additions: (1) new> (Post-M3 + Post-PR-4 note: ...)block-quote beneath the §6 opening paragraph naming all three retirements + replacement substrates + surviving Mock-X types; (2) nested(Post-M3 + Post-PR-4 update to the Round-3 list)item inside §6.1's pinned-commitments list inline-annotating each retired type with its anchor commit + replacement; (3) extension of the existing(Post-M3 note: ...)paragraph inside §6.1 Round-4b to include the Post-PR-4 retirements + name the contract-fidelity discipline as applying toFaultInjecting<...>wrappers (which honor the trait contract by delegating to the wrapped real production implementor's behavior — wrapper-injected failures fire BEFORE or AFTER delegation per the wrapper's documented semantics, not by substituting alternative return values). §6.2+ RNG-injection example snippets (lines 4102, 4149, 4177) retainMockLedger::with_seed(...)literal example text as a deliberate scope decision per15-deletion-and-debt.mdc"while we're here is the enemy" — those examples demonstrate the seeded-RNG injection MECHANISM (invariant under implementor name) and rewriting them would either lose pedagogical clarity or require a §6.2+ refactor outside C28's named-Copilot-finding scope. Closes Copilot finding ID 3278677269. - Gates per commit: each ran its scoped bisection-
discipline gates. C25 / C26 touched
shekyl-engine-coreRust files (cargo fmt -p shekyl-engine-core -- --check,cargo clippy -p shekyl-engine-core --all-targets --features test-helpers -- -D warnings, default-feature clippy,cargo test -p shekyl-engine-core --lib,cargo doc -p shekyl-engine-core --no-deps) all clean: 170 / 170 lib tests pass; 49 doc warnings unchanged (C9 baseline). C27 touchedCargo.tomlonly (comment-only change inside[features]); fmt / clippy / test all clean; no.rsfiles touched. C28 touched adocs/markdown file only; gate inheritance from C27.
C29 —
[Unreleased]doc-after-plans propagation for C25 – C28 (this commit):- Doc-only follow-up per
.cursor/rules/91-documentation-after-plans.mdc's final-task-always rule. After C25 / C26 / C27 / C28 landed locally with green gates, the design doc §7.X status banner was extended to enumerate C25 / C26 / C27 / C28 alongside C0 – C24 with landing SHAs and per-commit one-paragraph summaries, and the §7.X commit-block section gained a new**Commits C25 – C28 — Copilot post-PR-open fifth-round review responses.**block with per-commit prose + Copilot finding IDs + gate evidence. This*C25 – C28 — Copilot fifth-round review responses*subsection above is the matching CHANGELOG entry; the doc-after- plans propagation also updates the closing C0–C29 paragraph below. - Gate inheritance from C28: C29 is doc-only, so the
cargo fmt --check,cargo clippy --all-targets -- -D warnings,cargo test --lib, andcargo doc --no-depsgates all inherit C28's results unchanged (no rust files touched in C29).
PR 4 §7.X commits C0 through C29 are now all landed; PR #60 carries the full C0–C29 set. See the separate
### Addedand### Changedentries below for the trait- surface andEngine<S, D, L, R>four-parameter additions PR 4 ships, per the C8 spec atSTAGE_1_PR_4_REFRESH_ENGINE.md§7.X C8. -
RandomX v2 — Phase 1: pinned submodule + out-of-tree build wiring (
feat/randomx-v2-phase1, PR #54, merge commitc0c4a11e5, 2026-05-19). Addsexternal/randomx-v2submodule pinned to Shekyl-Foundation/RandomX SHAaaafe71322df6602c21a5c72937ac284724ae561(v2.0.1 release; identical totevador/RandomX:masterat pin time, per the dependency-discipline verification indocs/completed/RANDOMX_V2_PHASE1_PLAN.md§1.3). AddsBUILD_RANDOMX_V2_MINER_LIBCMake option (defaultOFF). WhenONon a single-config generator (Ninja, Make), anExternalProject_Addblock inexternal/CMakeLists.txtbuilds the v2 fork out-of-tree under${CMAKE_BINARY_DIR}/external/randomx-v2-build/and exposes theshekyl_randomx_v2IMPORTEDstatic-library target plus its include directory. The block forwards the standard CMake cross-build knobs (toolchain file, sysroot, Apple/Android settings, system name/processor, compiler launchers) to the sub-build via a semicolon-safeLIST_SEPARATOR-based forwarding pattern. On multi-config generators (MSVC, Xcode, Ninja Multi-Config) the option fails with aFATAL_ERRORdirecting the developer to-G Ninjaplus an explicit-DCMAKE_BUILD_TYPE; per-CONFIGwiring is the V3.x Phase 2 FOLLOWUPS item alongside the first real consumer. The out-of-tree build pattern avoids the target-name collision withexternal/randomx(v1.2.1), which declares the sameproject(RandomX)andadd_library(randomx ...)symbols; seeRANDOMX_V2_PHASE1_PLAN.md§2 for the collision analysis and disposition rationale. No Shekyl C++ consumer links the new target in this PR; first consumers are Phase 2 cross-check tests against the canonical v2 implementation (the new Rust craterust/shekyl-pow-randomx/) and Phase 3's miner cutover. The existingexternal/randomx(v1.2.1 at102f8acf) is unchanged; the v1 fallback path perdocs/design/RANDOMX_V1_FALLBACK.md§1 remains reachable. Seedocs/completed/RANDOMX_V2_PHASE1_PLAN.mdfor the full scope, theExternalProject_Addconfiguration rationale, the build-smoke test results, the §10 implementation-time dispositions (D1check_submoduleomission, D2 multi-config fail-fast, D3 toolchain forwarding expansion, D4 semicolon-escape), and the reversibility plan. -
LWMA-1 difficulty-adjustment migration — Phase 4 C++ cutover (
feat/daa-lwma1-phase4, 2026-05-18). Lands the consensus-atomic cutover from the inherited CryptoNote cut-windowed-average DAA to LWMA-1, plus the two paired FTL/MTP value changes, in a single PR invoking07-consensus-atomic-cutovers.mdc. The PR contains eleven commits that respect single-purpose scope per90-commits.mdc; the eleven-commit structure is the pre-flight-disposed shape (docs/design/DAA_LWMA1_PHASE4_PREFLIGHT.md§18). Closes work-items 1–14 ofdocs/design/DAA_LWMA1_PLAN.mdPhase 4 and the V3.0 DAA item indocs/FOLLOWUPS.md.Consensus-rule deltas (the load-bearing changes a validator must agree on):
- DAA:
Blockchain::next_difficulty(CryptoNote cut-windowed-average,DIFFICULTY_WINDOW=720,DIFFICULTY_LAG=15 // !!!,DIFFICULTY_CUT=60) is replaced by LWMA-1 fromzawy12/difficulty-algorithms#3withN=90,T=120s,GENESIS_DIFFICULTY=100. The FFI surface (shekyl_difficulty_lwma1_next) is wrapped at the threeBlockchaincall sites (get_difficulty_for_next_block,recalculate_difficulties,get_next_difficulty_for_alternative_chain) by thelwma1_next_difficultyhelper inblockchain.cpp, which throwscryptonote::difficulty_computation_error(declared insrc/cryptonote_core/difficulty_engine_error.h) on non-zero FFI return codes. - FTL:
CRYPTONOTE_BLOCK_FUTURE_TIME_LIMIT(60*60*2= 7200s) becomesSHEKYL_DAA_FTL_SECONDS= 540s (zawy12-requiredN*T/20). Tightens by 13.3×; reorgs more than 9 minutes deep on local-clock disagreement are no longer accepted. - MTP:
BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW= 60 becomesSHEKYL_DAA_MTP_WINDOW= 11. Tightens back from the Monero-era widening to the CryptoNote-original window.
Mechanical rewires (value-preserving):
DIFFICULTY_TARGET_V2(120s) consumers across the daemon, wallet, RPC, and tests are rewired toSHEKYL_DAA_TARGET_SECONDS(also 120s). 8 production sites and 5 test sites; verified by the consensus-invariants gate (scripts/ci/check_consensus_invariants.shinvariant 3).CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_SECONDS_V2is preserved with its RHS rewired fromDIFFICULTY_TARGET_V2toSHEKYL_DAA_TARGET_SECONDS; two live consumers (blockchain.cpp:4043,wallet2.cpp:7330) are unaffected.DIFFICULTY_BLOCKS_ESTIMATE_TIMESPAN(60s V1 alias used by tests as a generic "block time" multiplier) is replaced bySHEKYL_DAA_TARGET_SECONDS(120s) at 4 non-deletion test files (bulletproof_plus.cpp,chaingen.cpp,transactions_flow_test.cpp,block_validation.cpp:267). Semantic shift: 60s base → 120s base for tests' block-time approximation, matching the actual block rate.
Deletions:
- Seven inherited
#defines removed fromsrc/cryptonote_config.h:DIFFICULTY_TARGET_V[12],DIFFICULTY_WINDOW,DIFFICULTY_LAG(with its// !!!warning),DIFFICULTY_CUT,DIFFICULTY_BLOCKS_COUNT,DIFFICULTY_BLOCKS_ESTIMATE_TIMESPAN. The V1 lock-deltaCRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_SECONDS_V1is removed (pre-genesis Monero behavior, dead under60-no-monero-legacy.mdc). next_difficultyandnext_difficulty_64deleted fromsrc/cryptonote_basic/difficulty.{h,cpp}(surgical, per the pre-flight's drift-F6 amendment: thecheck_hashPoW family in the same file is retained with ~12 live production consumers).tests/difficulty/{difficulty.cpp,data.txt,generate-data,gen_wide_data.py,wide_difficulty.py}deleted (~23 KB) — exercised the now-deleted CryptoNote DAA; thelwma1-cross-checkharness (Phase 2 vintage) is retained intests/difficulty/CMakeLists.txt.lift_up_difficultyhelper plusgen_block_invalid_nonceandgen_block_invalid_binary_formattest classes removed fromblock_validation.{cpp,h}(V1-only fixtures, already disabled in the test driver).
Regression tests added:
tests/unit_tests/rpc_target_wire_contract.cpp— pins the public JSON-RPC wire contract formining_status.block_targetandget_info.targetat120. Both gtests plus thestatic_assert(SHEKYL_DAA_TARGET_SECONDS == 120, …)static pin remain after the cutover.tests/unit_tests/stall_detection_calibration.cpp— pins the daemon's stall-detection calibration: 1/7200 false-positive threshold,{45, 30, 15, 10, 5}expected-block counts across the five Poisson windows, and the zero-blocks-tail-probability boundary (the 600s window must NOT trip at λ=5; the four longer windows must trip at λ ≥ 10).
CI gate added:
.github/workflows/consensus-invariants.ymlplusscripts/ci/check_consensus_invariants.sh— three source-level grep invariants (no live consumers of the deleted DAA functions; no C-ABI inrust/shekyl-difficulty; no orphaned references to the deleted#defines). Shared landing pad for the upcoming RandomX v2 Phase 2f symbol-isolation checks. Binary-levelnm-on-shekyldverification is a deferred enhancement (recorded in this entry as a follow-up below).
Pre-flight drift findings closed:
- F1 — surgical (not wholesale) deletion of
tests/difficulty/; thelwma1-cross-checkharness stays. - F2 —
CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_SECONDS_V2preserved with rewired RHS (option B);_V1deleted. - F3 — V1
next_difficulty(...)fixtures inblock_validation.cppdeleted along with the helper that drove them. - F4 —
DIFFICULTY_TARGET_V2consumer count corrected from "~14 sites across 9 files" to the actual 8 production + 5 test sites enumerated by the commit-6 sweep. - F5 —
DIFFICULTY_TARGET_V2consumer undercount inblockchain.cpp: the plan's §9.7 enumeration missed two sites at lines 4239 / 4243 (an MTP-window correction and atimestamps.back() + DIFFICULTY_TARGET_V2adjustment insidecheck_block_timestamp); both rewired toSHEKYL_DAA_TARGET_SECONDS.wallet2.cpp's lines 181, 182, 5975, 11548 were never drift — the earlier text mis-attributed F5 towallet2.cpp; corrected 2026-05-18 per PR #53 Copilot review C-6. - F6 — surgical (not wholesale) deletion of
src/cryptonote_basic/difficulty.{h,cpp}: thecheck_hashPoW-validation family is retained; only thenext_difficultyfamily is deleted. - F7 —
check_difficulty_checkpoints()is NOT a deletion target. Pre-flight §14 (andDAA_LWMA1.md§7.1) erroneously enumerated it as a symbol-isolation deletion candidate. The function inblockchain.cpp:1066is a checkpoint-cumulative- difficulty comparison independent of the deleted DAA functions; retained. The spec doc and pre-flight are amended in this commit.
Reviewer-map structure (per
07-consensus-atomic-cutovers.mdcsub-clause 4.3):- A. Consensus-affecting changes (priority attention):
blockchain.cppDAA rewires (commit 3), FTL rewires (commit 4), MTP rewires (commit 5),cryptonote_config.hdeletions (commit 7),difficulty.cppdeletions (commit 8). - B. Mechanical rewires (value-unchanged):
DIFFICULTY_TARGET_V2→SHEKYL_DAA_TARGET_SECONDS(commit 6),DIFFICULTY_BLOCKS_ESTIMATE_TIMESPANrewires in tests (commit 7). - C. Deletions: legacy DAA tests + V1 fixtures (commit 9).
- D. New artifacts: regression tests (commits 1, 2), CI gate (commit 10), this changelog entry (commit 11).
Rollback procedure (per sub-clause 4.4):
If consensus breaks post-merge, the reversion is to revert the merge commit on
dev(single non-FF merge per06-branching.mdc) and re-tag. Because the cutover is atomic (FTL/MTP/DAA all in one PR), no partial reversion is required. The pre-merge state ofblockchain.cpp's threenext_difficultycall sites, the FTL/MTP consumer surfaces, and the deleted#defines are all captured in the pre-cutoverdevSHA recorded in the PR description; reverting the merge restores them byte-identically.Follow-up: binary-level
nm shekyld | rg -q '^.* (T|U) (next_difficulty_64|next_difficulty)\b'symbol-isolation check. Source-level grep (this PR's invariant 1) is a necessary precondition for binary absence; the binary-level check is a deferred enhancement when CI is restructured to expose the linked daemon binary to a post-link grep step. Tracked indocs/FOLLOWUPS.md. - DAA:
-
LWMA-1 difficulty-adjustment migration — Phase 0 design docs (
feat/daa-lwma1-phase0-design, 2026-05-17). Adds two Phase 0 design documents underdocs/design/:DAA_LWMA1.md(the primary design) andDAA_LWMA1_PLAN.md(the phased execution plan, five phases sequential, no parallel tracks). The primary design records the disposition to replace the inherited CryptoNote cut-windowed-average DAA (src/cryptonote_basic/difficulty.cpp,DIFFICULTY_WINDOW=720,DIFFICULTY_LAG=15with literal// !!!warning,DIFFICULTY_CUT=60) with LWMA-1 from zawy12's canonical reference atzawy12/difficulty-algorithms#3, implemented as a Rust crateshekyl-difficultyper20-rust-vs-cpp-policy.mdcrule 2 (cryptographic-contract surface). Concrete parameter selection: N=90 (zawy12 canonical for T=120s), T=120s (inherited), GENESIS_DIFFICULTY=100 (proposed), FTL=N*T/20=540s (zawy12-required, replaces inherited 7200s), MTP=11 (Cryptonote default unchanged). The design pins genesis-time landing per16-architectural-inheritance.mdcpre-genesis discount and60-no-monero-legacy.mdcno-version-dispatch rule. Sibling track to RandomX v2 but independent: math-orthogonal (DAA operates on(timestamps, cum_difficulties); PoW changes the hash function), no wallet V3.2 gate applies, no Monero release-time audit dependency. A pre-designrust/shekyl-difficulty/src/lwma1.rssketch is explicitly documented as not canonical (different formula, missing6*Tsolvetime clamp, missingN*N*T/20minimum-L floor, missing99/200bias factor) and was deleted during Phase 0 so Phase 1 starts from an empty crate directory; the divergence catalogue is retained inDAA_LWMA1.md§2.4 as the design record of why each non-canonical shape is rejected. Reversion clauses per21-reversion-clause-discipline.mdccover LWMA-2/3/4 and ASERT reopening criteria.Round 2 review update (2026-05-17): (a) reframes
shekyl-difficultyas a leaf crate with zero internal workspace dependencies per18-type-placement.mdc, with FFI exposure routed throughshekyl-ffi(DAA_LWMA1.md§2.1); (b) records the explicit "DAA is a primitive, not an actor" disposition (DAA_LWMA1.md§2.7) —lwma1_nextis a free function plus typed constants plus the FTL/MTP predicates, noDifficultyEngineactor wrapper; (c) pivots the consensus-constants source-of-truth from acbindgenhandwave to the existingconfig/consensus_constants.jsonJSON-authority pattern documented indocs/FOLLOWUPS.mdand the 2026-05-05 FFI constant-drift audit (DAA_LWMA1.md§4, plan Phase 1 task); (d) adds a chain-state- ownership disposition (DAA_LWMA1.md§17) acknowledging that daemon-side LMDB chain state remains in C++Blockchainthrough Phase 4 and that no Rust crate owns daemon-side chain state today; the future Rust validator actor will consume the same DAA transform without changes to the DAA crate.Round 3 review update (2026-05-17): (a) corrects the contradictory dispositions for
DIFFICULTY_TARGET_V2— design doc §9.2 now matches the plan's delete-not-rename directive (rename would preserve the hand-maintained#definedrift class the JSON authority exists to close); (b) corrects two real factual errors surfaced by a Round 3 reconnaissance grep of the C++ tree: the constant isCRYPTONOTE_BLOCK_FUTURE_TIME_LIMIT(notBLOCK_FUTURE_TIME_LIMIT; there is no_V2variant), andBLOCKCHAIN_TIMESTAMP_CHECK_WINDOWis currently60(Monero-era widening from the CryptoNote-original11), so the LWMA-1 disposition is a tightening — not preservation — from 60 back to 11; (c) adopts algorithm-version-free naming for the JSON keys (daa_window_n, etc.) and the generated C++ symbols (SHEKYL_DAA_*, notSHEKYL_DAA_LWMA1_*) so a future §10 reversion doesn't require renaming every consumer; (d) enumerates the full Phase 4 consumer surface in new sections §9.5 (CRYPTONOTE_BLOCK_FUTURE_TIME_LIMIT: 2 sites), §9.6 (BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW: 9 sites), and §9.7 (DIFFICULTY_TARGET_V2: ~14 sites across 9 files), and adds §9.8 to flag thecore_rpc_server.cpp:1452 res.block_targetRPC-contract preservation property; (e) acknowledges Phase 4 atomicity as a deliberate exception to06-branching.mdc(FTL/MTP value changes cannot stage behind alias#defines without weakening consensus in the intermediate state); (f) resolves the bias-factor location drift —99and200(plus6and1/20) appear as bare integer literals insidesrc/lwma1.rsto match canonical zawy12 verbatim, not as namedpub(crate) constinconsts.rs; (g) mechanizes Phase 5's conditional cross-reference to24-reviewer-discipline.mdcso the Phase 5 reviewer can verify by grep; (h) closes open question #3 (build.rs location) as Option A per the leaf-crate property in §2.1; (i) adds asolvetime[1]-Toffset regression vector to §8.1's required-vector list; (j) adds explicit MIT attribution to the Phase 2 vendoredtests/difficulty/zawy12_lwma1_reference.h; (k) moves long reviewer-note prose out of the long-livedCargo.tomlinto a Phase 1 review-checklist section; (l) flagsis_above_mtp's&[u64; 11]vs slice ergonomics as a Phase 1 implementation choice (not a Phase 0 blocker); (m) adds canonical line-number stability caveats to §5.3 step 7 and step 8 (line numbers are stable only against the Phase 2 pinned-spec revision); (n) updates Phase 4 work-item count from 11 to the actual 14.Round 4 review update (2026-05-17): (a) pivots the FFI ABI for difficulty values from
u128/__uint128_tto canonical little-endian[u8; 16]byte arrays (DAA_LWMA1.md§6.1 and plan Phase 3). Rationale: Rust'su128C ABI was unsound on several targets until rustc 1.77 (March 2024) and remains a target-portability footgun on uncommon platforms; for a consensus-critical surface that's unacceptable. Explicit byte arrays match the FCMP++ and KEM-derivation FFI precedent already in the workspace and immunize the boundary against target-dependent ABI surprises. C++ consumers memcpy between their nativeuint128_tand the canonical-LE buffer at every call site so the endianness assumption is a deliberate checkpoint rather than an implicit invariant. (b) Consensus-correctness fix to §8.1 test vectors. The Round 3 vector "perfectly stable hashrate producesnext_D == avg_D(within rounding)" was mathematically wrong: withsolvetime[i] == Tfor alli, the formula yieldsnext_D == avg_D * 99 / 100— a deliberate 1 % downward bias, which is the point of the99/200factor per §5.3 step 7's derivation. The Round 3 expectation invited three implementer failure paths (relax tolerance to absorb the 1 % shift; remove the bias from the algorithm to satisfy the test; misread "rounding" as ±1 %). Round 4 replaces all≈-shaped vectors with concrete numerical tuples: stable hashrate →0.99 * avg_D, 2× hashrate increase →1.98 * avg_D, 2× hashrate decrease →0.495 * avg_D, minimum-L floor (all solvetimes == 1) →~10.01 * avg_D. Tuples are derived analytically from §5.3 and force the Phase 1 implementer to confront the bias at design time, not at debug time. Also corrects an off-by-one in §2.6's "first N+1 blocks" framing (canonical'sheight < Nshort-circuit covers N blocks, not N+1; the Shekyl FFIchain_height < Ntranslation puts blocks1..=Nin the short-circuit per the new §5.6 validator consumer contract). (c) AddsDAA_LWMA1.md§5.6 "Validator consumer contract:chain_height → header.difficulty" specifying the off-by-one mapping between the DAA function'schain_heightparameter (predecessor's height) and the block-being-validated's height, plus the per-block disposition: block 0 (genesis) is exempt; blocks1..=NcarryGENESIS_DIFFICULTY; blocks≥ N+1are algorithm-computed. Pre-empts the Phase 4 reviewer's first question. (d) Closes all Phase 0 open questions.GENESIS_DIFFICULTY = 100andN = 90are ratified zawy12 canonical with reversion triggers in §10 covering simulation-driven change; the "Shekyl-empirical RandomX v2 single-CPU measurement" alternative referenced a measurement that cannot exist until RandomX v2 ships and is functionally identical to the §10 reversion trigger already in place. Phase 2 cross-check harness language closed as C++ test target (the canonical reference is C++; consuming it directly is simpler than Rust-side vendoring; the alternative was a cosmetic preference). Build.rs location (Option A) and JSON-key naming (daa_*algorithm-version-free) were already closed in Round 3 and are restated for completeness. No open questions are carried into Phase 1; the design-rounds-in- implementation-PR anti-pattern is closed at Phase 0. (e) Adds three LWMA1_() disambiguation anchors toDAA_LWMA1.md§3 and plan Phase 2: byte-offset range, first-line, last-line. zawy12 Issue #3 contains four LWMA reference functions (LWMA1_/2_/3_/4_); §5.3's "Issue #3, lines N–M" citations are otherwise ambiguous and would break Phase 2 cross-check at the smallest upstream reordering. (f) ReframesT = 120 sas Shekyl's chosen target block time (zawy12 LWMA-1 recommends 60–120 s for CPU-mineable chains) rather than "inherited from CryptoNoteDIFFICULTY_TARGET_V2." The numerical value matches; the source-of-truth is the JSON authoritydaa_target_seconds, not the inherited#define.Round 5 review update (2026-05-17): (a) FFI ABI pivot from
[u8; 16]byte arrays to#[repr(C)] struct ShekylU128 { lo: u64, hi: u64 }. Round 4 named theu128ABI unsoundness as a Tier 1 blocker but stopped short of proposing the specific wire representation. Round 5 closes this.ShekylU128decomposes the 128-bit value into twou64fields whose ABI is universally stable on every Shekyl- supported target — noimproper_ctypesexposure, no MSRV-pin-to-1.78 constraint, no per-target ABI verification matrix. The struct-with-named-fields shape preserves explicitlo/hisemantics (debugger-friendly, unambiguous, survives any future endianness disposition because the field meaning is carried by the field name). Endianness is consensus-locked inDAA_LWMA1.md§6.1:ShekylU128is little-endian by field semantics —lois the low 64 bits,hiis the high 64 bits, reconstruction isvalue = (hi as u128) << 64 | (lo as u128). Cost: one struct definition and four lines ofFromimpls per direction. Benefit: the consensus-critical surface is immune tou128-ABI target-portability issues permanently, not just on rustc ≥ 1.77. (b) MTP 60 → 11 trade-off framing. TheBLOCKCHAIN_TIMESTAMP_CHECK_WINDOW = 60→SHEKYL_DAA_MTP_WINDOW = 11change travels in opposite directions on two security axes simultaneously and a release-note skimmer reading the value change in isolation would misread it as a security regression. Surfaced explicitly: the MTP-only timestamp- attack defense weakens (it is easier for an adversary to satisfy "strictly greater than the median of 11 timestamps" than "strictly greater than the median of 60 timestamps" in isolation), and the LWMA-1-coupled defense engages (the canonical zawy12 math is calibrated against MTP = 11, not MTP = 60; running LWMA-1 with MTP = 60 would understate the algorithm's solvetime-clamp resistance).DAA_LWMA1.md§5.5 names all three checks (MTP + FTL + solvetime-clamp) as jointly load-bearing — the combined defense profile post-Phase-4 is stronger than either the pre-Phase-4 MTP=60-only profile or a hypothetical LWMA-1-with-MTP=60 configuration. The value change is the cost of moving from a MTP-only-anchored defense to the canonical zawy12-coupled defense; it is not a unilateral loosening. (c) RPC-contract preservation regression test (§9.8). The byte-identity assertion is now explicit: a wallet callingget_infoagainst the post-Phase-4 daemon receives ablock_targetfield that is byte-identical to the same wallet's response against the pre-Phase-4 daemon (captured as a fixture at PR-open). The value-identity assertion (120 == 120) catches value drift; the byte-identity assertion catches encoding drift (a future "change varint encoding to little-endian byte array" refactor would preserve the numeric value but break the wire contract). Both are required to make the RPC-contract-preservation property auditable rather than asserted. (d) "Consensus-atomic cutover" exception class drafted inDAA_LWMA1_PLAN.mdPhase 4 (four criteria: consensus-rule boundary; structural indivisibility; surface enumerated in advance; documented disposition citing the criteria). The class was drafted here as four criteria; the sibling PRfeat/consensus-atomic-cutovers-ruleratifies the criteria as.cursor/rules/07-consensus-atomic-cutovers.mdcand refines them through Round 6 / Round 7 review before landing (PR #50). The ratified form: the rule is opt-in (alwaysApply: false) and unreachable by any PR that does not cite it explicitly; criterion 2 is reframed as the structural-inapplicability of flag decomposition to consensus rules — a flag decomposition is consensus-safe only if both flag states are simultaneously valid, which for a consensus rule is impossible by definition, so criterion 2 is met whenever criterion 1 is met (closing the "yes-it's-consensus- but-splitting-would-be-inconvenient" loophole); criterion 3 adds a base-commit-anchored, timestamped grep so reviewers re-run against the same SHA; criterion 4 is numbered into sub-clauses 4.1–4.4 with reviewer-map-accuracy and rollback-correctness promoted into the criterion itself (rejecting the PR is the response to a map miss, not patching the map); a "what this is not" section disqualifies convenience / velocity / reviewer-bandwidth / retroactive-citation; and the history of application is split into "Approved invocations" (LWMA-1 Phase 4) and "Cases that might appear analogous but are not" (RandomX v2 Phase 3, where the 3a flag is build-system / FFI-routing rather than consensus, the algorithm change ships in Phase 1's submodule swap, and criterion 1 is therefore not met for Phase 3 at all — structurally inapplicable, not "evaluated and rejected"). The mechanism for future invocations is self-anchoring: an invoking PR must include a commit that adds its own entry to the rule's history-of-application section. Phase 4's section in this plan invokes the ratified rule by name and maps each criterion to LWMA-1 Phase 4 specifically; Phase 4's exception is auditable against the class's four criteria mechanically, not against LWMA-1-specific precedent. (e) Round 8 bias-factor stochastic-vs-deterministic clarification (DAA_LWMA1.md§5.3 step 7, §8.1). The Round 4 test-vector correction landed concrete numerical tuples that expectnext_D == avg_D * 99/100on the §8.1 perfectly-stable hashrate input (deliberate downward bias from the99/200factor). The Round 4 fix did not synchronously update §5.3 step 7's derivation prose, which still described the99/100factor as "compensating for a ~1 % upward bias" — leaving the doc internally contradictory: one section described the factor as canceling drift (stable input →avg_Dexactly), the other expected a 1 % residual. Round 8 resolves the contradiction by making the stochastic-vs-deterministic distinction explicit: the canonical zawy12 bias correction targets stochastic upward drift (Poisson skew,6*Tclamp truncation, jump-rule amplification from downstream LWMA-2+ variants) present under realistic chain operation; on §8.1's deterministic unit-test vectors (all solvetimes exactlyT, no clamp engagement, no PRNG), the same factor surfaces as a deterministic 1 % downward residual rather than as a corrective cancellation. Both readings of the algorithm are correct under their respective input shapes; the doc now says so explicitly so a Phase 1 implementer who transcribes the formula and observesnext_D == 990_000on the §8.1 stable vector knows that's a correctly implementing algorithm rather than a test expectation to "fix." A Phase 1 pre-flight verification step is added toDAA_LWMA1_PLAN.md: the canonical zawy12 C++ reference is run once against the §8.1 stable vector and the result recorded in the Phase 1 PR description before implementation begins, removing the residual ambiguity as a function of empirical evidence rather than as a function of prose interpretation. (f) Round 8 §11 wallet touchpoint correction. §11 previously read "LWMA-1 is not consumed by the wallet — wallets do not compute or check difficulty (validators do)" — true for the algorithm but incomplete for the target-block-time constantT, which §9.7's enumeration surfaced as a wallet consumer atwallet2.cpp:181, 182, 5975, 11548andwallet_rpc_server.cpp:163(unlock-time defaults, recent-spend-window math,seconds_per_blockconsumers,suggested_confirmations_thresholdmath — five wallet-side sites). §11 now reads accurately: the algorithm is not consumed by the wallet, butTis, with a value-preserving rewire fromDIFFICULTY_TARGET_V2toSHEKYL_DAA_TARGET_SECONDSacross all five sites. Phase 4's wallet impact is no longer mis-stated as "no wallet impact." The §11 prose-vs-§9.7 enumeration drift was a Round 1 grep finding that didn't make it into the §11 prose; Round 8 closes the loop. (g) Round 8 polish. (i)DAA_LWMA1.md§6.3 explicitly records that theis_above_mtpandis_timestamp_below_ftlpredicates committed in §2.5 are Rust-internal helpers consumed by the §17 future validator actor, not exposed via the FFI — the C++ side does the corresponding FTL and MTP checks directly against the generated header constants per §6.2's source-of-truth pattern, keeping the FFI surface minimal per §6.1's "one committed export" discipline. (ii)DAA_LWMA1.md§9.5 adds a Phase 4 reviewer note that with the FTL value change from 7200 to 540, the FTL test margin intests/core_tests/block_validation.cpp:137shrinks from "7.2 hours past FTL" to "1 hour past FTL"; the test must assert rejection specifically because of the FTL check (error-code equality, not generic "block rejected"), so the test can't pass for the wrong reason if a future refactor moves rejection to a different validation path. (iii)DAA_LWMA1.md§9.7 adds a Phase 4 reviewer note for thecryptonote_core.cpp:1817, 1829, 1838Poisson stall-detection sites: the rewire is value-preserving but the path is not exercised by any current test, so Phase 4 either confirms coverage exists or adds a minimal regression test; "rewire textually, value unchanged" alone is not a sufficient verification claim for a path with no test coverage. (iv)DAA_LWMA1.md§9.3 is repopulated with substantive consolidation prose pointing FTL/MTP enumeration cross- references to §9.5 and §9.6 respectively (was previously an empty "deprecated section header" pointer with no content). (v)DAA_LWMA1_PLAN.mdPhase 4 adds a reviewer-expectation note that the "14 work items" framing categorizes work but understates diff size: actual file-change count lands at roughly 45–55 files acrosssrc/andtests/. (vi)DAA_LWMA1.mdstatus block on line 3 updated from "Round 1" to reflect that Rounds 1–8 have all landed against this PR. (h) Round 9 zawy12 issue #24 cumulative-history review. Reviews the design against zawy12/difficulty-algorithms#24 ("LWMA's history"), the canonical author's cumulative log of known LWMA issues, fixes, and security-relevant findings. Five items receive explicit dispositions; four (#1, #2, #4, #5, #6, #10, #12, #15, #16) are confirmed already-addressed. Substantive changes:- Item #14 (September 2018 selfish-mine via out-of-sequence
timestamps). Algorithm-level change.
DAA_LWMA1.md§5.3 steps 2 and 3 adopt LWMA-3's running-max + signed-solvetime mechanism and symmetric±6*Tclamp, replacing the kyuupichan-style forward-pass-with-1-floor used through Round 8. The remainder of the algorithm (weighted-sum, minimum-L floor, bias factor 99/200, overflow guard, genesis-window short-circuit) stays LWMA-1-canonical. Disposition recorded in §1.3 (alternatives — "Partial LWMA-3 adoption"), §3 (pinned spec — deviation note +LWMA3_()reference pin), §5.3 steps 2/3/4 (algorithm rewrite to signed-i128 intermediates + symmetric clamp), §5.4 ("Signed-arithmetic discipline" property), §5.5 (defense-surface enumeration grows to four mechanisms), and §8.1 (out-of-sequence vector reformulated for running-max semantics, new "Selfish-mine attack regression (zawy12 issue #24 item 11)" required vector).DAA_LWMA1_PLAN.mdPhase 1 adds a signed-arithmetic discipline section detailing the i128/u128 boundary and lists the two Round 9 test vectors as required Phase 1 merge-gate criteria. Phase 2's cross-check harness composes expectations from both canonicalLWMA1_()andLWMA3_()references per §8.2. - Item #17 (May 2019 33% Sybil attack via peer-time-offset).
Closed by absence of substrate. The attack's precondition
("If your coin uses network time instead of node local
time") is not met by Shekyl.
Blockchain::check_block_timestamp(b)compares againsttime(NULL)directly (blockchain.cpp:4276);Blockchain::get_adjusted_time(height)is blockchain-derived (median of recent block timestamps) and consulted only by non-consensus paths. No peer-time-correction mechanism exists in the daemon; audit-trail grep returned zero matches fortime_offset|TimeOffset|GetAdjustedTime|GetTimeOffset|MAX_PEER_DELTA|MAX_TIME_DELTA|MEDIAN_TIME|TIMESTAMPS_FOR_TIME_SYNCagainst consensus-relevant surface. Lowering FTL from 7200 s to 540 s is therefore safe against the zcash/zcash#4021 attack class. Disposition recorded inDAA_LWMA1.md§5.5's "Disposition on peer-time-derived clocks" paragraph, with a forward-looking constraint: if a future Shekyl version adds peer-time correction, theFTL / 2revert-threshold relationship per zawy12 issue #24 item 14 becomes load-bearing at that point anddaa_peer_time_revert_threshold_secondsMUST be added to the JSON authority. The FTL value reduction (7200 → 540) pre-dates this round but the safety rationale is now explicit: it is safe because Shekyl does not implement peer-time-derived clocks. - Item #7 (Jagerman MTP patch). Verified present in
Shekyl's inherited
Blockchain::create_block_templateatblockchain.cpp:1650–1656(the canonical pattern: setb.timestamp = time(NULL), then ifcheck_block_timestampfails, raise tomedian_ts). The MTP window change from 60 to 11 preserves the patch's effectiveness; no Phase 4 work required. Disposition recorded inDAA_LWMA1.md§5.5 with code citation. A minor doc-vs-code drift atblockchain.cpp:1540's cached-template path is recorded as aFOLLOWUPS.mdcandidate, not a Phase 4 atomic-cutover work item. - Item #3 (window size N=60 vs N=90). Documentation polish.
DAA_LWMA1.md§4's N parameter row notes that zawy12 issue #24's 2018 "N ≈ 60" recommendation referred toT = 60 schains; the recommendation scales inversely withTand forT = 120 sthe canonical N is 90 (same ~90-minute window). - Item #9 (±7xT header timestamp limits vs FTL boundary).
Documentation only.
DAA_LWMA1.md§5.5 records that Shekyl uses MTP + FTL + symmetric solvetime clamp + running-max normalization (four mechanisms) as the defense surface and does not implement a separate per-block-header±7xTrule, consistent with zawy12 issue #24 item 9's post-FTL deprecation of±7xT.
DAA_LWMA1_PLAN.mdgains a "Round 9 dispositions" section recording all five issue-item dispositions and naming items #1, #2, #4, #5, #6, #10, #12, #15, #16 as already-addressed with their corresponding §ref.DAA_LWMA1.mdstatus block on line 3 updated from "Round 8" to "Round 9" to reflect the cumulative review pass. (i) Round 9 supplement — local-time-only FTL trade-off named. The Round 9 closure of zawy12 issue #24 item 17 (FTL vs peer-time-derived clocks) recorded the absence of substrate but did not name the threat-model trade the local-time-only FTL disposition deliberately accepts. This supplement makes the trade explicit so a future reader does not misread the disposition as missing functionality.DAA_LWMA1.md§5.5's "Disposition on peer-time-derived clocks" paragraph is expanded into four labelled subsections: (1) the trade-off, named explicitly — Shekyl trades the zawy12 #17 / zcash/zcash#4021 peer-time-Sybil attack class (a ~$1000 attack accessible to anyone with bandwidth to run enough peers) for an operator-side NTP-hygiene requirement plus a coordinated-NTP-infrastructure- compromise threat that requires state-level access; (2) residual threat-class ranking — four classes documented from highest- probability/lowest-impact (individual node clock skew, mitigated by standard NTP hygiene, isolates affected node without propagating to peers) through lowest-probability/highest-impact (coordinated NTP-infrastructure compromise at scale, requiring state-level access, not consensus-protocol-mitigated); (3) operator obligations — validators are responsible for keeping local clocks within ±540 s of network truth via standard NTP discipline (multiple time sources, drift monitoring); NTP failure is a liveness failure for the affected node, not a safety failure that propagates; (4) Y2038-adjacent note —time(NULL)returnstime_t, which on 64-bit platforms (the only Shekyl-supported platforms per the 32-bit retirement chore landed at commite06ee37d96af, recorded indocs/FOLLOWUPS.md) is 64-bit signed and Y2038 is not a concern; if 32-bit platforms ever return to scope, both the FTL comparison and the FTL/2 forward-looking peer-time constraint must be revisited.DAA_LWMA1.md§1.2 (Commitment 1) gains a closing observation: "The FTL-disposition choice (local-time- only, no peer-time-derived clock) reflects a deliberate threat-model preference for closing low-bar consensus attacks at the cost of slightly higher operator NTP-hygiene responsibility — consistent with Shekyl's broader posture on operator autonomy per75-system-autonomy.mdc." The trade itself, ranking observation, and the "safe because" framing on the FTL value reduction (7200 → 540) are now consistently cross-referenced from §1.2, §5.5, and this CHANGELOG entry. (j) Round 10 zawy12 issue #24 item-number reconciliation + issue pin + reference-file enumeration + commit-hash cite-stabilization. Round 10 review identified one load-bearing finding and three robustness improvements:- Item-number drift sweep (load-bearing). The Round 9
body edits used item numbers that did not match the live
zawy12 issue #24 numbering: 11 was used for the September
2018 selfish-mine attack (live: item 14), 14 for the May
2019 33% Sybil (live: item 17), 6 for the Jagerman MTP
patch (live: item 7), 8 for the post-FTL
±7xTdisposition (live: item 9), and 13 for the January 2019 LWMA-2/3/4 deprecation (live: item 16). The pattern was not a uniform offset but a cluster of mistranscriptions during Round 9's body edits while the status block was checked separately. The Round 10 sweep corrected 14 sites inDAA_LWMA1.mdbody, 2 sites inDAA_LWMA1_PLAN.mdbody, and 2 sites in this CHANGELOG entry — all now consistent with the live issue and with the status block's "items 3, 7, 9, 14, 17" enumeration. The discipline going forward: cite by date + description as the primary identifier (e.g., "September 2018 selfish-mine attack class") so renumbering by the upstream author does not silently invalidate cross-references; the item number is a redundant cross-reference resolving against the §3 pin (next item). - zawy12 issue #24 pin (audit-trail-stable).
DAA_LWMA1.md§3 gains a "zawy12 issue #24 pin (Round 10 addition)" bullet pinning the raw.bodyofzawy12/difficulty-algorithms#24viadocs/design/refs/zawy12_issue_24_history.mdat Phase 2 PR time, using the samegh api+jq -r .bodymechanism as the existing issue-#3 pin. Every "zawy12 issue #24 item N" cross-reference downstream now resolves against this pin's numbered list, not against the live GitHub-rendered issue. The pin's SHA-256 and capture timestamp land in §3's pin record at Phase 2 commit time.DAA_LWMA1_PLAN.mdPhase 2 task content extends to commit the issue-#24 pin alongside the existing issue-#3 pin. - Phase 2 reference-file enumeration clarified.
DAA_LWMA1.md§3's Round-9 disposition paragraph is expanded into an explicit three-file enumeration making clear thatzawy12_issue_3_lwma1.md(raw issue-#3.body, the canonical pin),zawy12_issue_3_lwma3.md(convenience extraction of just the LWMA3_() function, not the canonical pin), andzawy12_issue_3_lwma1_with_lwma3_step2.md(Shekyl-composed hybrid, a derived file used by the cross-check harness) are three distinct files with distinct purposes. The "snapshot pinned per §3" cross-reference at §5.3 step 2 now resolves unambiguously.DAA_LWMA1_PLAN.mdPhase 2 body section gains a "Round 9 + Round 10 supplementary reference files" subsection enumerating all four Phase-2-committed files (three issue-#3 derivatives plus the issue-#24 pin) and extending the anchors-file schema with the LWMA3_() byte-offset anchors. - Commit-hash cite for 32-bit-retirement chore.
DAA_LWMA1.md§5.5's Y2038-adjacent note and this CHANGELOG's Round 9 supplement entry both previously cited the chore by branch name (chore/retire-32bit-targets), which is a deleted post-merge branch and not a stable cite target. Both citations are now anchored on the merge commite06ee37d96af("Merge pull request #15 from Shekyl-Foundation/chore/retire-32bit-targets") with the rationale named in §5.5.
Status block on line 3 updates from "Round 9" to "Round 10" recording the cumulative review pass. No algorithm-level or consensus-rule changes in Round 10; the round is documentation drift remediation and audit-trail-stability improvements. (k) Round 11 consumer-count drift reconciliation (Copilot review of PR #49). Copilot's first review pass on the ready-for-review PR flagged two count-mismatch findings of the same shape as Round 10's item-number drift — prose totals that did not match their adjacent enumerations. The Round 11 sweep reconciles both flagged sites plus the adjacent sites Copilot did not flag but that exhibit the same drift pattern (per the Round 10 discipline: fix the pattern, not just the flagged instances).
- MTP consumer count (§9.6 in
DAA_LWMA1.md, propagated toDAA_LWMA1_PLAN.mdPhase 4 work item 6 and the breakdown paragraph). The §9.6 prose said "seven direct consumers ... plus two test-suite consumers" but the enumeration immediately below has always listed:blockchain.cpp:1981, 1985(2 daemon sites) +blockchain.cpp:4223, 4230, 4240, 4259, 4285, 4293(6 daemon sites) +tests/core_tests/block_validation.h:92, 97(2 test sites) +tests/core_tests/block_validation.cpp:106, 120, 122(3 test sites) — 8 daemon + 5 test = 13 total sites across 3 files. The prose now matches the enumeration: "eight direct consumers ... plus five test-suite consumers — thirteen total sites across three files." Downstream propagation: the Phase 4 work item 6 inDAA_LWMA1_PLAN.mdpreviously read "the nine MTP consumers ... (seven inblockchain.cpp, two inblock_validation.{h,cpp})"; it now reads "the thirteen MTP consumers ... (eight inblockchain.cpp, five inblock_validation.{h,cpp})." The Phase 4 file-change breakdown paragraph previously read "9 MTP consumer rewires across 4 files (§9.6)" and now reads "13 MTP consumer rewires across 3 files (§9.6)" — the file count was also wrong (blockchain.cpp+block_validation.h+block_validation.cppis 3 files, not 4; the prior "4" likely double-countedcryptonote_config.hwhere the#definelives, but that's already counted in the adjacent "1 MTP#defineremoved" item). DIFFICULTY_*count (§9.2 inDAA_LWMA1.mdand Phase 4 work item 3 + YAML phase4-cpp-cutover todo inDAA_LWMA1_PLAN.md). Copilot flagged the plan's Phase 4 work item 3 ("six constants" but enumerating seven names); the same drift exists inDAA_LWMA1.md§9.2 line 1973 ("all five inheritedDIFFICULTY_*#defines and the two timestamp-validation#defines") and in the plan's YAML todo block (line 18: "Delete the 6 inherited DIFFICULTY_*"). The §9.2 enumeration has always listed sevenDIFFICULTY_*defines plus two timestamp-validation defines, and the §9.3 cross-reference at line 2022 ("the sevenDIFFICULTY_*defines plus FTL plus MTP") and the plan's breakdown at line 789 ("7DIFFICULTY_*defines removed") have always been correct. The prose at line 1973, the plan's work item 3 body, and the plan's YAML todo are now reconciled to "seven" everywhere.- Forward-looking discipline. Both drift instances share the same pattern as Round 10's item-number drift: prose totals composed by hand on top of enumerations that accumulated incrementally across review rounds. The fix going forward, per the Round 10 discipline, is the same: a pre-PR scan for "prose says N, enumeration says M" mismatches catches the class before it lands as a Copilot finding.
Status block on line 3 updates from "Round 10" to "Round 11" recording the cumulative review pass. No algorithm-level or consensus-rule changes in Round 11; the round is documentation drift remediation surfaced by the first AI-reviewer pass on the ready-for-review PR. (l) Phase 0 closeout (Round 12): §5.3 step 2 pseudocode reorder, Phase 1 pre-flight execution, hybrid-reference rename. (2026-05-18 UTC). Phase 0 ratified after 12 review rounds. Three load-bearing closeout actions in a single commit:
- Status block transition.
DAA_LWMA1.mdline 3 transitions from "Status: DRAFT — Round 11 …" to "Status: RATIFIED — Phase 0 close (2026-05-18 UTC) — 12 review rounds. Round 12 was the final round; the status reflects ratification, not 'round 12 of N.'" The status block now records the Round 12 findings inline (pseudocode reorder, pre-flight execution, hybrid-reference rename, three reference pins landed) so that a future reader of the design doc sees the closeout summary without needing to read the CHANGELOG. - §5.3 step 2 pseudocode reorder (load-bearing correctness
fix). Round 12 review identified an order-of-operations bug
in the §5.3 step 2 pseudocode that contradicted the
surrounding prose at lines 957–960 and 994–996. The
pre-Round-12 pseudocode read
prev_max = max(prev_max, timestamps[i-1]); solvetime[i] = timestamps[i] - prev_max;which, on the first loop iteration (i=1), executesprev_max = max(timestamps[0] - T, timestamps[0]), evaluating totimestamps[0]sinceT > 0. This overwrites the-Tanchor the surrounding prose claims is preserved, producingsolvetime[1] = timestamps[1] - timestamps[0]rather than the intendedsolvetime[1] = timestamps[1] - (timestamps[0] - T) = T + T = 2Ton the stable input. The pseudocode is now reordered to subtract-then-max:solvetime[i] = timestamps[i] - prev_max; prev_max = max(prev_max, timestamps[i]);. On the first iteration this correctly evaluatessolvetime[1] = timestamps[1] - (t0 - T) = 2T(using the-Tanchor), then updatesprev_max = max(t0 - T, t1) = t1. The prose at §5.3 lines 957–960 and 994–996 is updated to make the subtract-then-max semantics explicit, including the empirical observation (from the pre-flight harness, below) that the canonical zawy12LWMA1_()reference behaves equivalently to the corrected Shekyl pseudocode on monotonic inputs (both produce 990_000 on the §8.1 stable vector) but diverges on out-of-sequence inputs (canonical 990_000 vs Shekyl-corrected 992_000 on the Round 12 regression vector), confirming the running-max mechanism's security property is load-bearing rather than cosmetic. - Phase 1 pre-flight verification (executed at Phase 0 close
per §5.3 step 7). Built a minimal C++ harness from the
canonical
LWMA1_()reference transcribed verbatim fromdocs/design/refs/zawy12_issue_3_lwma1.md(lines 77–119 of the pinned.body), compiled withg++ -std=c++17 -O2, and ran against the §8.1 "perfectly stable hashrate" input vector withavg_D = 1_000_000,N = 90,T = 120, andtimestamps[i] = 1_700_000_000 + i*Tfori ∈ 0..=N. Result: canonical output990_000(matches §8.1 expected value). An initial harness run withtimestamps[i] = i*Tproduced10_000_000due touint64_t(0) - uint64_t(120)underflow attimestamps[0] - T; corrected to realistic Unix epoch timestamps and re-ran with the expected result. The Shekyl-corrected algorithm (transcribed fromdocs/design/refs/shekyl_lwma1_running_max_symmetric_clamp.md) was also compiled and run against the same stable input, producing byte-identical990_000(confirming §8.2's cross-check assertion that monotonic inputs match canonical byte-for-byte). An out-of-sequence regression vector (the same stable timestamps withtimestamps[2] = timestamps[1] - 5*T) produced canonical990_000(attack neutralized to+1via canonical'sprevious_timestamp+1floor; no penalty) versus Shekyl-corrected992_000(attacker's negative-solvetime contribution toLproduces highernext_D, denying the attack). The §5.3 step 7 stochastic-vs- deterministic framing and §8.1's stable-vector expected value are both empirically confirmed; the running-max mechanism's load-bearing security property in §5.3 step 2 is empirically verified by the regression vector.DAA_LWMA1.md§5.3 step 7 and §8.1 record the inputs, the actual outputs, and the divergence on the out-of-sequence vector;DAA_LWMA1_PLAN.md's Phase 1 pre-flight subsection records the executed result and preserves the reversion-clause triggers for any Phase 1 re-run that produces a different number. - Hybrid-reference rename
(
zawy12_issue_3_lwma1_with_lwma3_step2.md→shekyl_lwma1_running_max_symmetric_clamp.md). The Round 9 working name attributed the running-max + symmetric-clamp mechanism to canonical LWMA-3 ("with_lwma3_step2"), but canonical LWMA-3 (per thedocs/design/refs/zawy12_issue_3_lwma3.mdextraction referenced in the Phase 2 plan) does not actually implement running-max, signed-solvetimes, or symmetric clamping in the form §5.3 step 2 specifies — these are Shekyl-specific refinements drawing on the idea of LWMA-3's out-of-sequence handling but composed independently. The file is renamed toshekyl_lwma1_running_max_symmetric_clamp.mdto reflect the Shekyl-specific construction; the file's preamble documents the naming rationale, the empirical equivalence on monotonic inputs, and the divergence on the regression vector. All cross-references inDAA_LWMA1.md§3 andDAA_LWMA1_PLAN.mdare updated to the new name. Thezawy12_issue_3_lwma3.mdconvenience extraction (verbatim LWMA-3 reference, not a pin) remains a Phase 2 work item perDAA_LWMA1_PLAN.md; it is not load-bearing for Phase 1. - Three reference pins landed at Phase 0 close. Per the
Phase 0 close discipline obligation, the three Phase 2 spec-
pin files landed as a Phase 1 precondition:
docs/design/refs/zawy12_issue_3_lwma1.md(canonical LWMA-1 pin, SHA-25614c68aee9780ca1b1fb8ca28ac43f7956996859f5281ef166cc0634b2cc50df9, captured-at 2026-05-18T05:25:21Z),docs/design/refs/zawy12_issue_24_history.md(LWMA history issue pin, SHA-25694a6fc8f10b57cf7d0731f62d07c0b4bbdf65d969d7c8679755b22eace76891d, same capture timestamp), anddocs/design/refs/shekyl_lwma1_running_max_symmetric_clamp.md(Shekyl hybrid reference, SHA-256f16f62695ae74b2ca47d15227b79035cdc349609d9fc73db2b7a3c57c0dfcc4a, same capture timestamp).DAA_LWMA1.md§3's pin records embed the SHA-256s and timestamps; theLWMA1_()byte-offset anchors and the LWMA3_() convenience extraction remain Phase 2 work perDAA_LWMA1_PLAN.md(not load-bearing for Phase 1).
Status block on line 3 updates from "DRAFT — Round 11" to "RATIFIED — Phase 0 close (2026-05-18 UTC) — 12 review rounds." Phase 0 is closed; Phase 1 (
shekyl-difficultycrate scaffold perDAA_LWMA1_PLAN.md) opens against ratified spec. The pre-flight harness source (transcribed from the pinnedzawy12_issue_3_lwma1.mdLWMA1_() function) is available at this commit and is reproducible viag++ -std=c++17 -O2 preflight.cpp -o preflight && ./preflight.(m) Round 13 post-Phase-0-close cleanup (§5.3 step 9 canonical-rounding-step documentation, §8.1 base-anchor convention and arithmetic correction, harness commit). (2026-05-18 UTC.) Addresses Copilot PR #49 findings 3, 4, 5 surfaced after the Phase 0 close commit. Phase 0 stays ratified; Round 13 is post-ratification cleanup against the same design intent. Four load-bearing changes:
- §5.3 new step 9 — canonical zawy12 LWMA-1 trailing
rounding step. Documents the previously-undocumented
((next_D + r/2) / r) * rrounding-to-3-significant-decimal- digits step from canonicalLWMA1_()(zawy12_issue_3_lwma1.mdlines 116–119 of the pinned.body). The §8.1 expected values all depend on this step; without it, the raw outputs are989_758(stable),1_035_252(out-of-sequence), etc. — close but not byte-equal to the canonical 3-significant- digit values. Round 13 adds the step explicitly so the §8.2 canonical-reference byte-cross-check is well-defined, and includes a reversion clause requiring a §10 disposition for any future PR proposing to drop or alter it. - §8.1 timestamp base-anchor convention (Copilot finding
5). All §8.1 vectors are now specified as
timestamps[i] = B + f(i)withB = 1_700_000_000(Unix epoch base). The pre-Round-13 specification usedi*Tor(i-1)*Tformulas withBimplicit; the latter producedtimestamps[0] = -T, unrepresentable asu64(wraps to~1.8e19) and the cause of the pre-flight harness's initial10_000_000mis-output before the Round 12 correction. Base-anchoring is now a §8.1 invariant rather than a harness-side workaround. - §8.1 out-of-sequence and minimum-L-floor vectors — full
arithmetic rederivation (Copilot findings 3, 4). The
pre-Round-13 out-of-sequence vector's worked arithmetic
inflated the numerator by ~1000× and omitted the
rounding step entirely (numerator
97_297_560 * 10^7instead of97_297_200_000_000; quotient1_035_521_504instead of step-9-rounded1_040_000). Round 13 rederivesL = T*(N-1)*(N-2)/2 = 469_920, computes rawnext_D = 1_035_252, applies step 9 to round to1_040_000, and cross-checks against the harness output. The minimum-L floor vector's expected output drops from10_010_000(analytic, missing step 9) to10_000_000(step-9-rounded); the analytic intermediate is preserved in the prose so the rounding-step contribution is auditable. - §8.1 selfish-mine attack regression — pinned numerical
outputs. The Round-9-era assertion was relational only
("Shekyl > kyuupichan output," "Shekyl > all-monotonic-T
reference"). Round 13 pins the empirical values: canonical
911_000, Shekyl1_040_000. Canonical's911_000is below the990_000stable reference, surfacing the load-bearing property that canonical LWMA-1 actually rewards this attack class (lower difficulty post-attack means cheaper subsequent mining) — the regression Shekyl's running-max + symmetric-clamp formulation exists to fix. The §8.1 entry is rewritten to specify the canonical-and- Shekyl outputs side-by-side, the divergence ratio (~1.14×), and the four-part assertion the test vector must verify. - Pre-flight harness committed to
tests/phase0/. The three C++ harnesses produced during Phase 0 close and Round 13 (preflight.cpp,preflight_corrected.cpp,preflight_outofseq.cpp) are now committed alongside the design doc as authoritative reproducibility artifacts, withREADME.mdexplaining build/run/license. The MIT SPDX identifier covers the canonicalLWMA1_()transcription; the Shekyl variant header documents Shekyl Foundation origin. TheDAA_LWMA1.md§3 reference list and §8.1 vector-derivation footer point at the harness directory; the Phase 1 implementer reproduces the pinned values viag++ -std=c++17 -O2 preflight_outofseq.cpp -o p && ./pbefore opening Phase 1's first commit.
Round 13 leaves the
RATIFIED — Phase 0 close (2026-05-18 UTC)line onDAA_LWMA1.mdline 3 unchanged — Phase 0 closed at Round 12; Round 13 is post-ratification cleanup of finding-classes that surfaced after PR #49 was marked merge-ready. The summary paragraphs below line 3 are extended with a "Round 13 applied:" block listing the four changes above. Phase 1 remains unblocked. - Item #14 (September 2018 selfish-mine via out-of-sequence
timestamps). Algorithm-level change.
-
07-consensus-atomic-cutovers.mdc— named exception to branching policy for consensus-atomic cutovers (feat/consensus-atomic-cutovers-rule, 2026-05-17). New rule ratifying the "consensus-atomic cutover" exception class drafted during PR #49's Round 5 review (DAA_LWMA1_PLAN.mdPhase 4) and refined through Round 7 before landing.06-branching.mdc's 5-working-day / 10-commit splitting guidance defends against unreviewable PRs accumulating; this rule names the small class of PRs that genuinely cannot split because every intermediate state would be a non-canonical consensus configuration. The rule is opt-in (alwaysApply: false) — a PR that does not explicitly cite the rule cannot invoke it. Four objectively-testable criteria, all required:- Consensus-rule boundary. The PR changes behavior all correctly-implementing nodes must reproduce byte-identically on the same input. Refactors, RPC formatting, internal caches, renames, and file reorganizations of consensus code that preserve the rule do not qualify.
- Indivisible under flag decomposition. Met whenever
criterion 1 is met, for structural rather than contingent
reasons. A flag decomposition only counts as consensus-safe
if both flag states are simultaneously valid (build-system
flags, performance-tuning flags, instrumentation flags
qualify). For a consensus rule, simultaneous validity is
impossible by definition: the flag would have to dispatch
identically regardless of state, which means it doesn't
gate consensus behavior at all. Hard-fork activations are
the consensus event the PR ratifies, not a decomposition of
it; Shekyl's
60-no-monero-legacy.mdcno-version-dispatch posture forecloses any other interpretation. This shape closes the loophole where a PR author argues "yes, this is a consensus change, but splitting would be inconvenient": either the change affects consensus output (criteria 1+2 both met) or it does not (neither met). - Surface enumerated in advance, with evidence. A grep-result-derived enumeration of every consensus-affecting symbol/file/constant pasted into the PR description, run against the PR's base commit and timestamped at PR-open so reviewers re-run the same grep against the same base commit to verify the surface hasn't shifted.
- Disposition documented in PR. Numbered sub-clauses: 4.1 rule citation; 4.2 per-criterion justification; 4.3 reviewer-map (with enforcement: substantive consensus changes found outside the map's "consensus-affecting" subsection are grounds for rejecting the PR — the response is re-opening with a corrected enumeration, not patching the map); 4.4 rollback procedure (with enforcement: procedure must be executable by a reviewer who has not seen the PR; tacit-knowledge rollback procedures fail 4.4).
A "what this is not" section explicitly disqualifies convenience, velocity, reviewer bandwidth, and retroactive citation as justifications. A "compensating discipline" section names scope-creep within an exception-invoking PR as itself grounds for rejection. The rule records LWMA-1 Phase 4 as its first approved instance under "Approved invocations," and RandomX v2 Phase 3 under a separate "Cases that might appear analogous but are not" subsection — Phase 3 ships implementation routing (the 3a flag is a build-system / FFI-routing flag, not a consensus flag; the algorithm body is byte-identical on both sides), so criterion 1 is not met and the exception is structurally inapplicable, not "evaluated and rejected" (the latter framing would invite precedent-erosion arguments against future invocations). The mechanism for future invocations is self-anchoring: the invoking PR must include a commit that adds its own entry to the rule's history-of-application section, so the audit trail cannot be reconstructed retrospectively. Per
21-reversion-clause-discipline.mdc's named-criteria principle, the exception is auditable mechanically against the four criteria, not against LWMA-1-specific precedent erosion. -
RandomX v2 Rust port — Phase 0 design docs (
feat/randomx-v2-phase0-design, 2026-05-16). Adds three Phase 0 design documents underdocs/design/:RANDOMX_V2_RUST.md(the primary design),RANDOMX_V1_FALLBACK.md(the contingency design), andRANDOMX_V2_PLAN.md(the phased execution plan with sub-PR breakdown and gating diagram). The primary design pins the permanent C-miner / Rust-verifier split, derived-first verifier architecture under18-type-placement.mdc, the one-function FFI target, no-prewarm disposition, performance budgets, C-library symbol-isolation invariant, and the wallet V3.2 gate before Track B. The Grover-bound argument scaffold is recorded inRANDOMX_V2_RUST.md§10; the concrete release-checklist target-range calculation is explicitly deferred to Phase 0 review per §10's closing sentence rather than shipped in this PR. The fallback doc records the late-binding unpin-and-revert recovery path (102f8acfpin plus verifier toggle) for any time between Phase 0 and genesis release if the algorithm-review gate fails perRANDOMX_V2_RUST.md§1.4. -
LWMA-1 difficulty-adjustment migration — Phase 2 cross-check harness + FFI export (absorbs original Phase 3) (
feat/daa-lwma1-phase2, 2026-05-18). Lands the C++ cross-check harness that validates the Phase 1 Rust implementation against both the canonical zawy12 LWMA-1 reference and the Shekyl hybrid (running-max + symmetric-clamp) reference across the §8.1 test corpus perdocs/design/DAA_LWMA1_PLAN.mdPhase 2, and lands theshekyl_difficulty_lwma1_nextFFI export the harness consumes.Phase 2/3 absorption. The original plan separated Phase 2 ("harness only") from Phase 3 ("FFI export only"). The "or" clause in Phase 2 ("via FFI declared in Phase 3, or via a tiny test-only C++ wrapper") collapsed to a single architectural disposition on audit: any C++ caller into Rust requires
extern "C"symbols, and the only architecturally clean place to host them isshekyl-ffi(hosting inshekyl-difficultyitself would violate the Phase 1#![deny(unsafe_code)]posture; hosting as a throwaway test shim would be torn down by the original Phase 3 anyway). The two paths collapsed: land the production FFI export in Phase 2 alongside the harness, and have Phase 3 collapse to a "see Phase 2" plan-doc note. The Phase 2 PR is correspondingly larger but produces zero throwaway code; the harness is the integration test for the production FFI surface.catch_unwindpanic-safety wrapper dropped. The original Phase 3 prescription wrapped the FFI body instd::panic::catch_unwind. The workspace runspanic = "abort"in bothdevandreleaseprofiles (rust/Cargo.tomllines 103, 106); underpanic = "abort",catch_unwindis a no-op because panics terminate the process before any catch can engage. The Rust algorithm body is panic-free by construction (returnsResult<u128, Error>for every spec error path; uses explicitchecked_*/try_fromoverflow guards), and the §8.1 corpus exercises both branches. The FFI shim callslwma1_nextdirectly.SHEKYL_DIFFICULTY_ERR_INTERNAL(-4) remains reserved in the C header for forward compatibility but is not currently emitted.Reference pinning. Three pin records land:
docs/design/refs/zawy12_issue_3_lwma1.anchors.json— byte-offset- first/last-line anchors for
LWMA1_()and the upstream LWMA-3 function inside the pinned.body, plus the pinned-body SHA-256 cross-reference (14c68aee9780ca1b1fb8ca28ac43f7956996859f5281ef166cc0634b2cc50df9). The anchors file's own SHA-256 (406320ca29e67e564b7c13eb0fd706b393f0af7558fd99bac391a73542250783) and capture timestamp (2026-05-18T18:22:42Z) land inDAA_LWMA1.md§3 as the pin record.
- first/last-line anchors for
docs/design/refs/zawy12_issue_3_lwma3.md— convenience extraction of the canonical LWMA-3 (next_difficulty_v3) function from the pinned body. Shekyl-authored header (SPDXBSD-3-Clause AND MIT) plus byte-identical extraction against the anchors above. The pinned upstream LWMA-3 contains malformed C++ at upstream lines 376-381 (incompletenext_D =assignment and an unbalanced)in the jump-rule branch); the extraction preserves the malformation as-is, documented in both the file header andDAA_LWMA1.md§3. The closing-brace anchor at upstream line 384 is a textual delimiter, not a balanced-brace marker. SHA-256:9e2db49a7e2151177cced1748a3d0a4e7cb68ed2b0ecd0c2995cf86f38323671.
FFI surface (
rust/shekyl-ffi/src/difficulty_ffi.rs). New module exposingshekyl_difficulty_lwma1_nextas apub unsafe extern "C" fnwith theShekylU128two-u64 decomposition ABI perDAA_LWMA1.md§6.1 (Round 5's disposition against target-defined Rustu128C ABI). Error codes wire-stable at0/-1/-2/-3/-4; null-input pointers permitted iffcount == 0(genesis short-circuit). Five unit tests cover theShekylU128round-trip, the genesis path, the null-pointer rejection paths (bothoutand inputs-with-nonzero-count), and theERR_INVALID_COUNTmapping.C header (
src/shekyl/shekyl_ffi.h). Adds theshekyl_difficulty_lwma1_nextdeclaration, thestruct shekyl_u128definition, and theSHEKYL_DIFFICULTY_OK/SHEKYL_DIFFICULTY_ERR_NULL_PTR/_ERR_INVALID_COUNT/_ERR_OVERFLOW/_ERR_INTERNALmacros. The struct lives inside the existing top-of-fileextern "C"block; macros sit at file scope below the block per the C++ rule thatextern "C"applies to linkage of declarations, not to preprocessor symbols.**Cross-check harness (
tests/difficulty/lwma1_cross_check.cpptests/difficulty/zawy12_lwma1_reference.h+tests/difficulty/shekyl_lwma1_hybrid_reference.h).** Iterates the seven §8.1 vectors and asserts the documented cross- implementation relations:
- Vectors 1-5 (monotonic): canonical ≡ hybrid ≡ Rust (byte-equal at the §8.1 pinned outputs).
- Vectors 6-7 (out-of-sequence): hybrid ≡ Rust (byte-equal at
1_040_000), both strictly different from canonical (1_010_000for vector 6,911_000for vector 7 — the load-bearing security divergence per zawy12 issue #24 item 14).
The canonical reference header carries the MIT SPDX header citing the pinned-body byte-offset anchor; the hybrid reference header is BSD-3-Clause-MIT dual-licensed (canonical portions are MIT; the step-2/3 refinement is BSD-3-Clause per the Shekyl Foundation copyright). The harness uses
SHEKYL_DAA_*constants fromshekyl/consensus_constants_generated.h(Phase 1's JSON-authoritative emit) so any drift between the JSON authority and the harness expectations fails the build.CMake / ctest integration. Extends
tests/difficulty/CMakeLists.txtwith thelwma1-cross-checktarget (linked against${SHEKYL_FFI_LINK_LIBS}) and thelwma1_cross_checkctest registration. Harness reports 100 % passing across the §8.1 corpus; failure aborts the test. -
**LWMA-1 difficulty-adjustment migration — Phase 1 crate scaffold
- spec-vector tests** (
feat/daa-lwma1-phase1-crate, 2026-05-18). Lands the Rust craterust/shekyl-difficultyperdocs/design/DAA_LWMA1.mdandDAA_LWMA1_PLAN.mdPhase 1. Pure- arithmetic#![no_std]+#![deny(unsafe_code)]leaf crate with zero internal workspace deps; the FFI export (shekyl_difficulty_lwma1_nextwith theShekylU128ABI perDAA_LWMA1.md§6.1) is deferred to Phase 3 inshekyl-ffi.
Public surface.
lwma1_next(chain_height, ×tamps, &cumulative_difficulties) -> Result<u128, Error>transcribes the §5.3 algorithm verbatim (running-max + signed-solvetime per the §5.3 step-2 Shekyl refinement, symmetric ±6T clamp per step 3, i128 weighted-sum accumulation per step 4, min-L floor at N²T/20 per step 5, bias-corrected99/200formula per step 7, overflow guard per step 8, and the canonical rounding-to-3-significant-decimal- digits step 9 added in Round 13). Coupled timestamp predicatesis_timestamp_below_ftlandis_above_mtpco-located in the same crate perDAA_LWMA1.md§2.5. Window-shape constantsN,T_SECONDS,FTL_SECONDS,MTP_WINDOW,GENESIS_DIFFICULTYflow through the existingconfig/consensus_constants.jsonJSON authority (extended with fivedaa_*keys); the bias factor99/200, the solvetime clamp6, and the min-L floor divisor20deliberately stay as bare integer literals insidesrc/lwma1.rsper the Round 3 disposition (DAA_LWMA1.md§4) because changing them is a deviation from canonical zawy12 LWMA-1, not a tunable parameter.JSON-authority extension.
config/consensus_constants.jsonaddsdaa_window_n=90,daa_target_seconds=120,daa_ftl_seconds=540,daa_mtp_window=11,daa_genesis_difficulty=100.cmake/generate_consensus_constants.pyextendsKEYS_INTEGERand the emitted header with fiveSHEKYL_DAA_*macros; until Phase 4 lands, these macros are emitted but have no C++ consumer (the Phase 4 cutover replaces inheritedDIFFICULTY_TARGET_V2,CRYPTONOTE_BLOCK_FUTURE_TIME_LIMIT, andBLOCKCHAIN_TIMESTAMP_CHECK_WINDOW).rust/shekyl-difficulty/build.rsreads the same JSON and emits the Rust mirrors toOUT_DIR(Round 3's Option A; extendingshekyl-engine-core/build.rswould have broken the leaf-crate property). The build script also emitsusizemirrors ofNandMTP_WINDOWas plainusizeliterals rather than viausize::try_from(u64)in a const block, becauseTryFrom::try_fromis not yet const-trait-stable in rustc 1.95.0 (issue #143874); this keeps the workspace'scast_possible_truncation = "deny"lint clean without per-site#[allow]annotations.Test corpus. 18 tests all pass with the workspace's full lint suite under
-D warnings. The 7 §8.1 spec vectors reproduce the Phase 0 C++ harness outputs byte-for-byte:990_000(stable),1_980_000(2× up),495_000(2× down),892_000(clamp engagement),10_000_000(min-L floor),1_040_000(out-of- sequence single back-step, Shekyl ≠ canonical's1_010_000),1_040_000(selfish-mine attack regression, Shekyl ≠ canonical's911_000). Edge cases: genesis short-circuit acrosschain_height ∈ 0..NreturnsGENESIS_DIFFICULTY, the §5.3 step-1 boundary surfacesError::InvalidCounton length mismatch, a non- monotonic cumulative-difficulty input surfacesError::Overflow, both branches of the §5.3 step-8 overflow guard execute cleanly, thesolvetime[1] = -Tregression computes without overflow, and the FTL/MTP predicates cover their respective boundaries.Gates. Per
45-rust-lint-checks.mdc,cargo test --package shekyl-difficulty,cargo clippy --package shekyl-difficulty --all-targets -- -D warnings, andcargo fmt --package shekyl-difficulty -- --checkall pass.cargo check --workspacepasses (the JSON authority extension does not affect existing consumers;shekyl-engine-core/build.rscontinues to read only the FCMP/RCT keys it already consumed). - spec-vector tests** (
Changed
-
Stage 1 PR 5 —
Engineparameterized overP: PendingTxEngine(fifth type parameter) (feat/stage-1-pr5-pending-tx-engine, C6 =0713591bf; defaultP = LocalPendingTx<LocalSigner, WalletGreedyOutputSelector, DaemonFeeEstimator>). Orchestrator methodsbuild_pending_tx/submit_pending_tx/discard_pending_tx/outstanding_reservationsdispatch throughself.pendingrather than readingEngine's former inline reservation map.Engine::replace_pending_tx(test-helpers) mirrors PR 4'sreplace_refreshpattern. -
Stage 1 PR 5 —
Engine::discard_pending_txreason-parameter drop at orchestrator boundary (C6). The orchestrator-facingdiscard_pending_tx(id)no longer acceptsDiscardReason; the trait surface retainsdiscard(id, reason)for V3.x consumer actors (ReservationTTLActor, etc.). Test call sites narrowed per the PR 4 precedent. -
Stage 1 PR 5 — reservation / pending-tx data-shape augmentation (C2γ).
Reservationgainssnapshot_id,extensions, and collection-membership encoding (noReservationStateenum under segment 2h);PendingTxgainssnapshot_id. Submit snapshot staleness returnsSubmitError::SnapshotInvalidatedwith rich ids; terminal daemon failures emitDiscarded { DaemonRejectedTerminal }; ambiguous failures emitSubmitPendingResolutionand keep the reservationin_flight. -
Stage 1 PR 5 —
engine/pending.rsfree-function extraction (C5β). Production paths live onLocalPendingTx; legacybuild_pending_tx_in_state/submit_pending_tx_in_state/discard_pending_tx_in_stateremain under#[cfg(test)]for migrated unit tests. -
Stage 1 PR 4 —
Engineparameterized overR: RefreshEngine(fourth type parameter) (feat/stage-1-pr4-refresh-engine, PR 4 C5a =553d70139; defaultR = LocalRefreshper the Round 4 turnkey-default discipline).Engine<S: Signer>becomesEngine<S: Signer, D: DaemonEngine = DaemonClient, L: LedgerEngine = LocalLedger, R: RefreshEngine = LocalRefresh>atengine/mod.rs. The orchestrator retry loop inengine/refresh.rsmigrates from a free-functionproduce_scan_result(...)to trait dispatch onRviaself.refresh.produce_scan_result(...)per PR 4 C5 =7140f726a; the legacy producer scaffolding (produce_scan_resultfree function +ProduceError+ProgressEmitter+ duplicated helpers + constants) is deleted fromengine/refresh.rsper PR 4 C5β =b6a1274de. The newEngine::replace_refreshtest-only constructor (consume-and- rebuild; refactored at PR 4 C7 =c9e65bbc6from its initial&mut selfsetter form per PR 4 C6α =e9310542a) lets theRtype parameter change between construction and replacement so test orchestration can build the engine withLocalRefreshat assemble time and rewire toFaultInjecting<LocalRefresh>for failure-injection scenarios.ViewMaterial::try_from_keysatengine/view_material.rsderives the trait-required view-and-spend material from theKeyEngineat engine-assemble time, populating theLocalRefreshconstructor argument. Crate-level public APIs consuming the engine type alias (Wallet,WalletWithLedger<L>test helpers,RefreshHandle, the benchmark fixtures) thread the additional type parameters forward with appropriate defaults; no consumer outside theshekyl-engine-corecrate is required to nameRexplicitly under the default-parameter discipline. -
Stage 1 PR 4 —
RefreshError::InternalInvariantViolation { context: &'static str }variant addition (PR 4 C3 =c45894ffe; Phase 0c amendment perdocs/design/STAGE_1_PR_4_REFRESH_ENGINE.md§5.4.7 R6 close-out). Resolves the Round 2 R6 "(a) extendConcurrentMutationor (b) introduceInternalInvariantViolation" cleanup pin at the design layer. Disposition (b): conflating "wallet under sustained merge contention" and "wallet hit an internal bug" intoConcurrentMutationwould deny downstream consumers (PeerReputationActor, telemetry, user-facing error surface) the structural distinction they need to respond correctly. The retry-loop call sites inengine/refresh.rs(per PR 4 C5 =7140f726a) and theRefreshHandle::joindropped-sender site surface state-machine invariant violations asInternalInvariantViolation { context }with compile-time-fixed developer content;&'static stris appropriate at the orchestrator-internal site because the field carries no attacker-influenced data (the memory- amplifier and log-exfiltration vectors the producer-trait unit-variant discipline closes do not apply here). The variant is one of threeRefreshErrorvariants reachable from aRefreshEngineimpl'sSelf::ErrorviaInto(alongsideCancelledunit andIo(IoError)); the other three variants (MalformedScanResult,ConcurrentMutation,AlreadyRunning) are orchestrator-constructed only per the §6.1.1 two-enum architecture pin and the F-Mock-3-sharpening trait-reachable-variant enumeration.
Removed
-
Electrum-words subsystem — Phase 2: JSON-RPC surface deletion (
feat/electrum-words-removal-phase2-rpc-deletion, 2026-05-19). Deletes the inherited CryptoNote 25-word seed surface from the wallet JSON-RPC layer perdocs/design/ELECTRUM_WORDS_REMOVAL_PLAN.mdPhase 2 anddocs/design/ELECTRUM_WORDS_REMOVAL.mdsubstrate §2.4. Closes Phase 0 Mission Audit Lens B finding B-1 at the RPC layer (FFI +wallet2core deletions follow in Phase 3 / Phase 4 / Phase 5). Landed across the commit list below (the bullet list is the source of truth; explicit count omitted because review iterations add closeout commits). All insrc/wallet/wallet_rpc_server*plustests/:restore_deterministic_walletJSON-RPC method + handler +COMMAND_RPC_RESTORE_DETERMINISTIC_WALLETrequest/response structs deleted. The method took a 25-word Electrum seed + optionalseed_offset+languageand reconstructed an account; Shekyl wallets restore from raw seeds via theshekyl_account_generate_from_raw_seedFFI surface (testnet/fakechain only perrust/shekyl-crypto-pq/src/account.rs's permitted network/seed-format matrix), not from word lists.get_languagesJSON-RPC method + handler +COMMAND_RPC_GET_LANGUAGESrequest/response structs deleted. The method enumerated Electrum-word language packs that have no analogue in the Shekyl seed flow.languagerequest field +is_valid_language(req.language)validation branch +wal->set_seed_language(req.language)call removed fromCOMMAND_RPC_CREATE_WALLETandCOMMAND_RPC_GENERATE_FROM_KEYS. The request-schema change drops the field from the deserializer surface. epee KV-serialization is pull-based: keys the consumer struct does not declare are silently ignored, so callers that still sendlanguage="English"do not see a parse error — the value is dropped on the floor andwallet2::generate()runs with the wallet2 default seed language. The §4.3 hard-error discipline is enforced at the FFI surface (Phase 1wallet2_ffi_create_wallet/wallet2_ffi_generate_from_keysreject non-emptylanguagepersrc/wallet/wallet2_ffi.cpp:309–320, 485–495); the wallet-RPC handler reacheswallet2::generate()directly, so the FFI's hard-error gate is not on this code path. The load-bearing property at the wallet-RPC layer is therefore structural unreachability of the field from request parsing — no read path from JSON to behavior — rather than runtime rejection. Phase 3 deletes the FFI parameter entirely, collapsing both surfaces. The underlyingwallet2::set_seed_languageandcrypto::ElectrumWords::is_valid_languagesymbols still exist (called fromwallet2_ffi.cppandwallet2internals); their full removal lands with the mnemonics module in Phase 5.seedandseed_offsetrequest fields + the entire seed-recovery branch (theif (!req.seed.empty()) { words_to_bytes / decrypt_key / account.generate(...) / spend-key match check }block atwallet_rpc_server.cpp:2316–2366) removed fromCOMMAND_RPC_STOP_BACKGROUND_SYNC. The branch was P0-broken on mainnet/stagenet under the legacy 3-argaccount.generate()overload (constant-drift auditdocs/audit_trail/2026-05-ffi-constant-drift-audit.md); password-onlystop_background_syncsurvives unchanged. A BIP39 / raw-seed replacement is adocs/FOLLOWUPS.mdV3.2 item, not Phase 2 scope.#include "mnemonics/electrum-words.h"removed fromwallet_rpc_server.cpp(no remaining ElectrumWords callers in the file).tests/functional_tests/(29 files, 6,786 lines) deleted outright. The plan-doc draft proposed migrating 12 (actually 28)restore_deterministic_walletand 3 (actually 4)stop_background_sync(seed=...)call sites to surviving RPC methods. Pre-flight investigation (2026-05-19) surfaced four blockers that flipped the disposition from migrate to delete: (a) the harness invokesmonerod/monero-wallet-rpcbinaries that don't exist in the Shekyl tree; (b)functional_tests_rpcandcheck_missing_rpc_methodswere silently skipped in CI because the build environment lacked therequests/psutil/monotonic/deepdiffPython deps atcmakeconfigure time — inherited dead code with no live caller; (c)shekyl-wallet-rpclacks a--regtest/--fakechainflag and defaults to mainnet, so the FFI rejects raw-seed restore on the regtest daemon's fakechain network; (d) the harness is Monero-shaped end-to-end and warrants a Shekyl-native rewrite under its own design doc, not a "while we're here" revival here. Per15-deletion-and-debt.mdc's default-delete posture, deletion is the disposition.add_subdirectory(functional_tests)removed fromtests/CMakeLists.txt; the Functional tests section oftests/README.mdis rewritten to record the deletion + the planning posture for a Shekyl-native replacement.
Build verification:
wallet_rpc_server,wallet,shekyld, andunit_teststargets all build clean;unit_testsctest pass (306s, 0 failures). -
Vestigial CLSAG-era
ring_sizefield (Phase 0 Mission Audit Lens E finding E.2-A; Batch α PR 2) (chore/audit-batch-alpha-pr2-ring-size-cleanup, 2026-05-17). Removes the surviving CLSAG-era "ring signature size" parameter from the C++ wallet RPC surface and from two blockchain-utility residue sites. Under FCMP++ with full-chain membership proofs, there is no user-tunable ring size; the anonymity set is the entire UTXO set. This entry completes the cleanup begun by the prior Rust-sidering_sizeremoval recorded above ("Decoy andring_sizeremoval from Rust RPC") by deleting the remaining C++ residue that pre-genesis audit reviewers would otherwise read as semantically live.Wallet RPC surface (
src/wallet/wallet_rpc_server_commands_defs.h). Deletedring_sizefield + serializer from four request structs (COMMAND_RPC_TRANSFER,COMMAND_RPC_TRANSFER_SPLIT,COMMAND_RPC_SWEEP_ALL,COMMAND_RPC_SWEEP_SINGLE; all four were accepted-and-ignored viaKV_SERIALIZE_OPT(..., (uint64_t)0)with zero readers in the post-FCMP++ codepath) and from one response struct (transfer_descriptioninsideCOMMAND_RPC_DESCRIBE_TRANSFER;KV_SERIALIZE(ring_size)mandatory in response, populated by the now-meaningless min-across-sources walk below).Wallet RPC handler (
src/wallet/wallet_rpc_server.cpp). Deleted the L1503–1505min(cd.sources[s].outputs.size())walk that populateddesc.ring_size; under FCMP++,cd.sources[s].outputs.size()does not represent a CLSAG ring and the computed value has no consensus meaning. Adjusted theres.desc.push_back({...})brace-init at L1471 to drop the correspondingstd::numeric_limits<uint32_t>::max()third element.Blockchain logging (
src/cryptonote_core/blockchain.cpp). Removed thering_sizelocal at L3192 and reformatted theMINFOlog line fromI/M/OtoI/O(inputs/outputs). Under FCMP++ the "M" (mixin / ring-member count) field pulled fromtxin_to_key.key_offsets.size()no longer represents a CLSAG ring and was a vestigial logging residue.Blockchain-usage analysis utility (
src/blockchain_utilities/blockchain_usage.cpp). Removed thering_sizefield from thereferencestruct, the corresponding constructor parameter (uint64_t rs), and updated the sole call site at L216 fromreference(height, txin.key_offsets.size(), n)toreference(height, n). The field was write-only across the utility's lifetime; the per-output frequency accounting at the loop's tail (L222–236) countsout.second.size()only.Scope and rationale. Pre-genesis Rule-60 residue cleanup per
.cursor/rules/60-no-monero-legacy.mdc. The standalone-PR disposition (rather than folding into the V3.1+ Legacywallet_rpc_serverRust cutover) was selected because folding means vestigialring_sizeships at genesis = concrete audit-confusion vector for genesis-audit reviewers (5 RPC structs + desc calc + log line + utility struct all look semantically live without reading FCMP++ disambiguators). Bisectable, mechanical, no architectural implications. Production- source diff (excluding this CHANGELOG entry, which adds ~70 lines of documentation delta):4 files changed, 4 insertions(+), 19 deletions(-). Not RingCT proper —rct::*types, output commitments, Bulletproofs+ range proofs, and the wider RCT machinery remain load-bearing underRCTTypeFcmpPlusPlusPqc.
Changed
-
RandomX v2 Phase 0 — Copilot PR #45 Round 2 findings addressed (5 inline + 16 low-confidence suppressed) (
feat/randomx-v2-phase0-design, 2026-05-17). Round 2 of Copilot's inline review surfaced 5 inline comments and 16 low-confidence suppressed findings against the four design documents. Triage and disposition follow; all 21 were accepted with fixes (no rejections). The findings clustered into seven themes:-
Error-taxonomy ambiguity (RUST.md:776, PLAN.md:290).
ERR_CACHE_DERIVE_FAILEDandERR_INTERNALboth claimed coverage of "Rust panic caught at FFI shim," making the taxonomy ambiguous for implementers. Resolved by assigning panics uniformly toERR_INTERNAL (-4)viacatch_unwindat the shim, whileERR_CACHE_DERIVE_FAILED (-3)covers only structured VM-level failures the derivation deliberately returns (e.g., debug_assert paths). The two codes are now disjoint by construction; the PLAN.md §2e prose mirrors the §17 taxonomy verbatim so future drift is impossible. -
Reviewer-rule misattribution (RUST.md:934, FOLLOWUPS reviewer-discipline rules-queue entry). Both entries cited
.cursor/rules/06-branching.mdcas the source of an "at least one reviewer who is not the author" rule. Verified against the file:06-branching.mdcgoverns branch flow and release operations and contains no reviewer-count rule. Rewrote both to acknowledge the requirement is an aspirational project convention, not a codified rule, and to record that the V3.124-reviewer-discipline.mdcrules-queue entry is the introducing rule rather than a promotion of an existing one. -
cncryptoPUBLIC-link survey gaps (RUST.md:499, PLAN.md:338/402, CHANGELOG.md:98). The Round 1 survey expansion (from 4 to 9 targets) was still incomplete and misnamedmonero_fcmp_pp_crypto. Re-ran the survey against the pinned tree: correctedmonero_fcmp_pp_crypto→fcmp_basic(withfcmpas the secondsrc/fcmp/target); addedsrc/blockchain_db/,src/checkpoints/,src/device/, andsrc/wallet/(wallet_rpc_server) to the production-src/direct-consumer list; addedtests/wallet_bench/,tests/daemon_tests/,tests/functional_tests/(two targets),tests/hash/, andtests/performance_tests/to the test-target list. Total direct-consumer count grew from 9 to 19 (13 production + 6 test). Also clarified the §10 vs §11 citation in PLAN.md: the survey is RUST.md §11, not §10 (§10 is the Grover-bound section); two PLAN.md references corrected. -
Phase 3c / Phase 4 ordering hazard (PLAN.md:338). Phase 3c deletes
slow-hash.c/rx-slow-hash.c/pow_cryptonight.cpptogether, butsrc/cryptonote_basic/miner.cppstill declaresslow_hash_allocate_state/slow_hash_free_stateextern "C"andsrc/cryptonote_basic/cryptonote_format_utils.cppstill callscrypto::rx_slow_hashandcrypto::cn_slow_hash(PoW and KDF). Phase 4 was scheduled to remove these callers, so the intermediate state between 3c-landed and 4-landed would not build. Added an explicit ordering precondition: Phase 3c assumes §15 (RPC payments delete) and Phase 4 (version-gate + IPowSchema deletion) have already cleared theminer.cppandcryptonote_format_utils.cppcall sites; if any caller remains at 3c open-time, the ordering is to pull that caller's removal forward into the 3c PR. Also noted thatcryptonote_format_utils.cpp'scn_slow_hashcalls at lines 1465/1473 are non-PoW KDFs that need a Rust-side replacement before 3c — a Phase 4 deliverable. -
RPC-payments §15.4 incompleteness (RUST.md:642). The deletion checklist omitted three surfaces:
src/rpc/core_rpc_ffi.cpp(registers the sixrpc_access_*JSON-RPC dispatch entries),src/rpc/core_rpc_server_commands_defs.h(defines theCOMMAND_RPC_ACCESS_*request/response structs), andtests/functional_tests/functional_tests_rpc.py(includes'rpc_payment'inDEFAULT_TESTSat line 13). All three added to the checklist. -
Section-number drift (CHANGELOG.md:23 inline + multiple §10 vs §11 references in PLAN.md). The May-16 changelog said "Grover-bound argument scaffold is recorded in
RANDOMX_V2_RUST.md§9," but the actual section is §10 (§9 is "Environment and Consensus Constants"). The PLAN.md Phase 3c step and its corresponding Risk acknowledgement said "Phase 0 §10 PUBLIC-link survey" where the survey is RUST.md §11. All corrected to RUST.md §10 (Grover) and §11 (cncrypto) respectively. -
Smaller items. (a)
#[export_name]CI grep extended in PLAN.md §2f to cover both bare and#[unsafe(export_name = "...")]forms, mirroring the existingno_manglepattern; the RUST.md §7.2 prose now names both spellings explicitly so the design doc and the CI grep cite the same patterns. (b) PLAN.md §6 "5 hours of baseline PoW work" rewritten to match RUST.md §8's canonical numbers: 2-hour C baseline (12 ms × 600k), 4-hour delta at 3.0× ratio, 6-hour Rust-target total. (c) PLAN.md §9 Grover "√2 speedup" corrected to "square- root speedup against unstructured preimage search, ~2²⁵⁶ → ~2¹²⁸" matching RUST.md §10. (d) PLAN.md §15 "rewrite or delete" reframed as the resolved-to-delete disposition. (e) PLAN.md §5 (and RUST.md §5)seedheight(height) -> u64discretionary export reshaped to the samei32-return + out-parameter discipline as the committed hash export, per40-ffi-discipline.mdc. (f) FALLBACK.md §2's "external/randomx-v2is not added in fallback mode" framing split into pre-Phase-1 and post-Phase-1 cases so §1's late-binding framing is honored. (g) FALLBACK.md §4 "filled after RUST.md §1" placeholder replaced with the concrete list of v2-deferred improvements drawn from RUST.md §1.3 (CFROUND throttling, F/E AES mix, program size, prefetch lookahead, efficiency-per-watt aggregate). (h) CHANGELOG.md May-16 entry's "six places" replaced with "eight places" so the count matches the enumeration that follows.
Files touched:
docs/CHANGELOG.md,docs/design/RANDOMX_V2_PLAN.md,docs/design/RANDOMX_V2_RUST.md,docs/design/RANDOMX_V1_FALLBACK.md,docs/FOLLOWUPS.md. -
-
RandomX v2 Phase 0 — Copilot PR-review-bot findings triaged and addressed (PR #45) (
feat/randomx-v2-phase0-design, 2026-05-16). Copilot's inline review of PR #45 surfaced 13 findings against the Phase 0 design docs. Triage and disposition follow; 12 accepted with fixes, 1 accepted as a CHANGELOG-only softening (the Grover §9 placeholder is intentional Phase 0 work, but the CHANGELOG previously overpromised that it was shipped).Fixes in this commit:
- CHANGELOG: PLAN.md added to the "Added" entry alongside RUST.md and FALLBACK.md (PLAN.md was added in this PR but the Added entry only named two of the three design docs). The Grover-bound claim softened to "scaffold recorded in §9; concrete release-checklist calculation deferred to Phase 0 review per §9's closing sentence."
- PLAN.md frontmatter:
overview:value wrapped in double- quotes per the WALLET_REWRITE_PLAN.md precedent so the unquoted:sequences (No prewarm: lazy, etc.) no longer break YAML parsing. Confirmed parsing viapython3 -c "import yaml; ...". - PLAN.md Decision #6 cost analysis + §6 perf budget: rewrote the "below Nielsen's 100 ms threshold" claim (mathematically wrong: 150 ms > 100 ms). New framing: "above 100 ms by ~50 ms but well below the 1 s continuous-flow threshold, and invisible in practical RPC-round-trip context."
- PLAN.md root-relative links: 32 cross-references rewritten
from repo-root-relative (
](src/...),](rust/...), etc.) to proper relative paths (](../../src/...)) so GitHub renders them correctly fromdocs/design/. Verified each rewritten link resolves to a real path (29 OK; 2 intentional forward references:external/randomx-v2is added by Phase 1,RANDOMX_V2_PHASE_3B_DELETED_CALL_AUDIT.mdby Phase 3b). - PLAN.md Phase 2f C-ABI exports invariant: the existing
extern\s*"C"\s*\{grep matches only foreign import blocks, not thepub extern "C" fnshape the invariant is supposed to forbid. Replaced with three explicit patterns:#[no_mangle](both spellings),extern "C" fn(any function declaration), and#[export_name = "..."](the bypass shape). The intent of each pattern is documented inline. - PLAN.md Phase 3 caller-survey scope: added an explicit
clarifying paragraph noting that the "six C++ daemon-side
caller files" is the Phase 3 rewire set, not the full repo-
wide
rx_*footprint. The four additional files Copilot's grep surfaced (miner.cpp,cryptonote_format_utils.cpp,rpc_payment.cpp,wallet_rpc_payments.cpp) are intentionally handled by §15 (RPC payments deletion) and Phase 4 (version- gate + IPowSchema deletion), not by Phase 3. - PLAN.md Phase 2e allocation guidance: softened the
misleading OOM coverage. The Phase 2e allocation APIs
(
Box::new_zeroed_slice,vec![]) are infallible: they abort on OOM rather than return an error, so the FFI shim never sees a result it could map toERR_CACHE_DERIVE_FAILED. The plan now records that OOM at cache derivation aborts and that a fallible-allocation path with anERR_CACHE_ALLOC_FAILEDtaxonomy entry is V3.x work if any future caller needs OOM- recoverable derivation. - RUST.md §1.2 reference clone: removed the
contributor-specific absolute path
/home/torvaldsl/shekyl/RandomX/(committing a single developer's$HOMEpath is non-reproducible). Replaced with a portable description noting that Phase 0 contributors may keep a sibling clone at the same pin as a contributor-local convention, with a fork URL for those who prefer not to. - RUST.md §11 cncrypto PUBLIC-link survey: expanded the
direct-consumer list from 4 targets to 9, adding the load-
bearing
commonlink (which sits below most subsystems and transitively re-exportsrandomx_*to everything depending oncommon) pluscryptonote_basic(two targets),cryptonote_core,daemon, andfcmp(two targets). Also recorded thattests/crypto/CMakeLists.txt'scncrypto-testsdoes not linkcncryptodirectly (it linkscommonand gets cncrypto transitively); the test name is historical. Phase 3 link-drop checklist is now accurate. - RUST.md §19 audit doc filename: renamed
RANDOMX_V2_PHASE3B_AUDIT.md(RUST.md's spelling) toRANDOMX_V2_PHASE_3B_DELETED_CALL_AUDIT.md(PLAN.md's canonical spelling). Single canonical filename across both design docs. - RUST.md §17 ERR_CACHE_DERIVE_FAILED semantics: clarified
that this code covers VM-level failures and Rust panics caught
at the FFI shim, not allocation failure. OOM during cache
derivation aborts the process via
handle_alloc_errorper Rust's default allocator; this is consistent with PLAN.md §2e's infallible-allocation choice. A futureERR_CACHE_ALLOC_FAILED (-5)entry is sketched as V3.x work if a caller ever needs OOM-recoverable derivation. - FALLBACK.md status block: rewrote the L6 status block to match §1's late-binding framing. The previous text ("invoked only if Phase 0 review concludes RandomX v2 is not ready") contradicted the §1 round-1 revision that made the fallback invocable any time between Phase 0 and genesis release.
Findings rejected or partially addressed:
- Grover §9 placeholder (RUST.md L481): Copilot flagged §9 as incomplete because it ends with "Phase 0 review must fill this section with the concrete target-range calculation." The placeholder is intentional — concrete numbers depend on Shekyl's final difficulty-target tuning, which is a Phase 0 review item, not implementation. Disposition: keep §9 as-is; soften the CHANGELOG's claim about Grover-bound coverage (done in this commit) so the doc-vs-changelog asymmetry resolves.
Files touched:
docs/CHANGELOG.md,docs/design/RANDOMX_V2_PLAN.md,docs/design/RANDOMX_V2_RUST.md,docs/design/RANDOMX_V1_FALLBACK.md. -
RandomX v2 Phase 0 — plan-vs-design-doc drift fix and four smaller items (
feat/randomx-v2-phase0-design, 2026-05-16). The previous round moved the algorithm-review gate from "before Phase 2" to release- time inRANDOMX_V2_RUST.md§1.4, butRANDOMX_V2_PLAN.mdstill carried the old Phase-2-gate framing in eight places: frontmatteralgorithm-review-gatetodo, frontmatteroverviewtext, frontmatterphase5-docstodo, body §"Algorithm-review gate (Track A intra-track)", body §"Track A — Algorithm-review gate", body §"Track A — Phase 2 (gated on algorithm review)" title, body §"Risk acknowledgments" v2-algorithm-posture entry, and the mermaid diagram. This commit aligns the plan with the design doc: the gate is release-time, Phase 2 proceeds in parallel with Monero's audit, and the mermaid diagram redrawn so the release gate sits after Phase 5 withMonAudit/MonDeployas parallel external inputs that don't block Track A or Track B. Also folds in four smaller items from the same review pass: (a)RANDOMX_V2_RUST.md§16 gains aconst _: () = assert!(...)compile-time assertion thatSEEDHASH_EPOCH_BLOCKS.is_power_of_two(), because the& !(SEEDHASH_EPOCH_BLOCKS - 1)mask in theseedheight()formula silently produces the wrong consensus result if the constant is ever changed to a non-power-of-2. (b) §17 adds an explicit four-case table for thedata/data_lenpairing so thedata == NULL && data_len == 0empty-input case is no longer ambiguous at the FFI boundary. (c) §23 gains §23.1 recording the per-gate reviewer-discipline calibration pattern as a candidate for promotion to.cursor/rules/24-reviewer-discipline.mdc. (d) Two new V3.1 FOLLOWUPS entries inFOLLOWUPS.md: one tracking the §22 Guix reproducible-build obligation pickup (fires when Guix integration lands; closes when the Guix-integration design doc rewrites §22 to point at the actual manifest), and one tracking the §23.1 reviewer-discipline rule promotion (sibling to the existing rules-queue entries). Softens the previous round's framing: the RandomX v2 work is primarily fresh debt clearance (IPowSchema/pow_registry,shekyl-consensus, RPC payments, and therx-slow-hash.cstateful core were not previously tracked in FOLLOWUPS), so the Phase 5 FOLLOWUPS pass is mostly forward-looking close-records rather than closure of pre-existing items. The V3.0 pre-genesis queue's accumulation/resolution trajectory is unaffected by this work. -
RandomX v2 Phase 0 — algorithm-review gate moves from Phase-2 to release (
feat/randomx-v2-phase0-design, 2026-05-16). RewritesRANDOMX_V2_RUST.md§1.4 to record that the two Phase-0 open questions ("who else deploys v2?" and "who funds the v1→v2 delta audit?") both resolve to Monero. Monero is in the process of deploying upstream RandomX v2 (PR #317) in parallel with Shekyl's implementation, and is funding the delta audit. Because Shekyl is non-divergent from upstream (§1.1) the audit's scope covers Shekyl's pinned code byte-for-byte; Shekyl inherits the audit result without coordinating it. The previously-listed "algorithm-review gate before Phase 2" is removed — Phase 2 is faithful spec implementation, not an algorithm-soundness decision, and gating it on external work Shekyl does not control would either delay or duplicate effort. §1.4 introduces the explicit release-time gate: before genesis, Monero's production v2 deployment must have had meaningful observation-window exposure AND the Monero-funded delta audit must have completed without contraindicating findings. §1.1 records that non-divergence is a load-bearing strategic posture — what buys Shekyl audit inheritance and the unpin-and-revert v1 fallback — not an accident. §23 reviewer-discipline updated to reflect the release-time gate and to distinguish inherited external review (via non-divergence) from Shekyl-direct external review.RANDOMX_V1_FALLBACK.md§1 reframes the fallback as late-binding (any time between Phase 0 and release), unpin-to-102f8acfrather than stop-and-restart, with explicit Production-deployment-failure and Inheritance- failure trigger classes added to the existing list. Plan todoalgorithm-review-gaterewritten from a Phase-2 blocker to a release-time gate that runs in parallel with implementation work. -
RandomX v2 Phase 0 — fork relationship and pinned source recorded (
feat/randomx-v2-phase0-design, 2026-05-16). RewritesRANDOMX_V2_RUST.md§1 from a forward-looking "Shekyl-controlled divergence" framing to the empirical picture: theShekyl-Foundation/RandomXfork tracks upstreamtevador/RandomXwithout divergence; RandomX v2 is the upstream tevador algorithm landed in PR #317 (commitbb6ed2c); and the fork's pinned commit isaaafe71("Prepare v2.0.1 release", 2026-05-10). (The original draft of §1.2 named a contributor-local sibling-clone path; that path was removed in the same review round per the portable-path rule, see the later Changed entry. The path is intentionally not quoted here either.) §1.3 distills the four concrete v1→v2 changes from the fork'sdoc/design_v2.md(CFROUND throttling, F/E AES mix replacing XOR, program-size 256→384, two-iteration dataset prefetch lookahead) and their ~130-165 % efficiency improvement on Zen 3/4/5 silicon. §1.4 records the algorithm-review status: the four 2019 audits in the fork'saudits/directory (Trail of Bits, X41, Kudelski, Quarkslab) cover v1 and bound the Phase 2 review scope to the v1→v2 delta rather than RandomX from scratch. §3 names the three normative spec files (doc/specs.md,doc/design_v2.md,doc/configuration.md) as the Rust port's source-of-truth references. §11 records that the currentexternal/randomxsubmodule is at v1-era102f8acfand Phase 1 addsexternal/randomx-v2ataaafe71as a new submodule alongside it (not a repoint) so the v1→v2 swap is a single reviewable commit later.RANDOMX_V1_FALLBACK.md§2 records that v1 lives at any pre-PR-#317 commit on the same fork (default fallback pin:102f8acf, already in the existing submodule), and §3 records that the four 2019 audits already ship in the fork'saudits/directory at the pinned v1 commit. -
RandomX v2 Phase 0 — RPC-payments disposition resolved to delete (
feat/randomx-v2-phase0-design, 2026-05-16). RewritesRANDOMX_V2_RUST.md§15 from the open "rewrite or delete" question to an explicit delete decision with the rationale recorded (no users pre-genesis per60-no-monero-legacy.mdc; the feature shipped with essentially zero Monero production adoption; a future monetization story is better designed fresh against 2026+ options than inherited from 2020). Adds §15.4 with the concrete deletion checklist — fiverpc_payment*files pluswallet_rpc_payments.cppplus a functional test deleted whole, surgical hook removal acrosscore_rpc_server,bootstrap_daemon,node_rpc_proxy,wallet2,wallet_args,wallet_rpc_helpers,wallet_rpc_server, the daemon CLI command files,cryptonote_config.h, and the two CMakeLists — so Phase 4 inherits a checklist rather than a question. Tightens Phase 4 scope materially: the v2 verifier FFI export is consumed by daemon block verification only, with no wallet wiring.RANDOMX_V1_FALLBACK.md§2 records the deletion as algorithm-independent and inherits the same checklist under fallback. -
RandomX v2 Phase 0 — Round 1 review-feedback revisions (
feat/randomx-v2-phase0-design, 2026-05-16). ExpandsRANDOMX_V2_RUST.mdwith new sections §16 (genesis-block seedhash handling, including therx_seedheightearly-block branch and a canonical Rustseedheight()form), §17 (FFI error-code taxonomy with stable negative codes), §18 (thread- safety contract forshekyl_pow_randomx_v2_hash), §19 (block.major_versionfield disposition after PoW dispatch deletion), §20 (BSD-3-Clause licensing and attribution), §21 (MSRV pin proposal and#[no_mangle]/#[unsafe(no_mangle)]grep coverage), §22 (Guix reproducible-build forward-looking impact), and §23 (reviewer discipline under the project's solo-architect reality). Tightens §3 with test-vector provenance rules (tests/vectors/spec/vstests/vectors/reference/), §8 with the synthetic pre-genesis 600k-block release-gate harness, and §15 with a checked-in grep result narrowing wallet-tree PoW touchpoints towallet_rpc_payments.cpp:156/158/163.RANDOMX_V1_FALLBACK.md§2 records the upstream-tevador/RandomX-vs-Shekyl-fork v1 source choice and theBUILD_RANDOMX_V2_MINER_LIBrename, §4 fixes the cross-reference toRANDOMX_V2_RUST.md§1, §6 corrects the same cross-reference, and §7 mirrors the reviewer-discipline section. -
Stage 1 PR 3 — close-out:
engine_trait_bench_key_account_public_addresspair (chore/stage-1-pr3-closeout, 2026-05-12). Introduces the criterion + iai-callgrind sibling pair for theKeyEngine::account_public_addresstrait method, classified under theengine_trait_bench_*threshold class viacompare.py'sclassify()function-name routing. The fixture isBox<LocalKeys>rather than the canonical(Box<Engine<...>>, TempDir)shape per the substrate-forced divergence documented inSTAGE_1_PR_3_CLOSEOUT_PREFLIGHT.md§1.2 —Enginedoes not yet hold aLocalKeysfield; orchestrator integration isKeyEnginePR-5 territory perSTAGE_1_PR_3_KEY_ENGINE.md§2.1.1 (Round 4a workflow-shape pivot). Workload class is trivial pure-read (cachedAccountPublicAddressborrow); iai-callgrind is the load-bearing signal because criterionmedian_nsreflects optimizer amortization across the iteration loop (§4.4 hoisting caveat). The bench-internals visibility expansion adds onlyLocalKeysto thepubsurface (following the exact precedent ofLocalLedgerat Stage 1 PR 2 — name-only expansion; fields stay private).LocalKeys::from_test_seedbecomespubunder#[cfg(any(test, feature = "bench-internals"))]matchingLocalLedger::populate_for_bench's gating.AccountPublicAddressstayspub(crate)— the bench helper returns a primitiveusizesummary (sum of address-field byte-lengths) rather than the natural&AccountPublicAddressreturn type, sidestepping the API-widening Copilot's PR review flagged. Closes two of four deferred-bench slots fromFOLLOWUPS.md's Stage-1-performance-baseline entry (ledger_balancepreviously satisfied at Stage 1 PR 2,key_account_public_addresshere); two EconomicsEngine slots remain pinned to the EconomicsEngine trait-introducing PR.
Changed
-
Stage 1 PR 4 — Round 4 review pass meta-review amendment (review of F1–F9 disposition substrate; three additional findings F11–F13 dispositioned without reopening Round 1–4) (
feat/stage-1-pr4-round-4, 2026-05-15). Doc-only meta-review of the F1–F9 disposition substrate itself, asking "do the dispositions create new attack surface or leave under-specifications that would surface at Phase 1 commit-authoring as substrate decisions?" Three additional findings emerged, each targeting an under- specification introduced by an F1–F9 disposition rather than a substrate decision Rounds 1–4 settled; none reopens a Round 1–4 disposition; the F1–F9 dispositions remain unchanged. F11 (per-transaction cancellation safe-point pin; meta-review of F2). F2's five-checkpoint discipline pinned that a per-transaction cancellation check fires but did not pin where in the per-transaction body. F11 pins the check fires between transactions, after the prior iteration'sZeroizing<…>-wrapped per-output materials have left scope, before the next transaction's view-tag / hybrid-decap / key-image derivation begins (forbidding mid-derivation firing that would defeat F2's lock-latency property by exposing partial-derivation state on the unwound stack to memory-disclosure adversaries). C7'sAssertionSink/ coherence-pair test substrate gains a safe-point fixture asserting no partial- derivation state at the observed cancellation point. F12 (cross-emitter ordering contract-gap; meta-review of F4). F4's seventh contract pin (per-emitter FIFO preserved; cross-emitter ordering undefined) is enforced procedurally; consumer-actor authors who depend on cross-emitter arrival order produce code that compiles cleanly, passes per-emitter FIFO tests, and silently misbehaves under reordering at audit. F12 closes the gap at the discipline level (V3.0: §5.4.6 amendment binding consumer actors to derive cross-emitter ordering from causal-context fields likeSnapshotId,ReservationId- version,
BlockHeight) and at the lint level (V3.1+: scope-extending the FOLLOWUPS F5 entry to a unifieddiagnostic_consumer_disciplinelint covering both recursive-trust-boundary and cross-emitter-ordering misuse sub-scopes). PR 5 §5.0.3 carries the parallel amendment. F13 (SuppressedRateLimitfield-shape pin; meta-review of F6). F6 added theSuppressedRateLimitvariant without pinning its field shape; counts, timestamps, and original-event payloads are each attacker-relevant signal (counts are a covert channel back from the producer's internal state; timestamps add scheduling side-channels; payloads defeat the projection-type discipline). F13 pins the variant carriesclass: SuppressedClassonly, whereSuppressedClassis a project-defined#[non_exhaustive]enum at the same crate-root scope with arms one-per-rate-limited event class; consumer actors derive the suppression count from absence-of- further-events within the attempt boundary. C2'sSuppressedClassenum addition lifts the flat-crate-root re-export list from eight items to nine. The implementation-branch authorization continues to hold; the meta-review amendment shapes Phase 1's substrate without reopening it or extending its scope. The meta-review pattern itself is recorded as a forward- template under16-architectural-inheritance.mdc's "audits-are-clean-so-compress" anti-pattern framing: clean F1–F9 dispositions invite declaring victory; the discipline asks whether the dispositions themselves carry the property they claim before implementation cuts against them.
Post-amendment sub-pins (F11-S, F12-S, F13-S, 2026-05-15). A third-pass review of the F11–F13 dispositions themselves surfaced three Phase-1-author-aware sub-pins. Each sharpens the corresponding F-finding's disposition without reopening it; none reopens a Round 1–4 substrate decision; none reopens an F1–F9 disposition. The recursive structure (review pass → meta-review → post-amendment) is the closure rule's reopening mechanism operating at each level of the substrate hierarchy. F13-S (
SuppressedRateLimitemission-cadence sub-pin; the substantive one). F13's field-shape pin (carries class only) closed the payload covert channel but left the emission-cadence covert channel open: if the producer emits one notice per suppression-fire, an attacker reconstructs suppression frequency by counting notice arrivals in their own emit-arrival timeline regardless of payload shape. F13-S pins emission cadence at "at most oneSuppressedRateLimit { class }per class per attempt" — the producer's per-attemptemit_statecarries a per-classnotice_emitted: boollatch, cleared at attempt start, never cleared mid-attempt; subsequent in-class budget exceedances drop events but do not emit further notices. Cross-attempt cadence (attacker forcing many attempts viaConcurrentMutation-driven retries) is bounded at the orchestrator's existing retry-loop policy layer; no producer-side state survives across attempts (the zeroization scope forViewMaterialandScannerforecloses producer-side cross-attempt state). F11-S (per-output safe-point escalation criterion). F11's per-transaction safe-point closes the mid-derivation residency window for typical transactions but may not hold under hostile transactions carrying many outputs (FCMP++ permits some upper bound; the §3.1 lock-latency property's content-independence becomes content-dependent ifrecover_outputs_in_tx's per-output cost grows linearly with output count above the lock-latency target). F11-S pins the escalation criterion: Phase 1 commit-author verifies against benchmarked cost on reference hardware and against the protocol-parameter upper bound on outputs per transaction; if worst-case per-tx scan time exceeds the §3.1 lock-latency target, the safe-point escalates to per-output granularity (check between consecutive per-output decap iterations). The C4 commit message records the measurement and the chosen granularity. F12-S (diagnostic_consumer_disciplinelint conceptual unification). F12's unification is at the contract level (one named discipline, two related properties); the implementation strategy follows each property's nature (F5 sub-scope likely as a compile-time trait-bound orclippylint over consumer constructors; F12 sub-scope likely as an AST-level pattern-match over event-handler bodies). F12-S pins the conceptual-not-monolithic clarification in the FOLLOWUPS entry, foreclosing a future "the lint doesn't exist as a single pass" finding from invalidating a multi-check implementation that delivers the unified discipline. The post-amendment pattern compounds the closure-rule discipline (PR 5 §7): each level closes the wargaming surface known at its own closure time; reopening is explicit at the level of the surface that surfaced. The implementation-branch authorization continues to hold; the sub-pins shape Phase 1's substrate without reopening it or extending its scope. - version,
-
Stage 1 PR 4 — Round 4 review pass (adversarial review of post-Round-4 substrate; nine findings dispositioned) (
feat/stage-1-pr4-round-4-review, 2026-05-15). Doc-only pre-implementation adversarial review of the post-Round-4 substrate before Phase 1 cuts. Two reviewers exercised the diagnostic-stream seam, the encrypted-persistence opt-in language at PR 4 §5.4.8 #1, and the resilience surface from a hostile-daemon perspective; the pass produced nine actionable findings, all dispositioned and applied as substrate hardening rather than reopening any Round 1–4 question. Full writeup at PR 4 §5.4.9. Findings cluster across three threat-model surfaces. Feature-soft-commitment hardening (F1, F7). F1 rewrites the §5.4.8 #1 R17 encrypted-persistence opt-in language from "V3.x evaluates" to a hard rejection at V3.0 with strict conditional reopening criteria (six attack vectors named: crypto code-path expansion, deserialization-on-startup, metadata side-channel, cross-wallet correlation, adversary-controlled DoS, forensic-artifact); F7 adds a parallel new §5.4.8 #6 rejecting "encrypted cache for RPC recovery" V3.x candidates at V3.0 under symmetric criteria. PR 5 §5.4 R17 carries the F1 hardening symmetrically; the FOLLOWUPSPersistenceConsumerActorentry is rewritten as a conditional-reopening bookmark with no version target. Checkpoint-discipline tightening (F2). §3.1 wallet-lock-latency property refines from "single-block scan time, typically tens of ms" to "per-transaction scan time, sub-block-bounded; millisecond- scale even under adversarial daemon block crafting"; §7 checkpoint discipline extends from four to five checkpoints with a per-transaction inner cancellation check inside the per-block scan loop (closing the adversarial-block-crafting / extended-spend-secret- residency vector). Diagnostic-stream contract pinning (F3, F4, F5, F6, F8, F9). F3 pinsAssertionSink/PanickingSinkas permanent CI regression coverage rather than one-shot landing tests; F4 adds a seventh contract pin at §5.4.6 (per-emitter FIFO ordering preserved; cross-emitter ordering undefined) — the same pin lands symmetrically in PR 5 §5.0.3; F5 strengthens §5.4.8 #4's aggregator-republisher recursive-leak framing with a V3.x forward-template (per-consumer external-surface audit, projection-or-rejection, future CI-lint enforcement) and gets a new V3.1+ FOLLOWUPS entry (consumer-actor-PR aggregator-republisher CI lint); F6 adds a producer-side per-class emission rate budget to §5.4.8 #5 (per-block ceilings per event class plus aRefreshDiagnostic::SuppressedRateLimitvariant); F8 adds a new §5.4.8 #7 acknowledging emit-timing variance as a microarchitectural side-channel residual with a Phase 1 implementation note for bounded-variance lock-free queues; F9 adds a §6 projection-type audit per event class with explicit V3.0 per-class projections forTracingDiagnosticSinkand gets a new V3.x FOLLOWUPS entry (diagnostic-stream spec-doc projection- type formalization). The §7.X commit decomposition absorbs the substrate hardening: C2 carries theSuppressedRateLimitvariant + per-class projections + 7th contract pin; C4 carries the per-transaction inner cancellation check + producer-side per-class emission rate budget enforcement; C7 carriesAssertionSink/PanickingSinkas permanent CI fixtures. The α-disposition still holds; all Round 1–4 dispositions still hold; the review pass hardens contract pins and attack-surface dispositions without reopening any design question. Implementation-branch authorization (per §6 Round 4 readiness gate) is unchanged; Phase 1 cuts against the hardened substrate. The review-pass shape is recorded as a forward-template artifact under16-architectural-inheritance.mdc's "discovery cadence" framing — substrate hardening ahead of implementation is reusable for PR 5+ pre-implementation substrate review. -
Stage 1 PR 4 — Round 4 close (commit decomposition + Phase 1 commit list) (
feat/stage-1-pr4-round-4, 2026-05-14). Single-commit doc-only Round 4 close onSTAGE_1_PR_4_REFRESH_ENGINE.mdper the PR 1 / PR 2 / PR 3 / PR 5 precedent. §4 Phase 0 candidates (0a–0e, with 0d struck) finalize as binding-pinned at the type-signature level; Round 4 audit confirmsDaemonOptwo-variant andProtocolErrorKindfive-variant refresh-reachable subset against the producer's actual call sites. §6 review checklist fills in following PR 5's shape (binding-check matrix againstV3_ENGINE_TRAIT_BOUNDARIES.md§2.3, test-substrate preservation list, call-site sweep audit, Round 4 readiness gate authorizing Phase 1 cut). §7 extends with the Round-4 retrospective + a new §7.X Phase 1 commit decomposition subsection — eight load-bearing-ordered commits (C0 doc-only spec amendment + C1 trait declaration +ViewMaterialtype; C2RefreshDiagnostic+DiagnosticSink+ Stage 1 sink impls; C3RefreshError::InternalInvariantViolationvariant addition; C4LocalRefreshaggregate + producer-body migration; C5Engineparameterization + retry-loop call-site migration +RpcErrorclassification; C6MockRefreshtest substrate +replace_refresh; C7 hybrid retry test +AssertionSink/PanickingSinkproperty tests; C8 docs + CHANGELOG). §8 closes out the five "Remaining for Round 4" items (each marked Round-4-deliverable or Phase-1-commit-target) and updates the round trajectory banner — all PR-4-internal design rounds are closed. Implementation branch (feat/stage-1-pr4-refresh-engine) is authorized to cut off the post-Round-4 dev tip per the §6 Round 4 readiness gate; no further design rounds open unless Phase 1 commit-authoring surfaces a structural finding (the closure rule perSTAGE_1_PR_5_PENDING_TX_ENGINE.md§7 governs reopening if it does). -
Stage 1 PR 4 — Round 3 confirmation (α confirmed by PR 5 Round 1's actor-mesh-framed disposition) (
feat/stage-1-pr4-round-3-confirmation, 2026-05-14). Single-commit doc-only Round 3 closure onSTAGE_1_PR_4_REFRESH_ENGINE.md. PR 5 Round 1's disposition under the actor-mesh framing (perSTAGE_1_PR_5_PENDING_TX_ENGINE.md§5.0 / §5.2 / §5.5) confirmed shape (1) — snapshot-ID pinning — with the reservation tracker holding monotone semantics under PR 4's α; PR 4 advances directly to Round 4 (commit decomposition- Phase 1 commit list). The
provisionally-load-bearing qualifier on Round 1's α
(per §5.3 / §5.4.7 R1 / §8) is closed; the re-evaluation gate
collapsed without firing. Four housekeeping items land alongside
the closure: (1) §3.1 acknowledges the V3.0 dual spend-material
holder state —
LocalRefresh/Scanner(PR 4 R4 (a), inheritance-asymmetry justification) andLocalSigner(PR 5 R11 (b), architectural-integrity-now justification), convergent to one holder via R4 (c) in V3.x; (2) §8 / FOLLOWUPS R4 (c) entry cross-references PR 5 R11 (b)'sSignertrait substrate as the V3.x migration target — the R4 (c) migration becomes "Scannerstops holding spend material; delegates key-image generation via the existingSignertrait" rather than designing the split from scratch, shrinking the V3.x cost to a producer-side shape change (no architectural change); (3) theREFRESH_DIAGNOSTIC_STREAM.md→DIAGNOSTIC_STREAM.mdrename housekeeping was already covered by PR 5 segment 2g — no PR 4 doc references remain to sweep (confirmed byrg); (4) §5.4.8 #1's drop-on-close-by-default rule is acknowledged as project-wide rather than refresh-specific per PR 5 R17's closure — V3.0 ships drop-on-close across all diagnostic streams; per-stream wallet-internal encrypted-persistence opt-in is a V3.x refinement evaluated at the diagnostic-stream spec doc. The discovery-cadence prediction in16-architectural-inheritance.mdc("PR 4 onward's audits are increasingly likely to be confirmations") holds at the Round 1 / Round 3 boundary on the load-bearing question; the Round 2 reframe and PR 5 R11 (b)'s reframe are the two structural-density events that surfaced inside this PR's design rounds.
- Phase 1 commit list). The
provisionally-load-bearing qualifier on Round 1's α
(per §5.3 / §5.4.7 R1 / §8) is closed; the re-evaluation gate
collapsed without firing. Four housekeeping items land alongside
the closure: (1) §3.1 acknowledges the V3.0 dual spend-material
holder state —
-
**Stage 1 PR 3 — close-out:
STAGE_1_PR_*design-doc past-tensing- plan-vs-state-divergence rules-queue input sharpening**
(
chore/stage-1-pr3-closeout, 2026-05-12). Three-commit close-out PR consolidating audit findings from PR #40 under the trinary rule-15 reading perSTAGE_1_PR_3_CLOSEOUT_PREFLIGHT.md:
- A1 commit (mechanical past-tensing sweep): reconciled
17-reference enumeration across
STAGE_0_HARNESS.md,STAGE_1_PR_1_DAEMON_ENGINE.md, andSTAGE_1_PR_2_LEDGER_ENGINE.mdto 13 in-scope references;PERFORMANCE_BASELINE.md's four references were deferred to the A2 commit which rewrites those sections wholesale. Mode-2 closing-out residue under the trinary rule-15 reading, swept inline rather than deferred. - B1+B2 + lemma commit (rules-queue input sharpening for V3.1):
extends
FOLLOWUPS.md§19 (plan-vs-state-divergence) with the commit-history-level fourth-precedent instance (PR #40's 4-vs-6-vs-8 commit divergence between planned logical units, pre-review commit count, and final merged commit count). Extends the rule-15 trinary entry with PR #40's applied-disposition table (eight dispositions across two review-response cycles, classified by mode 1/2/3). Adds a new V3.1 entry — "Rules-queue: encode the pre-flight-FOLLOWUP-scope discipline" — generalizing the recurrence that FOLLOWUP items naming target PRs as resolution points orphan when target pre-flights don't claim them; cites L353-379 KeyEngine slot's M-series-wide skip as precedent. - A2 commit (KeyEngine bench introduction): see the "Added" section above for full detail.
- C1-C3 audit verifications (recorded in PR description):
TransferDetailsfield removal structurally complete; M3-series naming sweep complete (preserved-as-history or false-positive);42-serialization-policy.mdcstale globs closed in M3e. Three clean-as-found invariants from PR #40's audit pass.
- plan-vs-state-divergence rules-queue input sharpening**
(
-
Stage 1 PR 3 — M3e: documentation realignment to post-M3d architecture (
feat/stage-1-pr3-m3e; six commits cut offdevpost-M3d, landing the four logical units planned at amendment-cycle time perSTAGE_1_PR_3_M3E_PREFLIGHT.md§4: the "preflight + review-response + amendment" logical unit landed across three actual commits — original preflight at82693bab7, forward-templates capture at4b931b1b5, amendment at1f9a7ad59— followed by three substantive commits at8e6780062/582c19caf/c61f0d38f. The plan-vs-state divergence between the four-logical-unit framing and the six-actual- commit landing is recorded inline in §4 of the preflight as an instance of the §19 plan-vs-state-divergence pattern at the commit- history level). Closes the M3-series migration ofTransferDetailsperdocs/completed/STAGE_1_PR_3_MIGRATION_PLAN.md§3.5 (M3e — documentation realignment-of-the-whole) anddocs/completed/STAGE_1_PR_3_M3E_PREFLIGHT.md§6 (Success criteria). The M3-series (M3a–M3e) is complete; the "secrets confined to engine" property activated by M3d is now reflected throughout the design-doc and rules-corpus surfaces.-
Design-doc realignment.
KEY_ENGINE.mdcarries a post-migration status banner and past-tensed forward-looking framing in §1.1, §1.2, §5.2; open questions in §7 are annotated per-question with[Closed at M3<X>; see <ref>]or[Remains open / Forward-looking record]while preserving original framing as historical record.V3_ENGINE_TRAIT_BOUNDARIES.mdreplaces the pre-migrationKeyEnginetrait block with the post-M3 4-method shape (account_public_address,derive_subaddress,try_claim_output,sign_transactionperrust/shekyl-engine-core/src/engine/traits/key.rs:616), refactors the per-method classification table and retry-safety enumeration, and updates 13 scattered narrative references tosign_with_spend→sign_transactionwhile preserving the "Round 2 dispositions" section's original Q9.1/Q9.2/Q9.3 framings as historical record.MIGRATION_AUDIT.mdgains a post-M3 status banner clarifying that the audit's commit hashes (ffcaa62e9ande6efaf5b5) are immutable historical anchors and are not to be refreshed to post-M3d state. The discrepancy between the M3e preflight's D2 count (claimed "0 references to oldKeyEnginemethod names outside the trait block") and the actual surface (17 references found) is documented in the commit message; the surface was classified as mode-2 mechanical-residue under the rule-15 trinary reading (perSTAGE_1_PR_3_M3E_PREFLIGHT.md§11.1) and swept inline rather than deferred. -
Rules realignment.
.cursor/rules/42-serialization-policy.mdcunderwent a mechanical rename of all 11 stale crate-path references (shekyl-wallet-state→shekyl-engine-state;shekyl-wallet-file→shekyl-engine-file) across theglobsfrontmatter, intro paragraph, pairing table, mechanical enforcement subsections, and procedure section. The staleglobsfield previously prevented the rule from auto-applying to any file under the workspace's renamed crate trees; the realignment restores the auto-application surface. Closes the M3d-surfaced rule-realignment FOLLOWUP (relocated to the "Recently resolved (audit trail)" section inFOLLOWUPS.md). -
FOLLOWUPS structuring.
FOLLOWUPS.mdgains a "Queue structure" preamble that splits the queue into V3.0 pre-genesis (load-bearing; per-PR overhead compounds the pre-genesis trajectory) and V3.1+ post-genesis (sustainable backlog) queues. The Stage 2KeyEngine-actor entry is updated to reflect the post-M3 trait surface. Two new V3.1 rules-queue entries are added: "Encode the rule-15 trinary reading in15-deletion-and-debt.mdc" (codifying the M3e §11.1 calibration shift that distinguishes in-scope mechanical- residue from out-of-scope structural-tangent) and "Consolidate the rules-queue itself into 1–2 PRs" (pinning the consolidation target from M3e §11.3 against the current six-deep rules-queue accumulation). -
Path-rename residue sweep. The 34-occurrence path-rename residue surfaced by the M3d → M3e D5 audit (path-rename surface across 12 files) was swept inline per the rule-15 trinary-reading calibration shift. Per-category disposition:
- Active narrative documents updated (current-state
references, no append-only constraint): 8 adversarial test
fixture markdown files + their README; 2 crate-internal
READMEs (
shekyl-engine-state/fuzz,shekyl-scanner); 3 benchmark prose documents (benchmarks/README.md,shekyl_rust_v0.manifest.md,wallet2_baseline_v0.manifest.md); theV3_WALLET_DECISION_LOG.mdintro paragraph only (dated entries preserved per append-only discipline); and theWALLET_REWRITE_PLAN.mdcurrent-state architecture descriptions (Mermaid diagrams, inventory section, gap section, locked-design section, code-block comment, narrative paragraphs at §3.2 and Phase 1 audit; PR-0.X sections preserved as historical PR descriptions). - References preserved as historical anchors (49
occurrences across 6 files):
CHANGELOG.md(6 occurrences: rename-event entries plus this M3d entry's historical reference to the pre-realignment rule state, which this M3e entry now closes);FOLLOWUPS.md(8 occurrences across historical audit-trail entries plus the new M3e entries that reference the rename event);V3_WALLET_DECISION_LOG.md(16 occurrences across dated decision-log entries protected by the file's append-only discipline);WALLET_REWRITE_PLAN.md(6 occurrences inside PR-0.X historical descriptions);shekyl_rust_v0.json(10 occurrences; captured baseline pinned togit_revanchora2bf417e4b7985ed2097dc5d3fb53affef306d1a); andshekyl_rust_v0.iai.snapshot(3 occurrences; historical iai-callgrind capture). Refreshing these would falsify their respective historical anchors.
The trinary-reading calibration anchors the sweep: substrate- change mechanical-residue (the rename was the substrate change; the path references inside active documents are its residue) folds into the closing PR; historical anchors and append-only entries are preserved by construction. The discriminating tests (derivability + boundedness + traceability
- surface-during-review) are satisfied by the sweep's
discoverability via single
rginvocation and by surface during the M3e preflight's D5 audit.
- Active narrative documents updated (current-state
references, no append-only constraint): 8 adversarial test
fixture markdown files + their README; 2 crate-internal
READMEs (
-
Removed
-
Stage 1 PR 3 — M3d: legacy secret-bearing fields removed from
TransferDetails(feat/stage-1-pr3-m3d; one pre-flight commit + one pre-flight-review-amendment commit + four implementation commits cut offdevpost-M3c). Activates the "secrets confined to engine" property for the orchestrator/engine boundary perdocs/completed/STAGE_1_PR_3_MIGRATION_PLAN.md§3.4 (and §3.4.1's M3d landing-notes cross-reference),docs/completed/STAGE_1_PR_3_M3D_PREFLIGHT.md§3.3, and the audit migration table atdocs/completed/STAGE_1_PR_3_MIGRATION_AUDIT.md§2.1 row 1 (now marked "Removed at M3d (landed 2026-05-11)").-
Schema change: five
Option<Zeroizing<[u8; N]>>fields deleted fromshekyl_engine_state::TransferDetails:combined_shared_secret(64 bytes),ho,y,z,k_amount(32 bytes each). Corresponding entries in theTransferDetailsSchemamirror struct, theimpl Zeroize for TransferDetailsblock, and therust/shekyl-engine-state/.zeroize-allowlistschema-mirror entries were removed in the same commit. -
Version bumps (paired per the in-source rule at
rust/shekyl-engine-state/src/wallet_ledger.rs:67):LEDGER_BLOCK_VERSION: 3 → 4;WALLET_LEDGER_FORMAT_VERSION: 3 → 4. Thewallet_ledger.rsdocstring is the authoritative in-source statement of the pairing rule ("Each per-block bump implies aWALLET_LEDGER_FORMAT_VERSIONbump") — the workspace-wide rule.cursor/rules/42-serialization-policy.mdcstill carries pre-renameshekyl-wallet-state/shekyl-wallet-filepath references (tracked as a focused FOLLOWUP for path-rename realignment). Per the workspace's15-deletion-and-debt.mdc"no in-Shekyl migration code" rule, v4 stores refuse v3 loads rather than migrate; pre-genesis usersrm -rf ~/.shekyland re-sync. -
Property activated: orchestrator-side
TransferDetailsno longer carries derived per-output secrets. The engine re-derives them inside its signing-session boundary from(view_secret, source_ciphertext)viaLocalKeys::derive_source_secrets_bundle(perSTAGE_1_PR_3_KEY_ENGINE.md§7.10–§7.12) and wipes them on drop. Orchestrator memory disclosure no longer exposes output-secret material; capability disclosure is unchanged (per Round 3 §7.10 / §7.11 framing). -
Snapshot regeneration: the two
.snapfiles that transitively serializeTransferDetails(schemas/ledger_block.snap,schemas/wallet_ledger.snap) drift; the three others (bookkeeping_block.snap,tx_meta_block.snap,sync_state_block.snap) are unchanged, confirming pre-flight invariant 8 (snapshot universe verification). -
Production write-site removed: the five
td.<field> = Some(Zeroizing::new(...))write lines atshekyl-scanner/src/ledger_ext.rs::process_scanned_outputsdeleted; the M3b deterministic-handle pathway (source_ciphertext,output_handlepopulated byengine::merge::populate_engine_handle_fields) is the only write path post-M3d. -
Test/bench fixture rewrites:
shekyl-engine-state(transfer.rs::tests,ledger_block.rs::tests,ledger_indexes.rs::tests,invariants.rs::tests, fourbenches/*.rs),shekyl-scanner::balance.rs::tests, and the engine-core bench fixtures (benches/common/engine_fixture.rs,benches/refresh_snapshot.rs) updated to the post-M3d shape. Where the prior fixtures populated the five legacy secrets, the replacement populatessource_ciphertext(via directHybridCiphertextconstruction) andoutput_handle(viashekyl_crypto_pq::handle::derive_output_handle) soOption-valued roundtrip / snapshot benches continue exercising non-default payloads representative of post-M3d transfers. Thepostcard_roundtrip_with_secretstest was renamed topostcard_roundtrip_with_handle_fields. -
Documentation cleanup (carve-out per
91-documentation-after-plans.mdc): the past-tensing edits toSTAGE_1_PR_3_KEY_ENGINE.md§3.5 ("residue of that direct port" paragraph),STAGE_1_PR_3_MIGRATION_AUDIT.md§2.1 row 1 (five legacy-field disposition column), anddocs/benchmarks/shekyl_rust_v0.manifest.md(the two §3 paragraphs referencing the five legacy fields) landed in M3d's final docs commit alongside the plan §3.4 amendment. The broader M3e doc sweep remains scope-bounded to whole-doc realignment. -
Commit decomposition (five commits, matching pre-flight's planned count but with a different load distribution): the per-commit-CI-green gate forced consolidation of pre-flight commits 1 + 3 plus the scanner's
from_wallet_outputcleanup into a single cross-crate schema-migration commit (commit 2); a fifth slot was reused for a small bench-fixture-fix commit (commit 4) feature-gating twoshekyl_crypto_pqimports underbench-internalsin the engine-core common bench fixture aftercargo clippy --all-targetssurfaced them as unused when included from the default-featuresynced_heightbench pair. See plan §3.4.1 for the forward-template framing (pre-flight wording may strengthen during implementation if the underlying property is preserved).
-
Added
-
Stage 1 PR 3 — M3c: additive end-to-end engine-bundle signing test (
feat/stage-1-pr3-m3c; one pre-flight commit + two implementation commits + one cross-reference commit cut offdevatea1df2539). Lands the validation milestone perdocs/completed/STAGE_1_PR_3_MIGRATION_PLAN.md§3.3 (with §3.3.1 cross-reference to the implementation disposition) and the pre-flight indocs/completed/STAGE_1_PR_3_M3C_PREFLIGHT.md§2.1 (Option C disposition; §2.1.1 Trim-1 amendment). Property delivery: complete for the bundle → SpendInput → SignedProofs cryptographic chain at thetx_builder::sign_transactionsurface — the precondition M3d depends on for removing the legacyTransferDetails-secret-bearing-fields fallback.- New unit test:
engine_derived_bundle_signs_through_tx_builder_end_to_end, inline inrust/shekyl-engine-core/src/engine/local_keys.rs'smod testsas a peer to M3b D5. Constructs aLocalKeysfromTEST_SEED; for each of 9 fixtures (3 input counts {1, 2, 3} × 3 subaddress indices {PRIMARY,SubaddressIndex::new(1),SubaddressIndex::new(42)} —SubaddressIndexis a flatu32, not a(major, minor)pair) synthesizes n_in outputs paid tosubaddress_keys(idx)for every idx (including PRIMARY — see the test docstring's relationship-to-M3b-D5 section for why bare primary spend keys cannot recover here); recovers each output viascan_output_recoverto compose a hand-derived legacy bundle; derives the engine bundle viaLocalKeys::derive_source_secrets_bundle; asserts engine and legacySpendInputs are byte-identical field-by-field at the input layer (12 fields per input including per-leaf_chunk- entry equality); callstx_builder::sign_transaction(...)once on the engine path; asserts BP+ deserializes viaBulletproof::read_plusand verifies viaBulletproof::verifyagainst un-cofactored output commitment points; asserts FCMP++ verifies viashekyl_fcmp::proof::verifyagainst engine-derived key images, the proof's pseudo-outputs, the synthetic h_pqc Selene scalars, the synthetic single-leaf-chunk tree root, and the samesignable_tx_hashpassed to the prover; assertsreference_blockandtree_depthecho unchanged. - Inline cryptographic tree-fixture helpers. Replicates the
recipe from
shekyl-fcmp::proof::tests::prove_verify_roundtripinline inlocal_keys.rs::tests(single-leaf chunk; depth = 1;tree_root = SELENE_HASH_INIT + multiexp_vartimeover Selene generators × leaf scalars; h_pqc derived deterministically viadalek_ff_group::FieldElement::wide_reducefor reproducibility; recipientoutput_indexoffset byn_in + 100to avoid the input/output commitment-mask collision that collapses FCMP++'s rerandomization scalar to zero in single-input/single-output sweeps with sharedcombined_ss). Helpers:build_synthetic_single_chunk_tree_root,make_synthetic_h_pqc_bytes,make_recipient_output_info,compute_test_key_image. New[dev-dependencies]onshekyl-tx-builder,shekyl-fcmp,shekyl-bulletproofs,shekyl-fcmp-plus-plus,shekyl-generators,shekyl-io,shekyl-primitives,multiexp,ec-divisors,ciphersuite,helioselene,dalek-ff-group,rand_coreper17-dependency-discipline.mdc. - Layered framing. The test docstring records three layers:
Layer 1 — cryptographic chain
bundle → SpendInput → tx_builder::sign_transaction → BP+ verify + FCMP++ verify(this test's scope); Layer 2 —KeyEngine::sign_transactiontrait method (PR-5+ scope; today returnsKeyEngineError::SignTransactionTraitSurfaceIncompletebecauseTxToSign'soutputsandfcmp_plus_plus_contextare PR-5-pinned forward-declared stubs); Layer 3 — orchestrator- engine message envelope / actor mailbox (PR-5+ scope; cryptographic chain in Layer 1 is invariant under that decision). The test docstring also records the relationship to M3b D5 as intentional layered coverage (M3b D5 pins bundle- byte identity without exercising recovery; M3c-via-C pins recovery-correctness which forces the recipient subaddress consistency M3b D5 doesn't enforce — the two pin complementary properties at adjacent layers). - Trim-1 disposition (post-implementation amendment). An
earlier draft issued a parallel sign call with legacy-derived
SpendInputs forcommitments/enc_amountsbyte-equality at the signer-output layer. Pre-flight review surfaced thatSpendInputbyte-equality at the input layer is strictly stronger (subsumes the original property by signer determinism, and additionally guards regressions inSpendInputfields irrelevant to commitments / enc_amounts but relevant to signature behavior or future field additions). Substituting the parallel sign call for input-layer byte- equality + sign-once on the engine path halves the test runtime (32s → 17.65s debug; 12s → 6.87s release). Pre-flight §2.1.1 records the discovery and names it as a forward template: implementation may strengthen pre-flight properties post-implementation; weakening requires explicit revisit. The named coverage gap (workspace sole-coverage oftx_builder::sign_transactionend-to-end success goes from 2× to 1×) is named-and-accepted given M3d removes the legacy bundle-derivation chain entirely; the engine path is the load-bearing path going forward and the redundant second exercise of the same signer would only have decaying value. Workspace-coverage note: pre-PR-3 this end-to-end success path had 0× coverage anywhere (shekyl-tx-builder/src/tests.rsonly validation-error paths;transfer_e2e[_iai].rsbenches explicitly elide full sign pending a checked-in tree-fixture;shekyl-fcmp::proof::tests::prove_verify_roundtripexercises FCMP++ in isolation only; FFI / engine-rpc are production callers without in-file tests; BP+ fuzz target only fuzzes BP+ in isolation). Post-Trim-1 the test is the workspace's sole end-to-end successful-execution coverage oftx_builder::sign_transaction. - Migration plan + FOLLOWUPS updates.
STAGE_1_PR_3_MIGRATION_PLAN.md§3.3.1 records the Option C disposition + Trim-1 amendment so a reader of the original §3.3 wording reaches the implementation-side disposition in one hop.docs/FOLLOWUPS.md's M3b-D5 re-location entry is refactored to cover both M3b D5 and M3c-via-C under the sameKeyEngine-widens-to-pubtrigger (one re-location PR bundles both tests; the visibility flip is the trigger for both, and bundling them keeps the migration-tail discipline cost bounded).
- New unit test:
-
Stage 1 PR 3 — M3b: scanner reroute + bridge source switch (
feat/stage-1-pr3-m3b; ten substantive commits + one mechanical rustfmt fix + one docs commit cut offdevat647f82d59on 2026-05-09). Lands theKeyEngine-mediated source-secrets derivation path perdocs/completed/STAGE_1_PR_3_MIGRATION_PLAN.md§3.2 and the pre-flight dispositions indocs/completed/STAGE_1_PR_3_M3B_PREFLIGHT.md§2 / §3 / §5. Property delivery: partial — every output the scanner ingests now carries a deterministicOutputHandleand theHybridCiphertextit was decapsulated from on itsTransferDetails; the legacy secret-bearingTransferDetailsfields remain populated transitionally to keep the bridge-impl fallback live until M3d removes them.- Two-layer derivation primitive split (D1).
shekyl_crypto_pq::output::recover_combined_ss(view_x25519_sk, ml_kem_dk, kem_ct_x25519, kem_ct_ml_kem) -> Result<SharedSecret, CryptoError>(Layer 1, transform-shaped, inshekyl-crypto-pq) extracts the X25519 + ML-KEM-768 + HKDF-SHA-512 re-decap chain fromscan_output_recover's prefix;LocalKeys::derive_source_secrets_bundle( source_ciphertext, output_index, subaddress_idx) -> Result<SourceSecretsBundle, KeyEngineError>(Layer 2, state-shaped, inshekyl-engine-core::engine::local_keys) composes Layer 1's output with the engine-ownedb(spend secret) andm_i(subaddress derivation scalar). Placement per18-type-placement.mdc: transform-shaped lives with its function; state-shaped lives with its owner. TransferDetailsschema extension (D3). TwoOption<…>fields —source_ciphertext: Option<HybridCiphertext>(the on-chain hybrid X25519 + ML-KEM-768 ciphertext the scanner detected) andoutput_handle: Option<OutputHandle>(the deterministic 16-byte handle from cSHAKE256 keyed by the view secret).Zeroizeimpl skips the new non-secret fields per35-secure-memory.mdc's redaction discipline. Both fields land behind anOptionso the bridge-impl fallback is feature-detected (presence ofsource_ciphertext↔ primary path;None↔ legacy field path).LEDGER_BLOCK_VERSIONandWALLET_LEDGER_FORMAT_VERSIONbumped 2 → 3; both schema snapshots regenerated. The new fields' wire stability is locked by extending thepostcardround-trip test;postcard-schema = "0.2"added as a direct dep onshekyl-crypto-pqper17-dependency-discipline.mdc(matches the existingshekyl-engine-statedirect-dep pin).TxInputSigningContextfield swap (D2). Dropssource_secrets: SourceSecretsBundle(the by-value secret carrier that contradicted the engine-confined-secrets property) in favor ofsource_ciphertext: HybridCiphertext+output_index: u64. The trait-surface input is now the public on-chain ciphertext; the engine derives the secrets internally viaLocalKeys::derive_source_secrets_bundle.Debugimpl simplified; redaction tests updated.- Engine post-pass at the orchestrator layer (Q2 δ disposition).
Engine::apply_scan_resultbecomes a three-step body inside oneLocalLedgerwrite guard:collect_detection_residue(pre-collects aHashMap<(tx_hash, internal_output_index), HybridCiphertext>from theScanResult's new transfers) →apply_scan_result_to_state(the existing sync bookkeeping merge, unchanged) →populate_engine_handle_fields(walks the freshly-mergedTransferDetailsand binds each to itssource_ciphertext+ deterministicoutput_handlefrom the residue map). Atomic against external readers — concurrent reads either see pre-merge or post-population, never an intermediate state. Idempotent. The sync helper is async-ready by design: M3b derives the handle directly viashekyl_crypto_pq::handle::derive_output_handle(a synchronous pure function that requires only(view_secret, tx_hash, output_index)); M3c+ wiresLocalKeysontoEngineand re-routes the helper throughKeyEngine::try_claim_output, at which point the helper signature becomesasync fnandEngine::apply_scan_resulttakes the corresponding.await. The two-step trajectory is intentional and pinned in the helper's doc-comment; M3b's architectural property (every output gets a deterministic handle) does not require the audit's "engine sole authority on handles" framing to activate, which lands at M3d. - Scanner residue plumbing.
RecoveredWalletOutputextended with four public on-chain residue fields (source_ciphertext: HybridCiphertext,view_tag: u8,enc_amount: [u8; 8],amount_tag: u8, all#[zeroize(skip)]per the type's redaction discipline) so the engine post-pass has the structured input it needs. The pre-flight estimated this commit as "~0–10 lines, may be no-op," but inspection revealedRecoveredWalletOutputwas discarding the on-chain residue at construction time. Reordered to land before the engine post-pass commit so each commit leaves the workspacecargo check-green; the layering is honest about producer (scanner) and consumer (engine). - Named failure mode (D6).
KeyEngineError::SourceCiphertextDecapsulationFailed(#[from] CryptoError)for re-decap rejection. The variant carries the innerCryptoErrorso audit logs distinguish whether the rejection was at the X25519 layer (LowOrderPoint), the ML-KEM-768 layer (DecapsulationFailed), or the input-shape layer (InvalidKeyMaterial); all three indicate the same operational class (corrupted or tampered persisted state) but name which step rejected the input. The expected operational case for this variant is none — re-decap runs only on outputs the wallet itself scanned and persisted; a failure implies storage corruption or malicious local actor. - Byte-identical-derivation property test (D5). Two unit
tests in
local_keys.rs::tests: (a)derive_source_secrets_bundle_byte_identical_against_legacy_chainasserts field-by-field byte-equality between the new Layer 2 chain (derive_source_secrets_bundle) and a hand-rolled bundle fromscan_output_recover'sRecoveredOutputacross 24 derivations (8 distinct (output_index, tx_hash) pairs × 3 subaddress indices — PRIMARY, idx=1, idx=42); (b)derive_source_secrets_bundle_diverges_across_distinct_seedsexercises cross-seed isolation. The second test's docstring pins a subtle property: ML-KEM-768 implements implicit rejection per FIPS 203, so a wrong-wallet decap succeeds with a junk bundle (the IND-CCA2 oracle defense); the isolation property is "junk bundle differs byte-for-byte," not "function refuses." Located alongside C6's smoke tests inlocal_keys.rs::testsrather than the pre-flight's plannedtests/byte_identical_derivation.rsintegration test, due to the M3a Round 4apub(crate)lock onLocalKeys,SourceSecretsBundle, andKeyEngineError; tracked for re-location indocs/FOLLOWUPS.md§ V3.2 if the visibility lock relaxes at the wallet-RPC cutover.
Property-delivery framing: structural — no consensus rule, no wire format on-chain, no FFI layout changes. The
TransferDetailsschema bumpsWALLET_LEDGER_FORMAT_VERSIONfrom 2 to 3, which is a wallet-state schema change handled by the pre-V3-launchrm -rf ~/.shekylmigration path per15-deletion-and-debt.mdc(no in-Shekyl format-detection code; pre-genesis users have no real state to preserve). M3c–M3e land the additive test caller (M3c), the legacy-fallback removal (M3d), and the audit closure (M3e). - Two-layer derivation primitive split (D1).
-
Stage 1 PR 3 — Phase 0:
AllKeysBlobzeroize-discipline realignment (chore/allkeysblob-zeroize-realignment; closesdocs/completed/STAGE_1_PR_3_KEY_ENGINE.md§3.5 (Phase 0e) and §7.5). Three rule-grounded edits that landed together as a focused chore PR before the M3b implementation, each closing an audit finding cited to a rule with a concrete failure mode prevented:- F1 /
35-secure-memory.mdc:21–22.AllKeysBlob.ml_kem_dk(the ML-KEM-768 decap secret key, 2400 bytes) was the lone unwrapped secret-bearing array on the struct; wrapped in a newMlKem768DecapKeytyped newtype inrust/shekyl-crypto-pq/src/keys.rsthat mirrors the establishedViewSecret/SpendSecretshape (#[repr(transparent)],Clone + Zeroize + ZeroizeOnDrop, noCopy, noDebug,pub(crate)constructor, publicas_canonical_bytes()accessor). Sweeps eight in-Rust read sites (account.rs's field/zeroed/rederive/test,local_keys.rs:344,refresh.rs:1283,account_ffi.rs:531); the FFI mirror keeps raw[u8; ML_KEM_768_DK_LEN]and the bit-for-bit layout invariant (size, alignment, per-field offsets) is preserved by#[repr(transparent)]and asserted directly byaccount_ffi::tests::struct_layout_matches. The producer [crate::account::ml_kem_keypair_from_d_z] returns the typedMlKem768DecapKeydirectly (constructed viafrom_zeroizingconsuming aZeroizing<[u8; N]>source) — the secret travels through the type system from producer to consumer without any call site materialising an untracked stackCopyof the 2400-byte buffer between them. - F2 /
35-secure-memory.mdc:23–25.AllKeysBlobmigrated from a hand-writtenDropimpl (which the design doc itself characterized as "documenting the lie" — the spec assertedAllKeysBlob: ZeroizeOnDropwhile the trait was not implemented) to#[derive(Zeroize, ZeroizeOnDrop)]. With every field nowZeroize-bearing (typed wrappers + zeroize-crate blanket impls on[u8; N]), the structural condition for the derive holds and the manual impl is replaced wholesale. The derivedDrop::dropcallsself.zeroize()once on every field; field-drop-glue then re-invokes eachZeroizeOnDropfield's destructor independently — an idempotent double-wipe documented in the struct rust-doc so futureZeroizeOnDrop-grep audits do not mistake the pattern for a discipline violation. - F3 /
KEY_ENGINE.md§7.5.AllKeysBlob: Clonederive deleted. Workspace audit (rg 'AllKeysBlob.*\.clone\(\)'+ per-call-site read;cargo build --workspace --all-targetsis the locking gate that compiles every#[cfg(test)]block) surfaced zero callers in production or test code; per30-cryptography.mdcand35-secure-memory.mdc:26-28,Cloneon a secret-bearing struct requires explicit justification, and none surfaced. Thetraits/key.rs:581doc-comment ("Not Clone — implementors wrapAllKeysBlob") becomes literally enforced.
ml_kem_ekdeliberately stays raw[u8; ML_KEM_768_EK_LEN]. Public encap key, broadcast in the address; outside35-secure-memory.mdc:21–22's reach as public material. Wrapping it would be uniformity-driven completionism without rule grounding (per15-deletion-and-debt.mdc's "while we're here is the enemy") and would create a permanent type-system signal collision (Zeroizesemantics on a public type as a distractor for any future grep-for-secrets audit). Five-reason disposition recorded inline atdocs/completed/STAGE_1_PR_3_KEY_ENGINE.md§3.5's "Closed (post-M3a, post-Phase-0)" subsection against re-litigation.Closure-path narrative. The originally-specified §3.5 sequencing was "Phase 0e lands first, before PR 3 cuts." The actual landing was post-M3a, via this chore. The deviation is substrate-change, not extension: §3.5 was specced when
AllKeysBlobcarried raw[u8; N]fields (wherederive(ZeroizeOnDrop)would have been a literal one-line addition). The interveningchore/allkeysblob-typed-wrappers-monero-sweep(which closed the inheritance audit'sspend_sk/view_sksecret-flow finding) leftml_kem_dkas the residual raw secret-bearing array, which prevented the parent derive from taking. This chore re-anchors §3.5's load-bearing goal (Q9.3 precondition true;AllKeysBlob: ZeroizeOnDropliterally implemented) to the post-sweep substrate; the work-shape adapted to the post-sweep state rather than extended from the original 5–10-line plan.Property-delivery framing: structural — no consensus rule, no wire format, no FFI layout changes. The deliverable is rule alignment between code and spec on the
AllKeysBlobzeroize discipline, restoring the precondition that Q9.3 / Phase 0d's cross-reference language now resolves cleanly against. M3b cuts off the post-mergedevtip with the precondition true. - F1 /
-
Single-source-of-truth JSON authority for the consensus-affecting constant subset:
config/consensus_constants.json. Mirrors the existingconfig/economics_params.jsonpattern. The JSON is the authority;cmake/generate_consensus_constants.pyemitsshekyl/consensus_constants_generated.hfor the C++ build, andrust/shekyl-engine-core/build.rsreads the same file and emits aconsensus_constants_generated.rsmodule thatrust/shekyl-engine-core/src/multisig/v31/intent.rsconsumes viainclude!(). Closes the C++/Rust drift class for the constants where drift causes silent wrong-output (vs. fail-closed-on-load).Constants in scope (per
docs/audit_trail/2026-05-ffi-constant-drift-audit.md):FCMP_REFERENCE_BLOCK_MIN_AGE = 5— reorg-safety margin locked by Decision 14. Pre-fix, hand-defined as5insrc/cryptonote_config.hand as10inrust/shekyl-engine-core/src/multisig/v31/intent.rs. The drift was Bug 3 of the audit and silently rejected legitimate multisig intents at the wallet layer.FCMP_REFERENCE_BLOCK_MAX_AGE = 100— same shape, no observed drift but in the same value class and migrated together.RCT_TYPE_FCMP_PLUS_PLUS_PQC = 7— single-source on each side today (enum RCTTypein C++;ProofType::FcmpPlusPlusPqc => 7inshekyl-oxide); both sides now stamped against the JSON viastatic_assert(C++ insrc/fcmp/rctTypes.cpp) and a runtime test (Rust, inintent.rs::tests::shekyl_oxide_proof_type_matches_consensus_authority).
Sentinel discipline: every consumption site that previously hand-defined a value now carries either a
static_assert(C++) or aconst _: () = assert!(...)(Rust) sentinel pinning the value to a Decision-14-era baseline. Bumping the sentinel requires updating both the JSON and the consumption-site comment, so a silent value drift through the JSON alone fails the build with a clear message.Fixture update:
intent.rs::tests::validate_temporal_rejects_ref_block_too_freshchanged fromtip = 905(age = 5, the boundary valueage < 5evaluates false under the post-fixMIN_AGE = 5) totip = 903(age = 3, unambiguously rejected). The test exercises the rejection branch (age < MIN_AGE) and stays correct as long asMIN_AGE > 3— i.e. it survives any tightening (MIN_AGEincreasing above 5) and any loosening down to and includingMIN_AGE = 4. Only a loosening toMIN_AGE = 3or lower would invalidate the fixture, which itself would warrant the consensus re-review the sentinel demands.Out of scope:
ADDRESS_VERSION_V1is single-source in Rust with no C++ duplicate, so there's nothing to align. The full-migration follow-up for the remainingSHEKYL_*fail-closed- on-misuse constants (~40) stays as FOLLOWUPS V3.0.
Documentation
-
Stage 1 PR 5 — address PR #43 Copilot review findings, Round 2 (post-Round-2-close follow-up cycle). Doc-only commit on
feat/stage-1-pr5-pending-tx-engine-design. Addresses nine additional Copilot review findings surfaced against the Round-2-close-out commit (b85edec9a), the first Copilot-fix commit (871efa40c), and the Round-1 CHANGELOG entries. The fixes consolidate hash-primitive dependency-discipline correctness, cryptographic-security- rationale framing, sink-binding shape alignment across segments, variant-name alignment in V3.x FOLLOWUPS entries, and architectural soundness of the V3.xTimeoutResolverActorcorrelation contract.-
Findings A + E + H —
SnapshotIdhash primitive correction (covers three Copilot comments on §4 Phase 0b, §5.4 R2 sketch, CHANGELOG segment-2g entry, §5.5 Round 2 summary, and the doc header). Segment-2g's prior binding pinnedSnapshotIdto SHA-256 viasha2 = "0.10", citingrust/shekyl-engine-core/Cargo.tomlline 115 as the workspace-state-reuse anchor. That citation was a dependency-discipline error:Cargo.tomlline 115 is in[dev-dependencies](test-only), and the productionsha2at line 33 isoptional = true(gated behind a feature flag). The Copilot-fix follow-up switches the primitive toshekyl-crypto-hash::cn_fast_hash(Keccak-256, original padding) —shekyl-crypto-hashis an unconditional[dependencies]entry per Cargo.toml line 28, the consensus-audited Keccak primitive Shekyl already uses throughout its codebase. Strictly better disposition against the17-dependency-discipline.mdcworkspace-state reuse rule against the actual production-dependency graph. The §5.4 R2 sketch also still showed a priorblake3::hashform from segment-2d's open-shape-not-primitive disposition; the sketch is updated to the bindingcn_fast_hashform.The security rationale is also reframed. Segment-2g's prior framing was "128-bit collision resistance gives ~2⁶⁴ classical work and ~2³² quantum work via Grover- doubled width." Two errors: (i) Grover's algorithm gives 2^(n/2) work against preimage attacks, not collision attacks — quantum collision is governed by BHT (Brassard–Høyer–Tapp), ~2^(n/3) ≈ 2⁴³ for 128-bit outputs; (ii) the use-case framing is incorrect —
SnapshotIdis a wallet-internal equality token over a bounded snapshot population, not a collision- resistance primitive against arbitrary inputs.Corrected framing: second-preimage resistance over bounded snapshot population. The wallet observes ≪ 2⁴⁰ snapshots over its operational lifetime (≤ ~10⁷ snapshots over 100 years; one snapshot per refresh merge). Classical second-preimage on 128-bit truncated hash is ~2¹²⁸ work; quantum Grover second- preimage is ~2⁶⁴ work — large but bounded under aggressive quantum-adversary assumptions. The impact bound under successful attack is also constrained: a daemon-forged colliding
LedgerSnapshotmerely makes the wallet submit a tx valid against the prior snapshot; the daemon could have rejected the tx anyway viaDoubleSpendif the prior snapshot's outputs are now spent on-chain. No consensus violation; no wallet- state corruption that refresh cannot reconcile. The versioned domain-separation prefix (b"shekyl-snapshot-id-v1") permits V3.x migration to a wider output or different hash family without cross- stage rebuild.Sites updated:
docs/design/STAGE_1_PR_5_PENDING_TX_ENGINE.md§4 Phase 0b binding, §5.4 R2 sketch + prose, §5.5 Round 2 summary, §6 review-checklistSnapshotIditem, and the header status block; this CHANGELOG segment-2g entry with a Copilot-fix forward-pointer. -
Finding B — §5.4 R2 cross-reference to rejected option (b). §5.4 R2 prose referenced
DIAGNOSTIC_STREAM_CONTRACTS.md(the parent-doc factoring option (b) considered in §5.0.3), but segment 2g's diagnostic-stream-doc generalization closed as option (a) — renameREFRESH_DIAGNOSTIC_STREAM.md→DIAGNOSTIC_STREAM.md. The cross-reference is updated to the chosen doc name with the closure rationale. -
Finding F —
&dyn DiagnosticSinkvsArc<dyn DiagnosticSink>inconsistency. §5.0.2.1 (the segment-2f sink-binding-closure section) used the earlier&dyn DiagnosticSinkform when the closure section itself pinsArc<dyn DiagnosticSink>; this is corrected for self-consistency. The Round-1-close CHANGELOG entry's&dyn DiagnosticSinkdescription receives a forward-pointer noting that segment 2f tightened the form toArc<dyn>for reference-shape ergonomics during R11 closure (the earlier wording remains historically accurate at Round 1 close). -
Findings C + D — V3.x FOLLOWUPS
SubmitFailureAnalyzervariant-name alignment. TheSubmitFailureAnalyzerFOLLOWUPS entry referencedSnapshotInvalidatedin two places andSubmitFailed { kind: Timeout }in one place; the binding variant names per segment 2f / Phase 0a / Phase 0f areSubmitSnapshotInvalidatedandSubmitFailed { kind: DaemonTimeout | DaemonUnavailable }respectively. All three sites updated; the timeout bullet is also expanded to cover both ambiguous-failure variants per segment-2f's daemon-side authority disposition (both carry the same operational signal for pattern-detection purposes). -
Finding G —
TimeoutResolverActorchain-observation correlation contract architectural mismatch. TheTimeoutResolverActorFOLLOWUPS entry described subscribing toLedgerDiagnostic::SnapshotMergedto observe whether the timed-outtx_hashlanded on chain — butSnapshotMergedis pinned by Phase 0g as{ new: SnapshotId, prior: SnapshotId, height: BlockHeight }and carries notx_hashfield, so the actor cannot implement the correlation from the stated event stream. The §5.4 R9 disposition prose carried the same mismatch. Disposition: soften both prose surfaces to defer the chain-observation mechanism to the V3.x consumer-actor PR's own design — the actor needs either (i) an additiveLedgerDiagnosticvariant carrying tx-confirmation payloads, or (ii) an additiveLedgerEnginechain-query accessor, or (iii) both (event-driven for low-latency notification, polling for restart-amnesia catch-up). Pinning the mechanism in PR 5 would overspecify a V3.x consumer-actor that doesn't ship in V3.0; theLedgerEngineandLedgerDiagnosticsurfaces have their own additive- extension discipline that the consumer-actor PR composes against. Wallet-correctness is preserved by R8'sReservationTTLActorsafety net regardless of whenTimeoutResolverActorlands. -
Finding I — PR #43 title + description scope correction. The PR title and body still framed PR #43 as a Round-1-only doc-only PR with three commits, but the branch now contains all seven Round 2 segments (segments 2a–2g) plus two Copilot-review follow-up commits. PR metadata updated to reflect the actual Round 1 + Round 2 closed scope, with the seven-segment summary and Phase 0 binding enumeration mirroring the design doc's §5.5 closure. The earlier "Round 1 only, Round 2 out of scope" wording is replaced.
Markdownlint baseline parity confirmed after edits (no new violations introduced). Round 2 remains closed; Round 3 (commit decomposition + Phase 1 commit list) is the next forward step pending user authorization.
-
-
Stage 1 PR 5 — address PR #43 Copilot review findings (Round 2 close-out follow-up). Doc-only commit on
feat/stage-1-pr5-pending-tx-engine-design. Addresses three Copilot review findings surfaced against the Round 2 segments and segment 2g close-out:- Finding 1 — §3.3 pre-flight checklist staleness
(raised against b85edec9a, line 609 of design doc;
re-raised on the same line). The pre-flight checklist at
§3.3 still marked R1 disposition / Phase 0 spec
amendments / PR 4 Round 3 input bundle as pending, even
though Round 1 closed those items (R1 in §5.5; Phase 0 in
segment 2g §4; PR 4 Round 3 bundle as confirmation per
§5.2 + §6). Fix: marked R1 / Phase 0 / PR 4 Round 3
items as
[x]with cross-references to the closure sections; Phase 1 commit decomposition remains[ ]pending Round 3. - Finding 2 — R8
ReservationTTLActorsubscription contract incomplete (raised against 2f177a0c3, line 987 of design doc). Segment 2e's R8 closure named onlyPendingTxDiagnostic::BuildSucceededas the actor's subscription, with no terminal events. This would leak closed reservations into the actor's in-memory age-tracking map indefinitely, producing staleReservationOutstandingwarnings on already-terminated reservations and spuriousAutoDiscardMessageround-trips toPendingTxActor. Fix: §5.4 R8 prose expanded with a full subscription contract section pinningBuildSucceeded(insert),SubmitSucceeded(remove — terminal success), andDiscarded(remove regardless ofreason— covers all fourDiscardReasonvariants including the segment-2fDaemonRejectedTerminaland the segment-2eTTLAutoDiscardself-cleanup). Explicit "whatSubmitFaileddoes not close" note per segment-2f R9's two-stage submit-flow + Finding-2 daemon-side authority disposition:SubmitFailedonDaemonTimeout/DaemonUnavailablekeeps the reservation inSubmitPendingDaemonAckand the actor keeps tracking. Memory-bound property pinned: actor's map size is bounded byPendingTxActor::outstanding(), not by cumulative reservation count. - Finding 3 — FOLLOWUPS
ReservationTTLActorentry has the same subscription gap (raised against 2f177a0c3, FOLLOWUPS line 3029). Identical finding to Finding 2, in the FOLLOWUPS entry rather than the design doc. Fix: same subscription-contract expansion applied to the FOLLOWUPS entry; cross-reference to the design-doc §5.4 R8 closure preserved. - Finding 4 — CHANGELOG Round 1 close entry residuals count predates R12 (raised against b85edec9a, CHANGELOG line 1449). The Round 1 close entry says "four carry to Round 2; one new (R11)"; R12 was added in a subsequent Round 1 follow-up commit (the immediately-following CHANGELOG entry). Fix: added a parenthetical forward-pointer to the Round 1 close entry noting R12's addition in the follow-up; preserves the entry's historical accuracy at commit time while resolving the in-isolation reader's apparent inconsistency. The follow-up entry's existing R12 documentation is unchanged.
No segment-2g substrate is revised; all four fixes are contract-clarification / status-update edits. Round 3 readiness gate per segment 2g §8 fenceposts is unaffected. Updates docs/design/STAGE_1_PR_5_PENDING_TX_ENGINE.md (§3.3 checklist; §5.4 R8 subscription-contract subsection); docs/FOLLOWUPS.md (
ReservationTTLActorentry subscription- contract subsection); docs/CHANGELOG.md (this entry + forward-pointer note on the Round 1 close entry). No code changes; no test impact. - Finding 1 — §3.3 pre-flight checklist staleness
(raised against b85edec9a, line 609 of design doc;
re-raised on the same line). The pre-flight checklist at
§3.3 still marked R1 disposition / Phase 0 spec
amendments / PR 4 Round 3 input bundle as pending, even
though Round 1 closed those items (R1 in §5.5; Phase 0 in
segment 2g §4; PR 4 Round 3 bundle as confirmation per
§5.2 + §6). Fix: marked R1 / Phase 0 / PR 4 Round 3
items as
-
Stage 1 PR 5 — Round 2 segment 2g (Round 2 close-out: §4 Phase 0 binding-form enumeration;
SnapshotIdhash primitive pin; §5.0.3 diagnostic-stream-doc generalization closure; §6 review checklist filled). Doc-only commit onfeat/stage-1-pr5-pending-tx-engine-design. Segment 2g closes Round 2 — the final segment that pins all Phase 0 binding-form type-signature detail, fills the review checklist, and finalizes the diagnostic-stream-doc generalization disposition. Round 3 (commit decomposition + Phase 1 commit list) is the next forward step. §4 Phase 0 binding-form enumeration finalized: Phase 0a (SubmitErrorandSubmitErrorKindenums per segment 2f); Phase 0b (SnapshotIdopaque type with binding hash primitive — see below); Phase 0c (REMOVED at the trait surface per segment 2d's R12 (a) closure); Phase 0d (Reservationstruct shape withextensions: Vec<ReservationExtension>per segment 2b R14); Phase 0e (reservation-lifecycle prose with R5 / R9 segment-2f / R10 closure cross-references); Phase 0f (PendingTxDiagnosticenum + constructor-boundDiagnosticSinkper segment-2f §5.0.2.1); Phase 0g (LedgerDiagnostic::SnapshotMergeddeferred to consumer-PR per segment-2g introduction-PR disposition — avoids speculative-introduction-without-consumer violation of the15-deletion-and-debt.mdcno-live-caller rule); four new Phase 0 candidates from segment-2b / segment-2c residual closures: Phase 0h (Signertrait surface per R11 (b) segment-2b closure); Phase 0i (OutputSelectortrait surface per R13 segment-2c closure); Phase 0j (FeeEstimatortrait surface +FeePriorityenum per R16 segment-2c closure with segment-2d V3.0-lift evaluation); Phase 0k (SubmissionStrategyActortopology slot per R15 segment-2c closure — V3.x introduction; no V3.0 trait amendment).SnapshotIdhash primitive pinned as Keccak-256 viashekyl-crypto-hash::cn_fast_hash(original padding, consensus-audited) truncated to the first 128 bits with versioned domain-separation prefix (b"shekyl-snapshot-id-v1"). (Forward-pointer: the Copilot-fix follow-up entry below revised this binding from segment-2g's priorsha2-based form to the Keccak-based form. The priorsha2citation referencedrust/shekyl-engine-core/Cargo.tomlline 115, which is in[dev-dependencies]and therefore unavailable to production code; the productionsha2at line 33 isoptional = true.shekyl-crypto-hashis the consensus-audited Keccak primitive already unconditional inshekyl-engine-coreproduction deps at line 28 — the strictly better dependency-discipline disposition.) Selection rationale (revised form):shekyl-crypto-hashis an unconditional[dependencies]entry per17-dependency-discipline.mdcworkspace-state reuse rule against the actual production-dependency graph; security framing reset from collision-resistance / Grover-doubled-width (technically incorrect — Grover applies to preimage, not collision; quantum collision is governed by BHT, ~2⁴³ for 128 bits) to second-preimage resistance over bounded snapshot population (wallet observes ≪ 2⁴⁰ snapshots over its operational lifetime; classical second-preimage ~2¹²⁸ work; quantum Grover second-preimage ~2⁶⁴ work; impact bound by adversary-controlled-daemon design-center per §5.3); versioned prefix permits V3.x migration to a wider output or different hash family without cross-stage rebuild becauseSnapshotIdis a wallet-internal token that does not cross the wire. §5.0.3 diagnostic-stream-doc generalization closure: option (a) — renameREFRESH_DIAGNOSTIC_STREAM.md→DIAGNOSTIC_STREAM.md(general). Existing FOLLOWUPS entry amended with rename rationale (shared contracts modest in volume relative to per-stream taxonomies; single doc with shared-then-per- stream structure lower cross-reference cost than parent-and-children factoring) and doc-structure prescription for V3.x introduction PR (shared contracts at top; per-stream sections forRefreshDiagnostic/PendingTxDiagnostic+DiscardReason/LedgerDiagnosticpending the consumer-actor PR). Option (b) — parentDIAGNOSTIC_STREAM_CONTRACTS.mdfactoring — preserved as retroactively-applicable if growth justifies. §6 review checklist filled: binding-check matrix against the §2.4 spec (trait surface methods unchanged; engine-type parameter additionsS: Signer,O: OutputSelector,F: FeeEstimator); test-substrate preservation list (AssertionSink/PanickingSinkproperty-test infrastructure inherited from PR 4 pattern; per-error-class R9 coverage; Finding-2 daemon-side authority coverage); call-site sweep audit enumeration (Phase 1 confirms every diagnostic-event emission site); PR 4 Round 3 input bundle resolved as confirmation per §5.2. Round 3 readiness gate: all §4 Phase 0 candidates binding-pinned; §6 filled; FOLLOWUPS amended for the segment-2g rename; Round 3 ready to proceed. Updates §4 Phase 0 enumeration (full rewrite with binding-form signatures for all candidates 0a–0k); §5.0.3 generalization-question section (closes as (a) rename); §5.5 "What Round 2 carried" inventory (seven-segment summary; Round 2 final form); §6 review checklist (filled with all sub-checklists); §8 fenceposts (segment 2g moves to "Round 2 — completed"; Round 3 named as next forward step); header status (Round 2 closed); CHANGELOG; FOLLOWUPS. No code changes; no test impact. -
Stage 1 PR 5 — Round 2 segment 2f (R9 two-stage submit-flow closure with daemon-side authority for Finding 2 ambiguous outcomes;
SubmitError+SubmitErrorKindenum pins; sink-binding constructor-bound closure for Finding 4). Doc-only commit onfeat/stage-1-pr5-pending-tx-engine-design. Segment 2f closes the last residual on the load-bearing submit path and the constructor-vs-per-method sink-binding question, leaving only Round 2 close-out work for segment 2g. R9 closure pins the two-stage submit flow with explicit internalReservationStatemachine (Active | SubmitPendingDaemonAck | Resolved); trait surface unchanged (outstanding()countsActive + SubmitPendingDaemonAck). Self-continuation message pattern pinned:PendingTxActordefers reply untilSubmitCompletedself-message arrives, preserving mailbox throughput. Per-error-class disposition table pins state-transition + diagnostic-event-sequence + trait-return tuples forAccepted/AlreadyInMempool/DoubleSpend/FeeTooLow/Malformed/Timeout/NetworkError. Finding 2 closes as (B) — daemon-side authority: onTimeoutorDaemonUnavailable, reservation stays inSubmitPendingDaemonAck; consumer-explicitdiscard(id, ConsumerExplicit)is the resolution path; R8'sReservationTTLActor(per-state TTL with shorter TTL onSubmitPendingDaemonAck) is the safety net for forgotten resolutions. (A) actor-state authority rejected because the phantom-spent-output window violates the monotonicity property the tracker delivers per §3.4.5 (the same "consumer checking does work the trait should be doing structurally" anti-pattern PR 4 named).SubmitError+SubmitErrorKindenums pinned in §5.0.2 (both#[non_exhaustive]):SubmitError = SnapshotInvalidated{..} | DaemonRejected{kind: SubmitErrorKind};SubmitErrorKind = DoubleSpend | FeeTooLow | Malformed | DaemonTimeout | DaemonUnavailable. R5 ↔ R8 ↔ R9 coherence verified — reactive cleanup (SnapshotRotationAutoDiscard), proactive cleanup (TTLAutoDiscard), and daemon-authority cleanup (DaemonRejectedTerminal) share theDiscardReason/Discardedevent infrastructure. No newPendingTxDiagnosticvariants needed (existing variant set sufficient for R9 state machine); no new trait surface methods needed (discard(id, ConsumerExplicit)is sufficient for consumer-explicit resolution of Finding-2 ambiguity;resolve_pending(id, chain_observation)preserved as a V3.x ergonomic-API candidate). Sink-binding closure (Finding 4): new §5.0.2.1 pinsLocalPendingTx::new(..., sink: Arc<dyn DiagnosticSink>, ...)as constructor-bound under PR 4 §3.4.5 / R4 (a) consistency. R11's segment-2b closure as (b) made the sink-binding question independent of spend-material disposition; the two close separately. Rationale: engine-identity coupling (1-to-1 mapping load- bearing at the type level); Stage 4 actor wiring alignment (spawn-time DI); call-site cleanliness; runtime-swap surface preserved via sink-side indirection; no load-bearing reason for per-method override in production engines. ExistingSubmitFailureAnalyzerFOLLOWUPS entry amended with segment-2f closure status; newTimeoutResolverActorFOLLOWUPS entry added naming the V3.x ergonomic-complement surface for Finding 2's daemon-side authority disposition. Updates §5.0.2 (SubmitError+SubmitErrorKindenum sketches); new §5.0.2.1 (sink-binding closure rationale); §5.4 R9 (closure prose with state-transition table); §5.5 "What Round 2 carries" inventory; §8 fenceposts (segment 2f moves to "Round 2 — completed"); header status; CHANGELOG; FOLLOWUPS. No code changes; no test impact. -
Stage 1 PR 5 — Round 2 segment 2e (R8
ReservationTTLActorcomposition closure;DiscardReason::TTLAutoDiscardvariant pin). Doc-only commit onfeat/stage-1-pr5-pending-tx-engine-design. Segment 2e closes R8 (reservation TTL / leak prevention) by pinning all V3.0 deliverables explicitly so V3.x'sReservationTTLActorintroduction is additive-only — no V3.x trait revision, no V3.x enum revision, no V3.x consumer-side breaking change per the16-architectural-inheritance.mdccontinuous-discipline corollary. The Round 1 reframe already namedReservationTTLActoras the consumer-actor composition shape (same pattern as PR 4'sPeerReputationActor/RecoveryActor); segment 2e pins the V3.0 deliverables: (1)PendingTxDiagnostic::BuildSucceededemitted at thebuild-success path inLocalPendingTx::build/PendingTxActor::handle_build(Phase 1 call-site review confirms); (2)PendingTxDiagnostic::Discarded { reason: SnapshotRotationAutoDiscard }emitted atsubmit's snapshot-mismatch path (R5's lazy-discard semantics); (3)PendingTxDiagnostic::ReservationOutstandingvariant exists in the#[non_exhaustive]enum (no V3.0 emitter; V3.xReservationTTLActoris the first emitter); (4) new in segment 2e:DiscardReason::TTLAutoDiscardvariant added to the#[non_exhaustive] DiscardReasonset so V3.x'sReservationTTLActorcan triggerPendingTxActorto emitDiscarded { reason: TTLAutoDiscard }events without a V3.x enum revision. R5 ↔ R8 coherence verified — R5'sSnapshotRotationAutoDiscardis the reactive cleanup path (cleanup-on-use); R8'sTTLAutoDiscardis the proactive complement (age-based policy on never-used reservations); both share theDiscardReason/Discardedevent infrastructure. Hard mitigation pins inherited verbatim from PR 4 §5.4.8 (restart-amnesia per #1; recursive trust boundary per #4; bounded mailbox per #5) bind on the V3.x consumer-actor PR via §5.0.3 — no PR 5 amendments needed. ExistingReservationTTLActorFOLLOWUPS entry amended with segment-2e closure-status confirmation and the newDiscardReason::TTLAutoDiscardvariant pin; no new FOLLOWUPS entry needed. The R1 disposition still holds; segment 2e is residual-closure work that finalizes R8's disposition for design purposes. Updates §5.0.2DiscardReasonenum sketch (addsTTLAutoDiscardvariant); §5.4 R8 (closure prose); §5.5 "What Round 2 carries" inventory; §8 fenceposts; header status; CHANGELOG; FOLLOWUPS. No code changes; no test impact. -
Stage 1 PR 5 — Round 2 segment 2d (R2 + R12 co-disposition; Phase 0c truly collapses;
SnapshotIdopacity closed as 16-byte content-addressed digest). Doc-only commit onfeat/stage-1-pr5-pending-tx-engine-design. Segment 2d closes the two remainingSnapshotId-adjacent residuals against the actual shape of theLedgerSnapshotsubstrate landed in PR 2. R12 closes as (a) — content-derivedSnapshotIdfrom existingLedgerSnapshotdata; substrate inspection confirmedLedgerSnapshotcarriessynced_height: u64+reorg_blocks: ReorgBlocks(deterministic by construction; sufficient for content- addressed derivation). Stage 1'sLocalPendingTxderivesSnapshotIdfromLedgerEngine::snapshot()(existing trait method); Stage 4'sPendingTxActorreceives identical values viaLedgerDiagnostic::SnapshotMergedevents using the same digest function. NoLedgerEnginetrait amendment; Phase 0c truly collapses. R2 closes as opaque 16-byte content-addressed digest (pub struct SnapshotId([u8; 16])); domain-separated hash overLedgerSnapshot's deterministic fields; specific hash primitive pinned at Phase 0 review (segment 2g) per §3.1 PQC-discipline alignment. Determinism required by §5.0's submit-handler field-comparison contract; height-leak side-channel closed by construction. §5.5 ground-1 prose softening — drop "(pending R12)" qualifier; ground 1 is now closure-confirmed alongside grounds 2 and 3. §4 Phase 0c prose softening — drop "(pending R12)" qualifier; Phase 0c is REMOVED at the trait surface, full stop. Projection-type discipline preserved-as-pattern — no V3.0 PR 5 call-site introduces a cross-trust-boundarySnapshotIdorSnapshotMergedconsumer; the projection-type implementation lands in the V3.x consumer-actor PR per PR 4 §5.4.8 #4's recursive- trust-boundary discipline. R16 conditional V3.0 lift evaluation (segment-2c trigger):LedgerBlockcarries no per-block fee data today; lifting R16 (c) to V3.0 would require either a storage-layout amendment (persistence- layer migration) or an unbounded historical-block walk per estimator call — neither is bounded cost; R16 (c) does not lift to V3.0, the conservative segment-2c default holds, and R16 (c) lands in V3.x behind a coordinatedLedgerEngine+FeeEstimatorPR. The R1 disposition still holds; segment 2d is segment-2c follow-through (closure-rule operational discipline applied to the conditional-V3.0-lift surface) plus theSnapshotId-substrate co-disposition the §8 fenceposts sequenced for this slot. Updates §5.4 R2, §5.4 R12, §5.4 R16, §4 Phase 0c, §5.5 ground 1, §5.5 "What Round 2 carries" inventory, §8 fenceposts, header status, and CHANGELOG. No code changes; no test impact. -
Stage 1 PR 5 — Round 2 segment 2c (closure-rule and lens-applicability refinements paired with R13 / R15 / R16 / R17 named with dispositions). Doc-only commit on
feat/stage-1-pr5-pending-tx-engine-design. Segment 2c lands two project-wide discipline refinements (lens-applicability structural-conditions test; closure-rule wargaming-surface- known-at-closure-time qualifier) alongside four named-with- disposition R-residuals (R13 output-selection algorithm; R15 submission-strategy as composable actor; R16 wallet-side fee estimation; R17 event-sourced recovery as user-controlled tradeoff). All four R-residuals close their V3.0 vs V3.x decisions with seam-design implications for Phase 0 (OutputSelector/SubmissionStrategyActor/FeeEstimator/ refined diagnostic-stream contract).§5.0.4 lens-applicability discipline. Section expanded with structured "Lens-applicability discipline" subsection establishing three structural conditions that govern when the actor-mesh lens applies to a per-engine extraction: (1) trait surface mediates state-mutation across actors, (2) adversarial review surfaces a cross-actor liveness or quiescence dependency, (3) Stage 4 actor-migration target is non-trivial. Per-engine PR pre-flights test applicability rather than presume it; the lens compounds across PRs whose structure admits it, not uniformly. Closure-rule cross-reference and fourth-shape adversarial- test record (Round 1 closure-review log: (1)-build paired with (3)-submit hybrid tested and rejected on criterion 5). Forward-template content for V3.1 rules-queue PR.
§7 closure-rule strengthening. Restructured into "Closure rule (strengthened)" + "Round 1 closure rule (applied to PR 5)". General rule pinned: Round-N closes when the wargaming surface known at closure time is genuinely exhausted; new shapes surfacing in Round-N+1 reopen Round N rather than slipping past closure (the closure rule pins what was known, not what could ever be known). Lens-applicability cross-reference: closure rule's "exhausted" criterion is satisfied differently depending on whether the lens applies. Round 1 fourth-shape closure-review test recorded as instance of the strengthened rule. Forward-template content for V3.1 rules-queue PR.
§5.4 R13 — output selection algorithm. Added with threat-model framing (deterministic-correlation, change- reuse, order-leak independent of FCMP++ ring semantics); options enumerated; disposition closed as V3.0 ships wallet2-greedy under
OutputSelectortrait-parameter seam (LocalPendingTx<S: Signer, O: OutputSelector>); V3.x landsRandomizedSelector/EntropyMaximizingSelectoralternatives.§5.4 R15 — submission strategy as composable actor. Added with threat-model framing (transaction-network-entry-point timing / routing as wallet-layer privacy weakness against
ANONYMITY_NETWORKS.mdadversary); options enumerated; disposition closed as V3.0 shipsSubmissionStrategyActorseam withDirectStrategydefault; V3.x landsJitteredSubmissionStrategy/CircuitRotationStrategy/BroadcastStrategy/BatchedStrategy.§5.4 R16 — wallet-side fee estimation. Added with threat-model framing (daemon-recommendation on-chain fingerprint exploitable by malicious daemon per §5.3 threat-model anchor); options enumerated; disposition closed as V3.0 ships daemon-recommendation-with-explicit-override under
FeeEstimatortrait seam; V3.x landsWalletSideEstimatoranalyzingLedgerEnginehistorical block fee distribution. Conditional V3.0 lift noted: if segment-2d Phase 0 review confirms boundedLedgerEngine- accessor cost, R16 (c) lifts to V3.0.§5.4 R17 — event-sourced recovery as user-controlled tradeoff. Added with threat-model framing (PR 4 §5.4.8 #1 restart-amnesia rule's privacy property = diagnostic-event persistence does not leak across trust boundaries; refinement narrows prohibition to cross-boundary persistence specifically); options enumerated; disposition closed as V3.0 ships PR 4 §5.4.8 #1 carryover (drop-on-close); V3.x optionally lands encrypted-persistence consumer for institutional / long-running / multi-day workflows. Diagnostic-stream contract pin refined: in-memory-by-default plus permitted user-controlled encrypted-persistence opt-in for consumers entirely within wallet's own encrypted-state surface (no cross-trust-boundary leak per PR 4 §5.4.8 #4).
FOLLOWUPS update. Four V3.x entries added (output- selection alternatives under
OutputSelectortrait seam; submission-strategy actors underSubmissionStrategyActorseam; wallet-side fee estimator underFeeEstimatortrait seam; encrypted-persistencePersistenceConsumerActorfor long-running deployments). Each names the V3.x trigger and the seam-design implication that segment 2c lands at V3.0.What Round 2 carries (§5.5). Inventory updated to reflect R13 / R15 / R16 / R17 named-with-dispositions in segment 2c; §5.0.4 lens-applicability discipline + §7 closure-rule strengthening landed in segment 2c; pending segments (2d / 2e / 2f / 2g) unchanged in scope.
§8 fenceposts. Segment 2c moved from "Round 2 — pending" to "Round 2 — completed" with structured prose (six sub-bullets: §5.0.4 + §7 + R13 + R15 + R16 + R17 + CHANGELOG forward-template note).
V3.1 rules-queue inputs (forward-template content). Two forward-template patterns this segment surfaces belong in the consolidated V3.1 rules-queue PR:
- Closure-rule wargaming-surface-known-at-closure-time
qualifier. "Round-N closes when the wargaming surface
known at closure time is genuinely exhausted; new shapes
surfacing in Round-N+1 reopen Round N rather than
slipping past closure." Lift to a project-wide
16-architectural-inheritance.mdcamendment (or standalone closure-discipline rule) when the rules- queue PR consolidates. - Lens-applicability structural-conditions test. The
actor-mesh lens compounds across PRs whose structure
admits it (three conditions: (1) trait mediates
state-mutation across actors; (2) adversarial review
surfaces cross-actor liveness/quiescence dependency;
(3) Stage 4 actor-migration target non-trivial). Per-
engine PR pre-flights test applicability rather than
presume it. Lift to
16-architectural-inheritance.mdcor a newdiscipline.mdcrule when the rules-queue PR consolidates.
Discipline note (forward-template). Segment 2c is discipline-strengthening + opportunity-surface naming work that compounds project-wide design discipline without reopening the load-bearing question. Where segment 2a was audit-readiness and segment 2b was architectural-integrity-now at the residual level (R11), segment 2c lifts the project-wide pattern that makes future per-engine PR pre-flights answer the same questions methodically rather than adversarially.
- Closure-rule wargaming-surface-known-at-closure-time
qualifier. "Round-N closes when the wargaming surface
known at closure time is genuinely exhausted; new shapes
surfacing in Round-N+1 reopen Round N rather than
slipping past closure." Lift to a project-wide
-
Stage 1 PR 5 — Round 2 segment 2b (R11 signing-actor split reframe to (b); R14 reservation extensibility seam). Doc-only commit on
feat/stage-1-pr5-pending-tx-engine-design. The post-Round-1-closure adversarial review's primary finding surfaced an architectural-integrity-now item that the Round 1 R11 working disposition deferred under PR 4 R4-consistency grounds; segment 2b reframes R11 to (b) — separateLocalSigner/SigningActorfrom Stage 1 — and adds R14 as a near-zero-cost reservation extensibility seam in the same commit.-
R11 reframe to (b) (architectural-integrity-now). §5.4 R11 prose replaced. Round 1's working disposition leaned (a) —
PendingTxActorholds spend material, "matches PR 4 R4's instance-scoped pattern" — with shape (b) (separateSigningActor) deferred to V3.x with the HW-wallet trigger. The cost-asymmetry argument that justified PR 4 R4's tactical (a) (Scanner already existed in C++ holding view + spend material; restructuring Scanner was the deferral trigger) does not apply to PR 5 R11: PR 5 is opening the trait surface;LocalPendingTxdoes not yet exist; the choice between (a) and (b) is the same cost either way (we are designing one or the other from scratch, not moving from one to the other). R4-consistency cuts the other way: PR 4 R4's (a) explicitly named (c) as the long-term shape with the HW-wallet trigger; PR 5 R11 lands that long-term shape from the start. HW wallets are core, not edge, per00-mission.mdc§1; designing the trait surface so spend material never entersPendingTxActoris the threat-model-correct shape; deferring it to V3.x treats the architecturally-cleaner shape as an optimization rather than the baseline. Audit surface narrows under (b) (one actor whose sole job is signing); Stage 4 actor-migration cost is asymmetric (splitting an existing actor is harder than designing actors split). §5.0.1 sketches updated to addsigner: Arc<S>(Stage 1) andsigner: ActorRef<SigningActor>(Stage 4) fields plus prose pinning the spend-material-locality discipline. -
R14 reservation extensibility seam. New §5.4 R14 entry.
Reservationshape gains anextensions: Vec<ReservationExtension>field;ReservationExtensionis#[non_exhaustive]with empty V3.0 variant set; same extensibility pattern asRefreshDiagnostic/PendingTxDiagnostic. Forecloses V3.x trait revision when coinjoin / atomic-swap / time-locked / multi-stage / composable reservation variants land in V3.x consumer-actor PRs. Round 2 hygiene at near-zero cost; large optionality preservation. -
FOLLOWUPS update. The pre-segment-2b
PendingTxEngine-(b)-signing-actor-split V3.x deferral entry inFOLLOWUPS.mdis replaced by a V3.x entry tracking HW-wallet integration as aSigner-impl substitution against the existing architecture. PR 4 R4 V3.x deferred-(c) (split-producer/recoverer for view-tag matching vs. final hybrid-decap) remains V3.x-deferred but benefits from PR 5 R11 (b)'sSigningActorinfrastructure: the spend-key- isolated actor R4 (c) needs has a precedent in PR 5'sSigningActor; lifting R4 (c) at the V3.x trigger becomes simpler. -
Discipline note (forward-template). R11's reframe is the architectural-integrity-now discipline applied at the residual-disposition level — R-residual dispositions inherit the same architectural-integrity-now discipline that PR 3 / PR 4 established at the load-bearing question. The cost-benefit-defer-to-later anti-pattern per
16-architectural-inheritance.mdcrecurred in a residual disposition rather than a load-bearing question; segment 2b's reframe makes future per-engine PRs subject to the same discipline at the R-residual level. -
Header status + §8 fenceposts updated. Header acquires a Round 2 segment 2b paragraph documenting the R11 reframe rationale and the R14 extensibility seam. §8 fenceposts: segment 2b moves to "Round 2 — completed" with a per-item breakdown; pending segments renumber as 2c (closure-rule + lens-applicability + R13 / R15 / R16 / R17 named with dispositions), 2d (R2 + R12 co-disposition), 2e (R8), 2f (R9 + sink-binding decouple from R11), 2g (close-out).
-
-
Stage 1 PR 5 — Round 2 segment 2a (audit-readiness): §5.3 criterion 5 strengthening + threat-model anchor explicit defense + §5.5 scorecard rationale clarification. Doc-only commit on
feat/stage-1-pr5-pending-tx-engine-design. The post-Round-1-closure adversarial review surfaced five refinements for Round 2; segment 2a lands the three audit- relevant items (3 / 4 / 5 from the outcomes summary) in one commit ahead of the R-residual dispositions per the audit-blocking sequencing decision so audit-prep does not sequence behind R2 / R8 / R9 / R11 / R12.-
Item 4 (audit-blocking) — §5.3 criterion 5 strengthening. Reframes the rejection ground for shapes (2)/(3) from "cross-actor liveness query" to "contract dependency on refresh quiescence at any point in the build/submit flow." Documents the stream-subscription steelman implementation (PR 4
RefreshDiagnostic::AttemptStarted/AttemptCompletedevents push-driving arefresh_in_flight: boolrather than a synchronous query) and explains why it still fails: the daemon controls whenAttemptCompletedfires, the bool staystrueindefinitely under drip-feed responses, and the build (or submit) stalls regardless of which mechanism observes quiescence. The load-bearing property is the contract dependency, not the observation channel — synchronous query, push-driven bool, mailbox await, polling, or any other mechanism delivering the "quiescent" signal carries the same daemon-controllable failure mode. -
Item 5 — §5.3 threat-model anchor explicit defense. Adversary-controlled-daemon-as-design-center made explicit (not citation-only). References
ANONYMITY_NETWORKS.mdplus the structural property "daemon outside the wallet's trust boundary by design choice, not as a hardened edge case." The Tor/I2P-first deployment posture means adversary-controlled daemons are the expected deployment, not an exception. Designs that admit structural single-peer DoS of transaction submission are rejected as structurally incompatible with the project's primary deployment model — the rejection is not "we can tolerate this in some deployments and harden against it in others"; it is "this contract shape contradicts the deployment model the design serves." -
Item 3 — §5.5 scorecard rationale clarification. One-line clarification expanded into structured prose explaining criteria 4 and 5 share underlying mechanism (the contract dependency on refresh quiescence) but score distinct consequences: criterion 4 (implementation-feasibility / actor-migration compatibility) evaluates "the implementation creates the vulnerability"; criterion 5 (threat-model-survival / adversarial-daemon resistance) evaluates "the threat model exercises the vulnerability." Both ✗s correctly scored; the shared mechanism is one structural property; the criteria evaluate distinct consequence axes; not double-counting.
-
Propagation: §5.1 (2)/(3) + §5.5 ground 3. Updated to use the contract-dependency reframe consistently with §5.3's strengthened framing. The standard implementation and stream-subscription steelman share the same fatal property (contract dependency on refresh quiescence); the prose says so explicitly; the rejection ground is named as "contract-level, not implementation-level."
-
Header status + §8 Round 2 fenceposts updated. Header acquires a Round 2 segment 2a paragraph documenting what landed and why the audit-blocking sequencing puts items 3/4/5 ahead of the R-residual dispositions. §8 restructured into "Round 2 — completed" / "Round 2 — pending" sub-sections with segment 2a marked completed and segments 2b/2c/2d enumerated as pending.
R1 disposition still holds — the strengthening sharpens the audit-blocking defense without reopening the disposition. Segments 2b (closure-rule + lens-applicability), 2c (R2/R12, R8, R9, R11 dispositions), and 2d (Phase 0 enumeration + close-out) follow at normal cadence.
V3.1 rules-queue inputs (forward-template content). Two patterns this adversarial pass surfaced belong in the consolidated rules-queue PR alongside the §19 / rule-15-trinary / pre-flight-FOLLOWUP-scope items already queued from PR #41 Commit 2: (i) closure-rule scope qualifier ("Round-N closes when the wargaming surface known at closure time is exhausted; new shapes surfacing in Round-N+1 reopen Round N rather than slipping past closure"), generalizes from PR 5's specific instance to any project-wide design discipline using round-by-round wargaming closure; (ii) lens-applicability discipline ("project-wide design lenses compound across PRs whose structure admits the lens; future per-trait PRs test applicability rather than presume it"), tempers PR 4 §5.4.6 / PR 5 §5.0.4's projection without weakening the institutional payoff claim. Both land in segment 2b's doc edits to §5.0.4 and §7; the V3.1 rules-queue PR will consolidate them with the other queued inputs.
-
-
Stage 1 PR 5 — PR #43 Copilot review-pass disposition: two R12-enumeration-consistency findings. Doc-only follow-up commit on
feat/stage-1-pr5-pending-tx-engine-design.copilot-pull-request-reviewersurfaced two valid findings on PR #43, both at the same audit-time question ("does R12 appear in every Round 2 residual enumeration?"): §5.1 closure summary at line 429 ("R3 / R5 / R10 dissolve by composition under §5.0; R2 / R8 / R9 / R11 carry to Round 2") and §7 discipline budget revised estimate at line 1294 ("Round 2 disposes residuals (R2 / R8 / R9 / R11)"). Both omitted R12 despite the surrounding sections (§1, §5.2, §5.4, §5.5 "What Round 2 carries", §8 fenceposts) consistently including it. Both fixed verbatim per Copilot's suggestions; defensive sweep via grep confirmed all six R-residual enumerations now consistently carry R12, and the four "what dissolves" enumerations correctly remain on R3 / R5 / R10. Doc-only; no Rust or C++ code touched. -
Stage 1 PR 5 — Round 1 follow-up: R12 (Stage 1
current_snapshotacquisition mechanism) added; §5.5 ground-1 prose softened against implicit overclaim. Doc-only follow-up commit onfeat/stage-1-pr5-pending-tx-engine-design. Round 1 review surfaced one R1-adjacent finding the closure commit implicitly overclaimed: §5.0.1'sLocalPendingTxsketch holdsledger: L"forcurrent_snapshotreads in Stage 1," but §5.5's first structural ground claimed Phase 0c collapses without naming Stage 1's actual snapshot-acquisition mechanism. Adding R12 names the three options without resolving them (deferred to Round 2 alongside R2'sSnapshotIdopacity disposition); the §5.5 ground-1 prose is softened from "Phase 0c collapses" to "Phase 0c collapses at the trait surface (pending R12)" to match the mechanism uncertainty.Three options enumerated in R12 (no resolution).
- (a) Content-derived
SnapshotIdfrom existingLedgerSnapshotdata (working hypothesis). Stage 1 reads snapshot identity via existingLedgerEngine/LedgerSnapshotsurface; computes content-addressed ID locally. Phase 0c truly collapses in this disposition; no new trait surface. - (b) Stage 1 subscribes to the
LedgerDiagnosticstream. Stage 1 implementation symmetric with Stage 4; modest implementation-symmetry cost inLocalPendingTx. Phase 0c still collapses at the trait surface. - (c)
LedgerEnginegrows a small additive accessor. Phase 0c partially restored, but additive only — read- only and idempotent; not the load-bearing coupling the original Phase 0c projected.
Round 2 confirms by inspecting
LedgerSnapshot's actual shape against the working hypothesis. Disposition's outcome triggers a small mechanical softening of §5.5 ground-1 prose (drop "pending R12" qualifier on (a); reword for (b)/(c) as needed) and the matching §4 Phase 0c hedge.Round 1 disposition unchanged. Grounds 2 and 3 (CAS-isn't-CAS / adversarial-daemon-resistance-as-structural) are independently sufficient to defeat shapes (2) and (3) under the actor-mesh framing per
STAGE_1_PR_5_PENDING_TX_ENGINE.md§5.5. Ground 1 is expected confirmation, not load-bearing for the disposition.Findings deferred to Round 2 (review-pass scoping).
- Finding 2 — mailbox-ordering vs daemon-side authority for R9 (terminal-rejection visibility): R9 contract clarification in Round 2.
- Finding 3 — criterion 5 strengthening from "cross-actor liveness query" framing to "contract-dependency-on-refresh- quiescence" framing: closes a steelman attack ("but you could implement (2) via stream subscription, no synchronous query") without changing the disposition. Round 2 prose pass.
- Finding 4 — sink-binding decoupling from R11 in §5.0.2: constructor-bound is the right answer on PR 4 §3.1 / R4 consistency grounds, independent of R11's spend-material disposition. Round 2 hygiene.
This is the architectural-integrity-now disposition per
16-architectural-inheritance.mdcapplied to documentation honesty: cheap residual addition + one prose softening preserves the discipline against the cost-benefit-defer-to-later anti-pattern (the Round 2 commit would otherwise have to correct an overclaim that lived in the Round 1 commit's prose). Doc-only; no Rust or C++ code touched. - (a) Content-derived
-
Stage 1 PR 5 — Round 1 close: actor-mesh reframe + shape (1) disposition (snapshot-ID pinning). Doc-only commit on
feat/stage-1-pr5-pending-tx-engine-design(offdevat PR-#42 merge6de8335d5). Closes the load-bearing open questiondocs/design/STAGE_1_PR_5_PENDING_TX_ENGINE.md§5 in one round rather than the seed's three-to-four-rounds projection because the §5.0 actor-mesh framing exhausts the wargaming surface in this round per the §7 closure rule. Shape (1) — build-against-current-snapshot + snapshot-ID pinning — wins on structural grounds; shapes (2) and (3) fail criterion 5 (adversarial-daemon resistance) by construction under the actor framing; no fourth shape survives. Two rounds saved against the seed projection.The §5.0 actor-mesh reframe. PR 4's Round 2 reframe established a project-wide design lens: the trait surface is the synchronous decision point that consumers branch on; the rich semantic surface lives on the diagnostic-stream seam (
DiagnosticSinkparameter; typed event enum). PR 5 inherits the lens from Round 1 — the cost-benefit-defer-to-later anti-pattern PR 4 named has its cure now structurally available per16-architectural-inheritance.mdc, applied at the load-bearing question rather than discovered in Round 2+.Three structural grounds shape (1) wins on (§5.1).
- Phase 0c collapses (§5.5 ground 1). Under the seed's
synchronous framing,
LedgerEnginehad to growcurrent_snapshot_id() -> SnapshotIdsoPendingTxEngine::buildcould read it inline. Under the actor framing, snapshot identity flows through the diagnostic-stream surface asLedgerDiagnostic::SnapshotMerged { new, prior, height }events emitted at the merge gate's normal operation. Phase 0c (load-bearing cross-trait surface coupling) collapses to Phase 0g (additive event-variant amendment). - The CAS isn't a CAS (§5.5 ground 2). Under the actor
mesh,
submitis a mailbox message; the actor processes one message at a time; "checkreservation.snapshot_idagainstcurrent_snapshot" is a field comparison in the message handler, not a compare-and-swap. There is no concurrency to swap against — the actor is the serialization point. R3 / R10 dissolve as trait-surface contract questions. - Adversarial-daemon resistance is structural (§5.5 ground
3, criterion 5). Under the actor mesh,
PendingTxActoris decoupled fromRefreshActor's liveness by mailbox. Hostile daemon stalling refresh keepsRefreshActorbusy inproduce_scan_result;PendingTxActor's mailbox continues processing build/submit/discard against the most-recently-merged snapshot regardless. Shapes (2) and (3) requirePendingTxActorto queryRefreshActor's state, which is what creates the DoS surface; shape (1) has no such query. Per00-mission.mdc§1 (security as precondition) andANONYMITY_NETWORKS.md(adversary- controlled daemons in privacy-wallet topologies), a shape that admits structural single-peer DoS of transaction submission is rejected even when its UX and trait surface are otherwise minimal.
Five-criteria scorecard (§5.5). Shape (1) passes all five; (2)/(3) pass criteria 1–3 but fail criteria 4 (Stage 4 actor-migration compatibility — their cross-actor query introduces the DoS surface) and 5 (adversarial-daemon resistance, structurally).
Implications for PR 4 (§5.2 — resolved as confirmation). PR 4 §5.3 deferred PR 4 Round 3 to PR 5 R1. Resolution: PR 4 α confirms; the "provisionally load-bearing" qualifier on PR 4 §5.3's α is withdrawn. PR 4 Round 3 is a confirmation-shape round, not a re-evaluation round — α holds and PR 4 advances directly to Round 4 (commit decomposition + Phase 1 commit list). No γ-style consumer-driven refresh-progress streaming is required: under the actor framing,
PendingTxActoralready gets refresh-progress state push-driven from the diagnostic stream; γ becomes a redundant pattern the framing makes superfluous.The diagnostic-stream seam for PR 5 (§5.0.2). Parallel to PR 4's
RefreshDiagnostic, PR 5 definesPendingTxDiagnostic(#[non_exhaustive]) carryingBuildSucceeded/BuildFailed/SubmitAttempted/SubmitSucceeded/SubmitFailed/SubmitSnapshotInvalidated/Discarded/ReservationOutstandingplus theDiscardReasonenum (#[non_exhaustive]:ConsumerExplicit/SnapshotRotationAutoDiscard(R5 lazy-discard) /DaemonRejectedTerminal(R9 terminal disposition)). The trait surface adds a&dyn DiagnosticSinkparameter onLocalPendingTx::new(constructor-bound, matching PR 4 §3.1 / R4 preference; constructor-vs-per-method shape jointly disposed with R11 in Round 2). (Forward-pointer: Round 2 segment 2f tightened the constructor parameter from&dyn DiagnosticSinktoArc<dyn DiagnosticSink>for reference-shape ergonomics during the R11 closure; see the segment-2f and segment-2g CHANGELOG entries below for the final binding form.) The cross-cuttingDiagnosticSinkcontracts from PR 4 §5.4.6 / §5.4.7 R6 reframe / §5.4.8 (non-blocking emit, recursive trust boundary, restart-amnesia detection, panic safety, concurrent emit, emission/return coherence) bind verbatim per §5.0.3.Residuals (§5.4). Five residuals dissolve by composition under §5.0; four carry to Round 2; one new (R11) surfaces. (R12 — Stage 1
current_snapshotacquisition mechanism — was identified in a subsequent Round 1 follow-up commit and added to the Round 2 carry list; see the immediately-following Round 1 follow-up changelog entry. Round 2 thus carries five residuals in total: R2 / R8 / R9 / R11 / R12.)- Dissolved by §5.0: R3 (build-during-refresh-during-reorg
— mailbox FIFO orders structurally), R5-trait-surface-aspect
(outstanding-reservations-on-rotation policy is local to
PendingTxActor, not a trait-surface question), R10 (concurrent build/submit/discard — mailbox FIFO is the actor-system contract). - Carry to Round 2: R2 (
SnapshotIdopacity / projection types; recursive trust boundary), R8 (reservation TTL / leak prevention — reframed asReservationTTLActorcomposition + V3.x FOLLOWUPS), R9 (daemon-side submit failure — reframed as two-stage submit flow with intermediatesubmitted-pending-daemon-ackstate and self-continuation message), R11 (signing-actor split — new under §5.0; Stage 1 keeps option (a) instance-scoped per PR 4 R4; V3.x FOLLOWUPS for option (b)SigningActorisolation, same trigger as PR 4 R4 deferred-(c) HW-wallet integration). - Retained but lower-priority hygiene: R4 (discard
semantics under invalidation), R5-policy-aspect, R6
(
outstanding()semantics), R7 (Send + Sync + 'staticonP).
Phase 0 net change (§4). One amendment removed: 0c (load-bearing cross-trait synchronous query →
LedgerEngine). Two added: 0f (PendingTxDiagnosticenum +DiagnosticSinkparameter onLocalPendingTx); 0g (LedgerDiagnostic::SnapshotMergedvariant addition — cross-trait but additive only, lives in the diagnostic-stream surface not inLedgerEngine's trait surface). Net effect: load-bearing surface coupling collapses to additive-only event-surface coupling, which is exactly the kind of structural cleanup16-architectural-inheritance.mdc's continuous-discipline corollary predicts.V3.x FOLLOWUPS landed in this commit.
ReservationTTLActorconsumer actor (closes R8 by composition; subscribes toBuildSucceeded/Discardedevents; restart-amnesia constraint per PR 4 §5.4.8 #1).SubmitFailureAnalyzerconsumer actor (subscribes toSubmitFailed/SubmitSnapshotInvalidated; pattern detection — manySnapshotInvalidatedin a row → adversarial reorg-churn; recurringFeeTooLow→ fee estimator drift; recursive trust boundary applies).ReservationAuditActorconsumer actor (subscribes to allPendingTxDiagnosticevents; in-memory wallet-action audit log; falls under recursive trust boundary discipline if it persists or exports — projections only).SigningActormigration entry (R11 option (b); Stage 4 spend-secret isolation; same HW-wallet-trigger language as PR 4 R4 deferred-(c)).
Cross-cutting
DiagnosticSinkcontract-doc generalization (Round 2 disposition). The contracts are now used by both PR 4 and PR 5; they are cross-cutting design invariants. PR 4's FOLLOWUPS nameddocs/design/REFRESH_DIAGNOSTIC_STREAM.mdas the spec doc; Round 2 disposes whether to rename toDIAGNOSTIC_STREAM.md(general) or factor a parentDIAGNOSTIC_STREAM_CONTRACTS.mdthat PR 4 / PR 5 inherit from. Doc-only.Doc-only; no Rust or C++ code touched. Cross-references:
STAGE_1_PR_5_PENDING_TX_ENGINE.md§5.0 (actor-mesh framing as Round 1 substrate), §5.1 (three-shape comparison under the lens), §5.2 (PR 4 α confirmed), §5.3 (five criteria), §5.4 (residuals), §5.5 (Round 1 disposition + scorecard);STAGE_1_PR_4_REFRESH_ENGINE.md§5.4.6 / §5.4.7 R6 reframe / §5.4.8 (the cross-cuttingDiagnosticSinkcontracts inherited by PR 5);V3_ENGINE_TRAIT_BOUNDARIES.md§2.4 (PR 5's binding trait surface — unchanged by Round 1). - Phase 0c collapses (§5.5 ground 1). Under the seed's
synchronous framing,
-
Stage 1 PR 4 — PR #42 Copilot review-pass disposition: two typos, one stale work-list row, two CHANGELOG link retargets, one CHANGELOG ordering correction. Six findings surfaced by
copilot-pull-request-revieweron PR #42's design-branch open. Validated each at source; five fixes landed verbatim, one fixed in the opposite-direction-from-Copilot-suggested (CHANGELOG ordering — Copilot suggested oldest-first; the file's established[Unreleased]convention is newest-first within substantive groupings, so the §5.5 hygiene entry moved to the top of the PR 4 cluster rather than to the bottom). Concrete dispositions:- Typo
Forecloseing→ForeclosinginSTAGE_1_PR_4_REFRESH_ENGINE.md§5.4.6 (R6 reframe, concurrent-emit pin discussion). - Typo
dispositon→dispositioninREFRESH_DESIGN_LANDSCAPE.md§6 (bandwidth/pruning interplay paragraph). - §5.5 work-list row for β internal-batching updated
from pre-Round-2 staleness (
V3.x (R2)/ "promotion to FOLLOWUPS pending Round 2 R2 disposition") to the settled Round 2 R2 disposition (closed — kept as §2.2 future-scaling note; not promoted to FOLLOWUPS yet; revisit if V3.0 RC stabilization bandwidth profiling identifies β as the remediation over alternatives). - Two CHANGELOG citation links retargeted from
self-references to
STAGE_1_PR_4_REFRESH_ENGINE.mdover to the actualengine/refresh.rssource. The link text named the source file; the link target pointed to the design doc. Audit readers couldn't follow the citation to code; that misled the audit trail. - PR 4 CHANGELOG cluster reordered so the newest commit
(§5.5 hygiene) sits at the top, matching the file's
[Unreleased]newest-first convention. The Round 1 chronological pair (disposition above review pass) is preserved as a narrative — moving them to the bottom of the cluster would have required two cross-reference rewrites (above→below) for marginal benefit; the minimal-invasive disposition is correct here. The Round 2 sub-cluster was already newest-first; only the §5.5 hygiene's position needed correction. PR #42 test plan updated to describe the resolved layout. Doc-only; no Rust or C++ code touched.
- Typo
-
Stage 1 PR 4 — §5.5 work-list hygiene: P3
apply_scan_result_to_stateVec<usize>-discard row added. Single-row addition to theSTAGE_1_PR_4_REFRESH_ENGINE.md§5.5 work-list against the dev-side FOLLOWUPS entry ("P3:apply_scan_result_to_stateallocatesVec<usize>even for trait-impl callers that discard it") that landed via PR #37 (commit0a0d46b38, 2026-05-10) during the design branch's pre-M3-tail window. The design branch was cut at9e53c82fa(pre-PR-#37); PR #37 reshaped the merge pipeline (LedgerIndexes::ingest_block,process_scanned_outputs,apply_scan_result_to_statecarry insertion-index ranges) and added P3 to FOLLOWUPS as a PR 4-triggered deferral. The work-list row closes the audit delta between the design doc's enumeration and the dev-side FOLLOWUPS state before the design branch lands ontodev. P3's disposition under α (Round 1) plus (a-instance-scoped) view-material (Round 2 R4) remains Round 3 / Round 4 trait-surface enumeration: eitherLedgerEngine::apply_scan_resultgrows to surface the insertion-range carryout (Vec consumed, optimization dead code) orRefreshEngineowns the post-pass directly and the trait method is removed (discard sites disappear). Doc-only; no Rust or C++ code touched. -
Stage 1 PR 4 — Round 1 disposition: α (preserved current shape) for the
RefreshEngineproducer-redesign question. Doc-only commit onfeat/stage-1-pr4-refresh-engine-design(offdevat9e53c82fa). Closes the load-bearing open questiondocs/design/STAGE_1_PR_4_REFRESH_ENGINE.md§5 named in the seed; α is the disposition because it satisfies all four review criteria — PR 4 extraction cleanliness, PR 5 two-phase build/submit/discard contract over reorg events, reservation-tracker reorg surfacing, Stage 4 actor-migration compatibility — without forcing additional discipline into the per-trait PR or its consumers. β (internal batching) and γ (consumer-driven streaming) are separated as independent validation surfaces per19-validation-surface-discipline.mdc(named ondev2026-05-10) and recorded as residual questions R2 (β as V3.x FOLLOWUPS) and a hypothetical follow-up PR (γ if R1's PR 5 design surfaces correctness need).- Adds
docs/design/STAGE_1_PR_4_REFRESH_ENGINE.md§5.4 (Round 1 disposition with four-criteria rationale and R1 / R2 / R3 residuals) and §5.5 (work-list table for every refresh-adjacent item with its target version and "where documented" pointer); marks the producer-redesign decision complete on §3.3's pre-flight checklist; rewrites §5.3's rounds trajectory to reflect Round 1's convergence on α and the resulting compression of Rounds 2–4. - Adds
docs/design/REFRESH_DESIGN_LANDSCAPE.md: refresh-design-space substrate covering the privacy-by-default precondition (§2), the operational view-tag pre-filter fromSTAGE_1_PR_3_KEY_ENGINE.md§3.1.1 (§3), FMD as a V4 research direction (§4 — negative result for V3.0), OMR as a V3.x research direction (§5 — negative result for V3.0), and the pruning-vocabulary sidebar (§7) disambiguating daemon-side--prune-blockchain/ archival--no-prune/ RPC-server prune / wallet-side prune-by-birthday / prune-by-skip-to-height. - Adds a V3.0
docs/FOLLOWUPS.mdentry ("Refresh bandwidth tradeoff under α") naming the cost-benefit artifact PR 4's α-disposition consumed; entry pinned to V3.0 RC stabilization (per the user's 2026-05-12 sequencing decision) so the cold-sync bandwidth tradeoff is load-bearing on RC stabilization rather than open-ended on the post-genesis backlog.
Doc-only; no Rust or C++ code touched. Branch posture:
feat/stage-1-pr4-refresh-engine-designstays ondev-rooted doc-only commits until M3e closes perSTAGE_1_PR_4_REFRESH_ENGINE.md's branch policy. - Adds
-
Stage 1 PR 4 — Round 1 review pass: more carefully-specified α (view-material flow, atomicity, error taxonomy). Same-day follow-up to the Round 1 disposition above. The review pass corrected
STAGE_1_PR_4_REFRESH_ENGINE.md§3.1's materially-wrong "no secret-touching surface" framing to master-secret isolation routed through R4 — the existing producer (engine/refresh.rs:1254) builds aScannercarrying both the view secret (X25519 view-tag pre-filter + hybrid-decap chain) and the spend secret (key-image computation) per attempt, so the load-bearing threat-model property is "no per-output derived secrets cross the trait surface," not "no secrets." The review pass surfaced four additional residual questions and three trait-contract observations:- R4 — view-material flow (constructor-bound vs. per-call
vs. split-producer/recoverer). Load-bearing; affects
LocalRefresh::newconstructor shape and Stage 4 actor envelope. §4 Phase 0a / 0b candidate. Round 2 disposition. - R5 — mid-scan reorg-abort at checkpoint 3. Mitigation for the reorg-amplification adversarial scenario (§5.4.5). Trade-off: extra daemon RPC cost vs. hostile-daemon work amplification. §4 Phase 0d (conditional). Discipline-budget gated. Round 2 disposition.
- R6 —
RefreshError::ConcurrentMutationboundary. Pinned as orchestrator-internal translation ofLedgerEngineerrors; excluded fromRefreshEngine::produce_scan_result's error type. §4 Phase 0c variant set:Cancelled,DaemonError(D::Error),ScannerContractViolation { kind, evidence },ReorgTooDeep { fork_height, max_rewind }. Round 2 hygiene disposition. - R7 —
ScanResultatomicity-under-cancellation contract. Confirmed against the existing implementation (cancel checks at lines 980 / 1140 / 1186 returnCancelledimmediately; partial state drops via the function frame). Pinned in the trait contract perV3_ENGINE_TRAIT_BOUNDARIES.md§2.3 / §7. §4 Phase 0a candidate. - Refines R1's working hypothesis to
build-against-current-snapshot with snapshot-ID pinning
— the reservation tracker carries a snapshot ID per
reservation; the submit path becomes a CAS against
current_snapshot == reservation.snapshot_id. PR 5's design rounds open with this as the working hypothesis.
§5.4.4 three-call-mode constraint. Cold open / restore, steady-state poll (~10–30 s), and post-submit confirmation have very different cost and cancellation profiles; per-call setup must be near-zero for steady-state. Phase 1's commit decomposition (Round 4) must not introduce per-call setup the inherent method did not have.
§5.4.5 adversarial scenarios under α. Four daemon-attack vectors recorded with their dispositions: reorg amplification (mitigation = R5), view-tag DoS (Scanner implementation property; constant-time framing assumes non-adversarial input rates), withholding / partial responses (inherited from PR 1's
DaemonEnginecontract), snapshot poisoning viaLedgerSnapshot(confirmed value-typed at lines 147–156), andScannerContractViolation.evidenceas memory-amplifier vector (bounded shape required).§5.4.6 trait-surface contract pins.
Send + Sync + 'staticbound onR: RefreshEngine(Stage 4kameoactor wrap predicate);Progress-channel trust-boundary pin (consumers must be inside the wallet trust boundary; refused as a design question if not).The α-disposition holds against all of the review pass' findings — none argue for β or γ. They argue for a more carefully-specified α. Doc-only; no Rust or C++ code touched.
- R4 — view-material flow (constructor-bound vs. per-call
vs. split-producer/recoverer). Load-bearing; affects
-
Stage 1 PR 4 — Round 2 close-out: Phase 0c
InternalInvariantViolation+ Phase 0eDaemonOp/ProtocolErrorKindseed enums. Same-day follow-up to the Round 2 reframe contract-pin refinements (immediately-following bullet) that resolves two items the refinements had flagged as "Round 4 vs Round 2 hygiene" questions. Both worth settling in Round 2 because of downstream impact: deferring to Round 4 re-opens a phase Round 2 was supposed to close.Phase 0c amendment —
InternalInvariantViolation { context: &'static str }on the orchestrator-sideRefreshErrorenum. Resolves the §5.4.7 R6 "(a) extendConcurrentMutationor (b) introduceInternalInvariantViolation" cleanup pin at the design layer, not Round 4 commit-decomposition. The retry-loop call sites atengine/refresh.rs:1672–1680and:2055–2065are state-machine invariant violations ("loop body itself is broken" per the existing site comments), not retry-budget exhaustion. Conflating both intoConcurrentMutationwould route "wallet under sustained merge contention" (back off and retry) and "wallet hit an internal bug" (report and stop) through the same variant; downstream consumers (PeerReputationActor, telemetry, user-facing error surface) need the structural distinction.&'static strforcontextis appropriate at this site — compile-time-fixed developer content, not attacker- influenced data; the memory-amplifier and log- exfiltration vectors the producer-trait unit-variant discipline closes do not apply. The variant also bounds future migrations: future "state machine reached a should-never-happen path" findings route here. Round 4 migration target: the two call sites migrate fromMalformedScanResult { reason: "..." }toInternalInvariantViolation { context: "..." }; existing reason strings becomecontextvalues.Phase 0e seed enums —
DaemonOpandProtocolErrorKindinitial variant sets, audited against the producer's actual call-site surface. Two ground-truth findings:DaemonOpnarrows to two variants per theengine/refresh.rsaudit. The producer issues exactly two daemon RPCs:daemon.get_height()(tip fetch; lines 1480 / 1958) andrpc.get_scannable_block_by_number(...)(per-block fetch; line 1190). Under FCMP++ with view-tag pre-filtering,get_scannable_block_by_numberreturns the full per-block payload; no separateGetBlocks/GetTransactions/GetOutputs/GetChainHashesare issued.GetFeeEstimatesandSubmitTransactionarePendingTxEngine-issued (PR 5), not refresh-issued.ProtocolErrorKindis fresh-defined, not a re-export of upstreamshekyl_rpc::RpcError. UpstreamRpcErroris a flat enum carryingStringpayloads in three of its eight variants (InternalError(String)/ConnectionError(String)/InvalidNode(String)) and is not a bounded re-export candidate. The producer must classify upstream into the bounded enum at theRefreshDiagnostic-emission boundary; theStringpayload elision is the load-bearing classification step per §5.4.7 R6's memory-amplifier closure. Initial variant set seeded against the call-site- reachable subset for the refresh producer:{ ConnectionError, InternalError, InvalidNode, InvalidTransaction, PrunedTransaction }. The other upstream variants (TransactionsNotFound,InvalidFee,InvalidPriority) are not reachable from refresh-issued RPCs.
Round 4 commit-decomposition re-audits both seeds (the audit may surface additional reachable variants the seed missed, or paths the seed listed that aren't actually reachable); the audit is authoritative. The seeds serve as design-doc completeness and as an audit checklist.
Doc-only; no Rust or C++ code touched.
-
Stage 1 PR 4 — Round 2 reframe contract-pin refinements: concurrent-emit clarification, producer-panic-safety property, and test-as-canonical-reference pin. Same-day follow-up to the Round 2 reframe follow-up (immediately-following bullet) that closes three smaller remaining holes before Phase 0 closes. None re-open the reframe; each closes a class of drift / failure-mode that would otherwise propagate to the V3.x consumer-actor PR.
Concurrent-emit clarification on the non-blocking pin (§5.4.6 +
DiagnosticSinkdocstring). TheSend + Syncbound permits concurrentemitfrom multiple tasks; the non-blocking contract holds under concurrent emission, not merely per call. Serializing internal synchronization that admits unbounded contention —Mutex<VecDeque<_>>,RwLock-wrapped state, any shared mutable structure without bounded-wait guarantees — violates the contract even when eachemitcall returns promptly in isolation. Conforming implementations use lock-free queueing (crossbeam::queue::ArrayQueue,flumenon-blocking sends), atomic counters, or sharded mailboxes. Forecloses a class of implementation that type-checks against the literal per-call non-blocking property and still re-introduces the producer-liveness hazard at scale — load-bearing under any future producer-side parallelism shape or Stage 4 actor-mesh topology where multipleLocalRefreshinstances share a sink.Producer-panic-safety property and Round 4
PanickingSinktest deliverable (§5.4.6). The non-blocking pin closes the producer-liveness hazard from a blockingemit. It does not close the adjacent hazard from a panickingemit— a buggy or third-party sink implementation that panics (null pointer dereference in a logger, allocator failure in a metrics consumer, panic-on-overflow in an aggregator) propagates unwind through the producer's call stack while theScanner(holding spend material) is live across theemitcall. Pinning "MUST NOT panic" onemitas a hard trait contract is rejected — it is unenforceable at the type system and pushes development cost onto every sink author for limited gain. The load-bearing property lives on the producer side: any panic propagating out ofemitresults in a predictable refresh-attempt failure withScannercleanly zeroized viaDrop, no leaked half-state, and the cancellation token consistently in either fired-or-not state. Phase 1 test deliverable: theAssertionSinkcoherence property test grows aPanickingSinkvariant that panics on configured event variants; the test asserts (a)Scanneris dropped before the panic crosses the producer frame (visible via aZeroizeobserver wrapper in the test harness), (b) no inconsistent producer state remains observable after the unwind, and (c) the panic propagates withoutDrop-chain corruption or double-panic. Round 4 commit-decomposition pass records this alongside theAssertionSinkcoherence test as a Phase 1 deliverable.Test-as-canonical-reference pin on the coherence contract (§5.4.6 +
DiagnosticSinkdocstring). When theAssertionSinkcoherence property test lands in Round 4 it becomes executable documentation of what coherence means. If a future implementer reads §5.4.6 prose and is uncertain about an edge case (e.g., "does aScanProgressemission count toward coherence for aMalformedScanResultreturn?" or "do two distinct error-class events from the same scan span count as one emission or two?"), the test's behavior is the authoritative answer. Prose ambiguities resolve against test behavior, not the other way around; if the test is wrong, the test is fixed and the prose follows, never the reverse. Per19-validation-surface-discipline.mdc, the property test is one of the validation surfaces for the coherence rule; naming it as authoritative makes prose / test drift impossible without explicit re-examination — a future PR landing prose changes to the coherence contract is required to re-examine the test, and vice versa.§5.5 work-list amendments and §8 Round 4 deliverable update. New work-list rows record the four-part contract-pin bundle (
non-blocking+ concurrent-emit clarification + coherence + canonical-reference) and the producer-panic-safety Round 4 test deliverable. §8's "Remaining for Round 4" prose names the pairedAssertionSink(coherence) andPanickingSink(panic-safety) test deliverables as Phase 1 test-design outputs.Doc-only; no Rust or C++ code touched.
-
Stage 1 PR 4 — Round 2 reframe follow-up:
DiagnosticSinkcontract pins and §5.4.8 refinements. Follow-up to the Round 2 reframe (immediately-following bullet) that pins load-bearing contracts the V3.x consumer-actor PR would otherwise have to re-derive from first principles, and tightens two §5.4.8 attack-surface dispositions whose Round 2 framing was correct but underspecified.Two contract pins added at §5.4.6 / §5.4.7 R6 / Phase 0e docstring.
- Non-blocking
emitcontract.DiagnosticSink::emitMUST NOT block. Implementations usetry_send-shaped semantics; on a full bounded channel, unavailable consumer, or any other back-pressure condition,emitdrops the event silently and returns promptly. Pinned to close the producer-liveness hazard: a blocked sink would pin the producer at the emission call holding the Scanner's spend material and would block observation of the cancellation token at checkpoints 2 and 3 — defeating both the §5.4.4 invocation-overhead constraint and the §3.1 wallet-lock-latency property. Without the trait-surface pin, a hostile or buggy consumer-actor sink in V3.x can introduce the hazard post-hoc with no structural reason for the consumer- actor author to know they did. - Emission/return coherence contract.
RefreshEngineimplementations MUST emit at least one correspondingRefreshDiagnosticevent to the sink for every non-CancelledRefreshErrorreturned, before returning the error. Pinned to close the silent-error failure mode (orchestrator rotates peer with no telemetry; reputation actor blind) and the phantom-error failure mode (telemetry attributes a defect to a peer but the wallet then merges that peer's scan result as authoritative). Both fail open at the type-system level; only a contract pin closes them. Phase 1 delivers a property-test CI invariant: anAssertionSinkwrapsLocalRefreshand asserts coherence on fuzzed inputs (Round 4 test-design deliverable).
§5.4.8 #1 — restart-amnesia named explicitly as a deliberate threat-model consequence. The no-persistence posture is correct privacy-first, but an adversary who can observe or trigger wallet restarts (process kill, RPC-daemon restart, scheduled rotation, OOM, user quit-and-restart cycles) can rate-limit hostile behavior to evade reputation accumulation. Pinned forward to the V3.x consumer-actor PR design: detection logic is coarse-window-based, not credit-history-based; no "trust accumulation" over time. Forecloses the evasion-via-restart-cycle and the dual evasion-via-trust- accumulation patterns. Binding on
PeerReputationActorandViewTagAnomalyDetectordesign.§5.4.8 #4 — trust-boundary framing re-phrased recursively. The current text targeted obvious network-bound consumers (analytics, crash reporters, remote tracing); the subtler case is the in-process aggregator-republisher — a consumer in-process by topology but trust-boundary-crossing by publication (metrics-export actors with HTTP endpoints, debug UI actors over IPC, logger actors writing files collected by remote infrastructure, developer-mode flags dumping to off-host log collectors). The principle reframed: full- fidelity events flow only to actors whose external surface is itself inside the wallet trust boundary, recursively. The recursion creates a continuous audit obligation that binds on every PR touching the consumer- actor topology, anchored procedurally to
19-validation-surface-discipline.mdc.Phase 0e seed —
MalformedKindinitial variants recorded. Six daemon-attributable variants (NonEmptyForEmptyRange,RangeLengthMismatch,RangeMembershipViolation,DuplicateHeight,MissingHeightEntry,ResidualAfterApply) covering the currentMalformedScanResult { reason: &'static str }call sites inengine/merge.rsandengine/refresh.rs, so the unit-variant migration has a straightforward mapping at Round 4 commit decomposition. Non-daemon- attributable call sites (the retry-loop-exhaustion reasons inengine/refresh.rs:1678–1680and:2061–2064) are flagged for Round 4 cleanup — they don't belong onMalformedScanResult's "peer rotation decision needed" structural branch.Variant-ordering / serialization forward-note. Under the §5.4.6 / §5.4.8 #4 in-process trust-boundary pin, the diagnostic stream is not serialized to any stable external format and variant ordering is not load-bearing; the
#[non_exhaustive]attribute preserves additive evolution. The note exists for a hypothetical future PR that records diagnostic streams to disk for test replay — at that point, on-disk-format stability becomes load-bearing and the additive-evolution discipline acquires a backward- compatibility constraint. No PR 4 action required; the note is forward-recorded so the future PR has the constraint named.FOLLOWUPS amendments.
- Added
ViewTagAnomalyDetectorV3.x entry with the explicit producer-side dependency: before the detector lands, the producer must grow aViewTagFalsePositive { observed_rate, expected_rate }(or equivalent) variant.#[non_exhaustive]makes the addition additive without trait-surface revision. - Extended the diagnostic-stream spec-doc FOLLOWUPS entry
(
docs/design/REFRESH_DIAGNOSTIC_STREAM.md) to record the four binding contract pins (non-blocking, coherence, recursive trust-boundary, restart-amnesia detection discipline) as load-bearing spec content that consumer-actor PRs reference rather than re-deriving.
Doc-only; no Rust or C++ code touched.
- Non-blocking
-
Stage 1 PR 4 — Round 2 reframe: diagnostic-stream seam supersedes Round 2 first-pass R5 / R6 dispositions. This bullet supersedes the immediately-following bullet's R5 and R6 dispositions per the Round 2 reframe section §5.4.7 R5 reframe / §5.4.7 R6 reframe / §5.4.8. The immediately-following bullet's R1 / R2 / R3 / R4 / R7 dispositions are unchanged and still hold.
Why the reframe. Round 2's first-pass R5 (defer to V3.x with telemetry trigger) and R6 (keep
MalformedScanResult { reason: &'static str }) reasoned aboutRefreshEnginein a synchronous function-call graph where the error is a single isolated event and the payload question is "what does this caller branch on." The design target is an actor-mesh fabric (Stage 4) where the error is a stream event with temporal context, and the same event routes to multiple consumers with different security properties per consumer. The first-pass disposition is the cost-benefit-defer-to-later anti-pattern per16-architectural-inheritance.mdc; the reframe is the architectural-integrity-now answer — lay the seam now, defer only the consumer implementations.The two-channel shape (R6 reframe). The synchronous trait return and the actor-mesh diagnostic stream are different artifacts with different consumers and different security properties; they get different types.
- Channel 1: synchronous trait return
RefreshError— unit variants only. Three variants:Cancelled,Io,MalformedScanResult. No string, no evidence, no payload of any kind. The orchestrator's branch table is structural (cancel-propagate / retry-with-backoff / peer-rotation); the decision needs zero information beyond the variant tag. Closes the memory-amplifier vector by construction — there is no attacker-controlled data anywhere on the producer trait error surface. - Channel 2:
RefreshDiagnosticevent stream emitted viaDiagnosticSink. Rich structured events fan out to specialized consumer actors with per-consumer trust posture and sanitization rules.produce_scan_resultgains adiagnostics: &dyn DiagnosticSinkparameter (per-call; runtime-dispatch; locked now so Stage 4 doesn't re-rev the trait). Stage 1 emits a minimal seed variant set (DaemonMalformed { kind: MalformedKind },DaemonTimeout { op, elapsed },DaemonProtocolError { kind },ReorgObserved { fork_height, depth },ScanProgress { height, candidates }); Stage 1 sinks areNoopDiagnosticSink/TracingDiagnosticSink; the actor-mesh sink lands in V3.x. The enum is#[non_exhaustive]so the variant set grows additively with PR 1's peer-awareDaemonEnginesurface and future-PR consumer patterns. - Sanitization is a property of the consumer, not the stream. Full-fidelity events stay in-process per the §3.1 / §5.4.6 trust-boundary pin (extended from the Progress-channel pin to the broader diagnostic-stream pin); persisted or exported projections are lossy by design.
R5 dissolved by composition (R5 reframe). The reorg-amplification scenario resolves via a
ReorgAmplificationDetectorconsumer actor that subscribes toRefreshDiagnostic::ReorgObservedevents, maintains a windowed count, and signals cancellation back through the existingCancellationTokencheckpoint-3 plumbing. The producer's §7 checkpoint discipline does not grow. No per-checkpoint-3 daemon RPC; no §7 amendment. The capability is added by composition of the actor mesh's consumers; the implementation deferred to the V3.x actor-mesh PR. Trigger is policy-driven, not evidence-driven — the previous "if hostile-daemon work-amplification scenarios become measurable" gate is withdrawn.What the reframe unlocks (consumer-side; deferred implementations). Fail2ban-style intra-session mitigation via
PeerReputationActor(per-peer event history with decay; threshold-based graduated response); pattern-based recovery viaRecoveryActor(Byzantine-fault-tolerance-flavored N-of-M agreement on contested data); reorg-amplification detection viaReorgAmplificationDetector(R5's natural home); future variant additions as the consumer-pattern surfaces mature.Five new attack surfaces honestly enumerated (§5.4.8). The reframe is not free; the diagnostic-stream seam introduces five attack surfaces, each with a mitigation pinnable now and a deferred consumer-actor implementation.
- Peer-reputation fingerprint → in-memory only, scoped to wallet session, drop on close. Privacy-first wins over classical fail2ban's cross-session memory.
PeerIdstability under Tor/I2P →PeerIdis a transport-defined opaque token; decay calibrated to circuit-rotation cadence; Stage 1 variants omit peer attribution entirely until PR 1's peer-awareDaemonEnginesurface lands.- Rotation-timing side-channel → jittered rotation,
batched decisions, temporal decoupling of
event-observation-time from rotation-action-time inside
the
PeerReputationActor. - Diagnostic stream as covert channel → trait-contract pin (§5.4.6 / §3.1): full-fidelity events flow only to in-process consumers inside the wallet trust boundary; cross-process or network-bound consumers receive only explicitly-sanitized projection types.
- Mailbox saturation as DoS → bounded consumer mailboxes with explicit overflow policies (drop-oldest for diagnostics consumers; aggregate-on-overflow for reputation; event-sequence-aware drop for recovery). Producer-side: emit at natural rate; lossless delivery is not promised.
Phase 0 finalized under the reframe.
- Phase 0a: trait-surface contract pins +
ViewMaterialtype definition (R4) + diagnostic-stream trust-boundary pin (Round 2 reframe). - Phase 0b:
LocalRefresh::new(view_material: ViewMaterial)constructor + flat-crate-root exports (R3 confirmation +ViewMaterial). - Phase 0c: reframed — unit-variant
RefreshError(Cancelled/Io/MalformedScanResult; no payload). Orchestrator-sideRefreshErrorretained with backward-compat content constructed orchestrator-side; no attacker-controlled trait payload. - Phase 0d: retired — R5 resolves by composition, not by deferral.
- Phase 0e (new):
RefreshDiagnosticenum +DiagnosticSinktrait +produce_scan_resultsignature change (diagnostics: &dyn DiagnosticSinkparameter). Stage 1 sinks:NoopDiagnosticSink,TracingDiagnosticSink.
FOLLOWUPS amended. The previous Round 2 "extend checkpoint 3" V3.x FOLLOWUPS entry is withdrawn and replaced by the
ReorgAmplificationDetectorentry. Three new V3.x FOLLOWUPS entries added:PeerReputationActor(with §5.4.8 #1 / #2 / #3 mitigation pins binding on the implementation),RecoveryActor, anddocs/design/REFRESH_DIAGNOSTIC_STREAM.mdspec doc (seeded by PR 4's §5.4.7 R6 / §5.4.8 content; grows additively as consumers are designed).Trajectory after the reframe. Only Round 4 remains as PR-4-internal work (Phase 0 commit decomposition + §6 review checklist); PR 5's design rounds carry R1 forward with the snapshot-ID-pinning working hypothesis. The α-disposition's provisionally load-bearing status remains the re-evaluation gate.
Meta-observation recorded. The reframe is the recurrence pattern named by
16-architectural-inheritance.mdc"the cost-benefit-defer-to-later anti-pattern" working against itself — Round 2's first pass defaulted to deferral and minimal-surface; the architectural-integrity-now answer was to lay the structural seam (one parameter, one enum, one trait) and defer only the consumer implementations. The compounded benefit is what16-architectural-inheritance.mdc's "continuous discipline as inheritance prevention" framing predicts: the seam landed now removes the need for V3.x to re-litigate the trait surface.Doc-only; no Rust or C++ code touched.
- Channel 1: synchronous trait return
-
Stage 1 PR 4 — Round 2 dispositions: R2 / R3 / R4 / R5 / R6 / R7 settled. Same-day follow-up to the Round 1 review pass above. Round 2 closes all seven residuals named by Round 1 + the review pass; the more-carefully-specified-α frame is now closed and PR 4's design surface is Phase-0-ready.
- R1 —
PendingTxEngine::buildduring long refresh. Carried into PR 5's design rounds as the working hypothesis build-against-current-snapshot + snapshot-ID pinning — the reservation tracker carries a snapshot ID per reservation; the submit path becomes a CAS againstcurrent_snapshot == reservation.snapshot_id. Of the three sub-options, the only one that gives the reservation tracker monotone snapshot semantics + low-latency UI without serializing user input behind background work. - R2 — β internal-batching. Stays as the §2.2 "future scaling refinement" note; not promoted to FOLLOWUPS. The V3.0 bandwidth FOLLOWUP entry already names α's bandwidth cost; V3.0 RC stabilization profiles cold-sync; if β is the right remediation, promote then. Premature promotion overspecifies against alternatives (daemon-side prefix matching, view-tag pre-filter improvements, wallet-side prune-by-birthday).
- R3 —
RefreshOptions/RefreshProgresspublic-module promotion. Confirmation, not discovery:RefreshOptions,RefreshProgress,RefreshSummary,RefreshHandle,RefreshReorgEvent,RefreshPhaseare already crate-publicly re-exported fromshekyl_engine_core/src/lib.rs:25–30at the flat crate root, matching theDaemonEngine/LedgerEngineconvention. Stage 4'skameoactor implementor imports them as Stage 1 callers do today; no module promotion needed. - R4 — view-material flow to the producer (load-bearing).
Disposition: (a-instance-scoped) —
LocalRefresh::new(view_material: ViewMaterial). New publicZeroize + ZeroizeOnDroptype carrying{ spend_pub, view_scalar, x25519_sk, ml_kem_dk, spend_secret }— exactly the fieldsbuild_scanner_from_keysextracts from&AllKeysBlobtoday. OneScannerheld forLocalRefresh's lifetime; per-attempt cost drops to snapshot+daemon RPC (no scanner construction). Stage 4 actor mailbox carries no secrets. Wallet-lock semantics dropLocalRefreshand zeroize via the existingZeroizeOnDropchain. (c) split-producer/recoverer deferred to V3.x FOLLOWUPS with trigger "HW-wallet-backed signing or post-V3 threat- model refinement requires producer-side spend-key isolation." (b) per-call rejected (hostile to actor migration). - R5 — mid-scan reorg-abort at checkpoint 3. Deferred to
V3.x FOLLOWUPS. The per-checkpoint-3-hit
get_heightRPC cost (~per-block; ~10K+/wallet-day in steady-state) is non-trivial; the reorg-amplification attack is mitigated at a higher layer by PR 1'sDaemonEnginepeer-rotation contract; the discipline-budget cost of extending §7's checkpoint discipline is non-trivial. Trigger for V3.x: "hostile-daemon work-amplification scenarios become measurable in V3.0 RC stabilization or post-genesis production telemetry." - R6 —
RefreshError::ConcurrentMutationboundary + variant set. Promote the existing crate-internalProduceError(engine/refresh.rs:202) to publicRefreshEngineError; use it asRefreshEngine::Error: Into<RefreshError>. Variant set:Cancelled,Io(IoError),MalformedScanResult { reason: &'static str }— the existing name and bounded payload are kept; the user-proposedScannerContractViolation { kind, evidence }rename declined for V3.0 since&'static stris the strictest possible memory-amplifier-mitigation bound. Excluded from producer trait error:ConcurrentMutation(orchestrator-internal merge-gate concern),AlreadyRunning(orchestrator-internal handle-racing concern),ReorgTooDeep(kept as Ok-with-rewind merge-layer detection per §1.5 actor-identity reasoning). The trait/orchestrator split is a Phase 0c spec amendment. - R7 —
ScanResultatomicity-under-cancellation contract. Pinned inV3_ENGINE_TRAIT_BOUNDARIES.md§2.3 / §7 prose: aproduce_scan_resultcall returns either aScanResultcovering the full span scanned, orRefreshError::Cancelled; no partial-spanScanResult. Already true in the existing implementation per the cancel checks atengine/refresh.rs:980 / :1140 / :1186; the contract pin prevents future drift.
§4 Phase 0 finalized. Phase 0a: trait-surface contract pins (
Send + Sync + 'staticonR; Progress-channel trust boundary;ScanResultatomicity per R7;LedgerSnapshotvalue-typed contract;ViewMaterialtype definition per R4). Phase 0b:LocalRefresh::new(view_material: ViewMaterial)constructor + flat-crate-root export ofViewMaterial. Phase 0c:RefreshEngineErrorpromotion per R6. Phase 0d: retired (R5 deferred).Two new V3.x FOLLOWUPS entries in
docs/FOLLOWUPS.md: R5 mid-scan reorg-abort deferral; R4 (c) split-producer/recoverer deferral. Both have named triggers per15-deletion-and-debt.mdc.Trajectory after Round 2. Only Round 4 remains as PR-4-internal work (Phase 0 commit decomposition + §6 review checklist). PR 5's design rounds carry R1 forward with the snapshot-ID-pinning working hypothesis. The α-disposition's provisionally load-bearing status remains the re-evaluation gate: if PR 5's R1 resolution requires γ for correctness, PR 4 re-opens; otherwise PR 4 advances directly to Round 4.
Doc-only; no Rust or C++ code touched.
- R1 —
Fixed
-
CI bench gate no longer false-fails on
baseline=0capture anomalies; the anomaly is surfaced as informational rather than silenced. Discovered on PR #34: thebench-baselinebranch's most-recent refresh (from dev-tip647f82d5) recordedinstructions=0for sixhot_path_bench_ledger_postcard_*entries that the prior nine baselines measured at ~4.4M / 44M / 444M instructions each, with no causal code change between snapshots and iai-callgrind's own run summary embedded inbaseline.iai.snapshotreporting6 without regressions; 0 regressed; 6 benchmarks finished— the capture ran to completion. Cause is unknown (runner-image drift, iai-callgrind-runner version skew, build-flag drift, or a transient anomaly in the measurement layer are all candidates); investigation lives onchore/investigate-bench-baseline-flake-2026-05-09.scripts/bench/compare.pynow routes(base_val == 0 && pr_val != 0)into a distinctbaseline_zerobucket — informational, not gating — that preserves the PR-side measurement for diagnosis.scripts/bench/post_comment.pyrenders the bucket under its own header line ("Baseline anomaly (informational, not gated)") and table rows with a_baseline=0_verdict badge distinct fromok/FAIL/added/missing, so the anomaly surfaces to reviewers rather than being silently masked under the "new in PR" label. The post-mergeupdate-baselinejob re-captures from the next push todev; if the next refresh produces real numbers the anomaly was transient and self-heals, if zeros persist the investigation branch has a fresh signal. Regression guards: real regressions still tripfail(validated with a +39% hot_path fixture); the(base=0, pr=0)edge case is preserved as a 0% deltaokrather than getting routed away. Lock-down:scripts/bench/test_compare.pypins the routing logic with four regression tests (baseline-zero-bucket, real-regression-still-fails, both-zero-stays-ok, added-in-pr-distinct-from-baseline-zero); stdlib-only, runs viapython3 scripts/bench/test_compare.py. -
Bench-capture producer guard rejects
instructions=0rows at source so the anomaly cannot reachbench-baselineagain. Paired defense-in-depth with the consumer-sidebaseline_zerobucket (above): the consumer routes around already-corrupted baseline data; the producer prevents new corruption from being written. Implemented inscripts/bench/capture_rust_baseline.shinside the JSON-assembly heredoc, post-parse / pre-write: any iai entry withmetrics.instructions == 0causes the script to exit2with a structured error that lists the offending(crate, bench_target, group, function, run_id)tuples and points operators atdocs/investigation/2026-05-09-bench-baseline-flake.md. The canonicalshekyl_rust_v0.jsonis not written when the guard trips, so the prior goodbench-baselinecontent is preserved across both pipeline arms —update-baseline(push todev) andcapture-pr(per-PR baseline). The raw stdout snapshot atshekyl_rust_v0.iai.snapshotis still written unconditionally as bisection evidence, and a diagnostic side-file atshekyl_rust_v0.json.flake.jsoncarries the parsed envelope plus aflakeblock enumerating the zero entries — investigators cangh run download-style fetch it without re-running the harness. Bypass:SHEKYL_BENCH_ALLOW_ZERO=1skips the check with a loudWARNINGline for local debugging of the capture-zero phenomenon itself; CI workflows must not set this. Validated with three smoke-tests against the heredoc body in isolation: mixed-healthy-and-zero rejects with exit 2 and writes only the flake side-file; bypass env var allows write-through with the warning; clean capture flows normally with no flake side-file. The guard's error message frames a workflow rerun as the expected operator response, matching the empirically observed flake rate (the same runner class typically produces a healthy capture on retry). -
account_base::generate(...)no longer hardcodesFAKECHAIN; the legacy 3-arg overload is deleted entirely and every caller spells its network out explicitly. Pre-fix, the 3-argaccount_base::generate(recovery_key, recover, two_random)overload (with default argssecret_key{} / false / false) hardcodedDerivationNetwork::Fakechainas the raw-seed derivation salt regardless of the wallet's actualnetwork_type. Three production callers reached it via the implicit FAKECHAIN default:wallet2::generate(name, password, recovery, recover, ...)(the CLI / RPC wallet-creation and recovery entry),wallet2's 0-change dummy-destination address generator (transfer_selected_rct), andwallet_rpc_server::on_stop_background_sync's seed-recovery path. On TESTNET, every from-seed wallet creation produced a FAKECHAIN-salted account that failedwallet2::load's rederive (which usesm_nettype, not FAKECHAIN). On MAINNET / STAGENET, the call was doubly broken: the FAKECHAIN-derived keys disagreed with the rederive salt, and RAW32 isn't a permitted seed format on those networks anyway. This footgun was masked for the entire window during which Bug 1's off-by-one was preventing any wallet from loading. Bug 4-adjacent in the 2026-05-05 FFI constant- drift audit.Fix: the new
account_base::generate(recovery_key, recover, two_random, network_type nettype)overload threads the caller's network throughgenerate_from_raw_seed, and is now the onlygenerate(...)overload — the legacy 3-arg form is deleted entirely.wallet2::generate(...)andwallet_rpc_server::on_stop_background_syncmigrated to passm_nettype/m_wallet->nettype(). The 0-change dummy- destination caller inwallet2::transfer_selected_rctmigrated to the same 4-arg form withcryptonote::FAKECHAINhardcoded — it's a transient one-shot whose secret keys are discarded; properly network-matching the dummy address requires a BIP-39 path on MAINNET / STAGENET (RAW32 isn't permitted there) and is filed under FOLLOWUPS V3.2. All 28 test callers acrosstests/{unit_tests,core_tests,performance_tests,trezor, functional_tests,wallet_bench}migrated to passcryptonote::FAKECHAINexplicitly. The structural deletion eliminates the "one omitted argument away from FAKECHAIN" footgun class entirely — there is no longer agenerate(...)overload that can pick a network silently.Failure-mode change: on MAINNET / STAGENET, every
wallet2-routed raw-seed creation path now throws cleanly via the FFI'spermitted_seed_formatcheck instead of silently producing FAKECHAIN-salted unspendable wallets. The throw scope is wider than just the recovery path:wallet_rpc_server::on_create_wallet(fresh CSPRNG-seed wallet creation) andwallet2_ffi::create(FFI wallet creation) also throw on MAINNET / STAGENET. Both paths were already silently broken pre-fix — the post-fix behaviour is a strict improvement (fail-loud over fail-silent), but neither becomes a finished feature: fresh-seed wallet creation on MAINNET / STAGENET viawallet2simply does not work by design until the wallet2 BIP-39 entry point lands (Bug 4 in the audit, deferred per the Rust wallet migration). On TESTNET / FAKECHAIN, every migrated caller produces correctly- network-salted accounts that round-trip throughwallet2::load.New regression test:
tests/unit_tests/account.cpp::generate_uses_explicit_nettype_argumentpins (a)generate(..., TESTNET)matchesgenerate_from_raw_seed(..., TESTNET), (b)generate(..., FAKECHAIN)produces a distinct account (different HKDF salt), and (c)generate(..., MAINNET / STAGENET)throws for bothrecover=true(recovery) andrecover=false(fresh CSPRNG seed). Seedocs/audit_trail/2026-05-ffi-constant-drift-audit.mdBug 4-adjacent. -
FCMP_REFERENCE_BLOCK_MIN_AGEaligned to consensus authority (5).rust/shekyl-engine-core/src/multisig/v31/intent.rsdefinedFCMP_REFERENCE_BLOCK_MIN_AGE = 10whilesrc/cryptonote_config.hdefines it as5(locked by Decision 14 in commit6561278d9, asserted bytests/unit_tests/fcmp.cpp:668, documented indocs/FCMP_PLUS_PLUS.md:432). The Rust multisigSpendIntentwas added in744ab640723 days after Decision 14 and copied the pre-Decision-14 value10. Bug 3 of the 2026-05-05 FFI constant-drift audit. Failure mode: a multisig wallet would reject reference blocks at heightstip-9..tip-5that the daemon consensus accepts — fail-closed at the wallet's own pre-broadcast validation, no path to silent acceptance, but still a real bug (UX: legitimate intents rejected by the proposer's own check). Fixed by aligning the Rust value to5, with a doc-comment that cross-references the C++ authority and the audit. Testvalidate_temporal_rejects_ref_block_too_freshupdated to usetip = 903(age = 3) instead oftip = 905(age = 5, which was the boundary value that masked the regression — age = 5 is not< 5).docs/SHEKYL_MULTISIG_WIRE_FORMAT.mdaligned. Thechore/cbindgen-consensus-constantsfollow-up generates this value from the Rust authority into the C++ build to prevent recurrence. Seedocs/audit_trail/2026-05-ffi-constant-drift-audit.md. -
C++/Rust FFI constant disagreement broke every wallet round-trip on every network.
src/shekyl/shekyl_ffi.hdefinedSHEKYL_CLASSICAL_ADDRESS_BYTES = 64while authoritativerust/shekyl-crypto-pq/src/account.rs::CLASSICAL_ADDRESS_BYTES = 1 + 32 + 32 = 65. BecauseShekylAllKeysBlobis#[repr(C)]with byte-aligned[u8; N]arrays, the 1-byte deficit shifted every later field's offset by one. C++populate_account_from_blobreadspend_skandview_skfrom the wrong bytes; the resulting non-canonical Ed25519 scalars failedsc_checkinsidesecret_key_to_public_key, soverify_keysreturned false and everywallet2::loadthrewerror::wallet_files_doesnt_correspond. Header constant set to65. Bug 1 of 2 surfaced bywallet_storage.{store_to_mem2file, change_password_mem2file}. Seedocs/audit_trail/2026-05-ffi-constant-drift-audit.md. -
C++/Rust FFI constant disagreement caused every RAW32 wallet to silently mis-encode its
seed_formatbyte.src/shekyl/shekyl_ffi.hdefinedSHEKYL_SEED_FORMAT_BIP39 = 0/_RAW32 = 1while authoritativerust/shekyl-crypto-pq/src/account.rsdefinesSEED_FORMAT_BIP39 = 0x01/SEED_FORMAT_RAW32 = 0x02(with0reserved for "unset"). C++ wrotem_seed_format = 1to disk meaning RAW32; onwallet2::load, the FFI receivedseed_format = 1and Rust decoded it asBip39;permitted_seed_format(Fakechain, Bip39)returnedfalse; the rederive returnedfalsewith"(network, seed_format) pair disallowed or derivation inconsistent". The BIP-39 path was equally broken (both sides held0, which Rust rejected as "unset") but had no test exercising it at the C++/FFI layer — the bug went undetected for the entire window during which Bug 1 was masking it. Header constants set to1/2. Bug 2 of 2. Pre-V3 launch: no on-disk wallets exist, so no migration code is required. Seedocs/audit_trail/2026-05-ffi-constant-drift-audit.md. -
wallet_storageround-trip tests now constructwallet2withcryptonote::FAKECHAIN.wallet2::generate(name, password)routes through the legacyaccount_base::generate()test wrapper, which hardcodesFAKECHAINfor raw-seed derivation regardless of the wallet'sm_nettype. The default-constructedwallet2inheritedMAINNET, so the rederive onloadpassedMAINNET, which doesn't permitRAW32. Tests now usetools::wallet2 w(cryptonote::FAKECHAIN, 1, true)to keep the in-memory derivation network and the on-disk rederive network aligned. The same hardcoded-FAKECHAIN footgun inaccount_base::generate()'s callers (thewallet2::generate("", password)test path andwallet_rpc_server::stop_background_sync) is the Bug 4-adjacent finding indocs/audit_trail/2026-05-ffi-constant-drift-audit.md, slated for the sibling branchfix/legacy-account-generate-network-guard.
Performance
- Refresh post-pass cost drops from O(n × B) to O(k × B). The
engine post-pass at
shekyl-engine-core::engine::merge::populate_engine_handle_fieldspreviously scanned the fullledger.transfersVec on everyEngine::apply_scan_resultinvocation, even though onlyresult.new_transfers.len()entries can match the residue map. At a 100k-transfer ledger refreshed across 1k batches with k≈10 new transfers per batch, the post-pass alone executed ~10⁸ HashMap probes againstresiduethat found nothing — ~5 s of refresh-time wallclock. The merge pipeline now threads the inserted-index list out ofLedgerIndexes::ingest_block(nowRange<usize>), throughLedgerIndexesExt::process_scanned_outputs(nowRange<usize>) andapply_scan_result_to_state(nowResult<Vec<usize>, RefreshError>); the post-pass walks only the freshly-merged indices. Trait-impl wrappers (LocalLedger::apply_scan_result,EngineFixture::apply_scan_result) discard the Vec via.map(|_| ())so the orchestrator-public surface is unchanged. Closes the FOLLOWUPS V3.0 entry "populate_engine_handle_fieldsO(n) → O(k) per scan". Pre-flight:docs/design/PERF_MERGE_INSERTION_INDICES_PREFLIGHT.md.
Removed
- Monero-era keys-file fixtures and unconditionally-skipped
wallet_storagetests deleted. Thetests/data/wallet_00fd416a*andtests/data/wallet_9svHk1*fixtures were inherited from upstream Monero and predate the SHKW1 master-seed envelope entirely; they cannot be loaded under any version of the v3-from-genesis keystore. The three tests that referenced them (wallet_storage.{store_to_file2file, change_password_same_file, change_password_different_file}) had been gated behindGTEST_SKIP()for that reason and were providing zero coverage. Per.cursor/rules/15-deletion-and-debt.mdc's "default: delete": 4 fixture files (~2.3 MB) and 3 skipped tests removed.
Added
-
Rust-internal FFI constant equality-assertion tests (
rust/shekyl-ffi/src/account_ffi.rs::tests).ffi_classical_address_bytes_matches_rust_authorityandffi_seed_format_constants_match_rust_authoritypin the FFI re-exports to the authoritativerust/shekyl-crypto-pq/src/account.rsconstants. Scope (honest): these tests compare two Rust-side values; they do not readsrc/shekyl/shekyl_ffi.h. A hand-edit to the C++#definealone — the exact drift that produced Bugs 1 and 2 — would still leave them green. They catch a different and narrower bug class: divergence introduced inside the Rust workspace between authoritative and re-exported constants, before the C++ build runs. Cross-boundary detection (catching C++-side drift) is the explicit job of the reduced-scope generator in the sibling branchchore/cbindgen-consensus-constants, which generates a header from the Rust constants forRCTTypeFcmpPlusPlusPqc,FCMP_REFERENCE_BLOCK_*_AGE, andADDRESS_VERSION_V1. Full migration of the remaining ~40 fail-closed-on-misuse constants is filed as FOLLOWUPS V3.0 (target pre-audit-final). -
tests/unit_tests/account.cpp— BIP-39 + MAINNET coverage. Four new tests close the only path Bug 2 broke that the existing test surface didn't exercise:rederive_from_bip39_reproduces_account_mainnet(full BIP-39 derive + rederive round-trip viaaccount_base),bip39_passphrase_changes_account_mainnet(passphrase isolation),generate_from_bip39_rejects_fakechain_and_testnet,generate_from_raw_seed_rejects_mainnet_and_stagenet,rederive_from_bip39_reproduces_account_stagenet, andrederive_from_raw_seed_reproduces_account_testnet(consensus-level(network, format)matrix invariants). Thewallet2-level BIP-39 entry point that would let the test use the production API end-to-end does not exist by design — see Bug 4 below. -
CI tripwire defending the
wallet2::generate_from_bip39absence (tests/unit_tests/wallet_storage.cpp). Three SFINAE detectors + one combinedstatic_assertthat fires at build time if a future contributor addswallet2::generate_from_bip39with any of the three most plausible signatures ((std::string&, std::string&, network_type),(epee::wipeable_string&, epee::wipeable_string&, network_type), or(std::string&, network_type)— the defaulted-passphrase shorthand). The honest scope: an exotic signature could still slip past the detectors, so the load-bearing artifact remains the FOLLOWUPS architectural decision, not the tripwire itself. Includes per-detector positive-control self-tests (tripwire_self_test::synthetic_has_member_*) so a refactor that breaks any detector fails its own assertion rather than silently letting the negative one pass for the wrong reason. Tripwire deletes itself withwallet2.cppat Phase 5 of the Rust rewrite. Architectural decision recorded indocs/FOLLOWUPS.md§"V3.1+ Legacy C++ → Rust rewrite scope". Seedocs/audit_trail/2026-05-ffi-constant-drift-audit.mdBug 4. -
Cross-reference comment in
shekyl-crypto-pq::tests::generate_from_bip39_mainnet_roundtrips_to_rederive. Identifies the Rust test as the primary functional guarantee for BIP-39 wallet creation on Mainnet and points forward at the C++ tripwire and the FOLLOWUPS architectural-decision entry. A future investigator asking "where is BIP-39 wallet creation tested?" finds the answer here, not in C++. -
docs/audit_trail/2026-05-ffi-constant-drift-audit.md— one-page audit record. Documents the wallet_storage failure trace, the Bug 1 / Bug 2 / Bug 3 / Bug 4 findings, the 43 constants confirmed aligned, and the prevention work pattern (per-PR equality assertions in this branch, reduced-scope generated header in the cbindgen sibling, full migration in V3.0). Audit-quality artifact for the August external review. -
AllKeysBlobandKeyImagetyped-wrapper sweep (between Stage 1 PR 3 M3a and M3b; short-lived sweep branch off the M3a PR head perdocs/completed/STAGE_1_PR_3_MIGRATION_PLAN.md§3 "Landing notes (M3a closed)"). Closes the deferred-from-M3a typed-wrapper migration that the M3aViewSecretwork pre-announced inshekyl-crypto-pq::keys's "near-term workstream" docstring. Three new newtypes plus two API extensions, no consensus or wire format changes (every wrapper is#[repr(transparent)]; serde formats use#[serde(transparent)]).shekyl-crypto-pq::keysnewtypes:SpendSecret— secret-bearing scalar mirroringViewSecret's discipline exactly:#[repr(transparent)],Clone + Zeroize + ZeroizeOnDrop, noCopy, noDebug,pub(crate) fn from_bytes,as_canonical_bytes()accessor for raw-byte consumers at the boundary.SpendPublicKey/ViewPublicKey— public-key identity values:Copy + PartialEq + Eq + Hash + PartialOrd + Ord + Zeroizefor use as registry keys (LocalKeys'sHashMap<SpendPublicKey, SubaddressIndex>reverse-lookup registry); manual truncatedDebugmatchingKeyImage's privacy-correlation discipline (first two bytes only);pub fn from_canonical_bytesconstructor — engine boundaries outside this crate (shekyl-engine-core::engine::local_keys::derive_subaddress) are legitimate construction sites, mirroringKeyImage's pattern. NoZeroizeOnDropbecause that conflicts withCopy(Rust trait coherence rule); the surroundingAllKeysBlob::dropclears these public fields explicitly via.zeroize()for the same uniform- write-pattern reason raw[u8; 32]fields had.
AllKeysBlobfield migration:spend_pk: [u8; 32]→spend_pk: SpendPublicKeyview_pk: [u8; 32]→view_pk: ViewPublicKeyspend_sk: [u8; 32]→spend_sk: SpendSecretview_sk: ViewSecret(already typed in M3a Commit 2; unchanged)
The
Dropimplementation simplifies:spend_skandview_sknow wipe via field-drop-glue (ZeroizeOnDrop), only public-key + composite fields remain in the manual zeroization block. The#[repr(transparent)]invariant continues to be asserted byshekyl-ffi'ssize_of::<...>()test againstShekylAllKeysBlob.shekyl-crypto-pq::key_image::KeyImageAPI extensions:- Now derives
Zeroize,Serialize,Deserialize, with#[serde(transparent)]. Wire format remains byte-identical to[u8; 32]. Zeroize(withoutZeroizeOnDrop, which would conflict withCopy) lets containers that hold aKeyImagealongside genuinely- secret material (shekyl_engine_state::TransferDetails,shekyl_scanner::RecoveredWalletOutput) wipe every field onDropfor uniform-write-pattern hygiene — the sameCopy + Zeroizepairing the new public-key newtypes use. The manualDebugand absence-of-Displayprivacy discipline is unchanged.
KeyImagecall-site sweep across the workspace:shekyl-engine-state::TransferDetails.key_image:Option<[u8; 32]>→Option<KeyImage>. The on-disk and postcard-schema layouts are preserved byKeyImage's#[serde(transparent)].TransferDetails::zeroizecontinues to wipe the field;ZeroizeonKeyImageremoves the special-caseOption-then-bytes accessor previously needed at the wipe site.shekyl-engine-state::LedgerIndexes.key_images:HashMap<[u8; 32], usize>→HashMap<KeyImage, usize>. Method signatures onmark_spent,unmark_spent,detect_spends,set_key_image,freeze_by_key_image,thaw_by_key_imageupdated to take&KeyImage/KeyImage/&[KeyImage]. The[0u8; 32]filter on rebuild/ingest is removed: the runtime-scanner path always produces a real key image, andOption<KeyImage>already encodes "not yet computed" — sentinel-byte gating was redundant in the on-disk path. (See FOLLOWUPS for the matching deferred promotion ofRecoveredWalletOutput.key_imagetoOption<KeyImage>in V3.1.)shekyl-scanner::RecoveredWalletOutput.key_image:[u8; 32]→KeyImagewith#[zeroize(skip)]. The boundary inledger_ext.rsretains a[0u8; 32]test-fixture filter soRecoveredWalletOutput::new_for_test's zero placeholder maps totd.key_image = None(preserving the offline-derivation /set_key_imagefill-in semantics view-only wallets rely on); a FOLLOWUPS V3.1 entry tracks promoting the field itself toOption<KeyImage>and deleting the boundary filter.shekyl-engine-core::scan::KeyImageObserved.key_image:[u8; 32]→KeyImage. Constructor sites inrefresh.rs's per-block input-walk wrap raw bytes viaKeyImage::from_canonical_bytes.shekyl-proofs::reserve_proof::{ReserveOutputEntry, VerifiedReserveOutput}.key_image:[u8; 32]→KeyImage. The 192-byte per-output wire layout is unchanged — the proof'swrite_per_outputconsumes viakey_image.as_bytes()and the verifier wraps the on-wire bytes back intoKeyImageat the return boundary.shekyl-engine-core::multisig::v31::prover::ProverInputProof.key_image:[u8; 32]→KeyImage.signable_bytes()consumes viakey_image.as_bytes(); serde wire format unchanged.shekyl-engine-core::multisig::v31::counter_proof::CounterProof.consumed_inputs:Vec<[u8; 32]>→Vec<KeyImage>;CounterProofChainView::is_tracked_unspentsignature updated to take&KeyImage.shekyl-engine-core::engine::traits::key::SubaddressKeyPair:spend_pk/view_pktyped asSpendPublicKey/ViewPublicKey.shekyl-engine-rpc::handlers::parse_key_image: now returnsKeyImage(constructor site at the wallet-RPC boundary).- All
[u8; 32]test fixtures acrossshekyl-engine-state,shekyl-engine-core(including bench fixtures and adversarial multisig tests), andshekyl-scannerupdated to constructKeyImage::from_canonical_bytes(...)explicitly.
Cascade closure (verify API + tests). Final pass on the cascade — the public verifier surface and the last test-helper seams:
shekyl-fcmp::proof::verify:key_images: &[[u8; 32]]→&[KeyImage].pseudo_outs: &[[u8; 32]]stays raw — pseudo- output commitments are a different concept, and the type-system protection is specifically for the key-image slot. The verifier consumes typed inputs via.as_bytes()exactly once at the point where the function downcasts to the upstream FCMP++ library's byte-shaped API. Newshekyl-fcmpregular dependency onshekyl-crypto-pq(cycle-free:shekyl-crypto-pqreferencesshekyl-fcmponly as a[dev-dependencies]entry). The type is re-exported aspub use shekyl_crypto_pq::key_image::KeyImagefromshekyl-fcmp::proofso callers (fuzz harnesses) can name it without taking a direct dep.shekyl-ffi::lib'sshekyl_fcmp_verifymarshaling: rebuildsVec<KeyImage>viaKeyImage::from_canonical_bytesfrom the C-supplied*const u8buffer;pseudo_outsmarshaling is unchanged.shekyl-fcmpfuzz targets (fuzz_tx_deserialize_fcmp_type7,fuzz_fcmp_proof_deserialize) updated their key-image generators toVec<KeyImage>since they callverifydirectly.shekyl-engine-core::engine::refresh::tests::make_block_with_spending_tx:key_image: [u8; 32]→key_image: KeyImage; the typed value is unwrapped via.as_bytes()exactly once at theInput::ToKey { key_image: CompressedPoint(...) }construction site (the on-wireCompressedPointis the raw-byte boundary).shekyl-ffi/tests/signing_round_trip::ScannedSecrets.key_image: intentionally remains[u8; 32]. This is a C-ABI scratch buffer:shekyl_scan_and_recoverwrites viakey_image.as_mut_ptr()and the bytes are re-handed to a later FFI call via.as_ptr(). The C ABI is the authoritative raw- byte boundary on both sides; wrapping inKeyImagehere would injectfrom_canonical_bytes/as_bytesshuffles at every seam without adding type protection. A doc-comment on the struct records the rationale.
Property-delivery framing. This sweep is structural — no consensus rule, no wire format, no FFI layout changes. The type-system protection is the deliverable: every secret-bearing 32-byte field and every per-output
KeyImagefield now refuses accidental cross-wiring through Rust's nominal type system, which is what M3d's "secrets confined to engine" property is later going to lean on. M3a alone landedViewSecretand theKeyEnginetrait; this sweep extends the typed-wrapper coverage to every remaining call site so M3b–M3e don't have to revisit the same surface. -
Monero-reference rename for Shekyl-genesis primitives (sweep branch follow-on; analogous to M3a Commit 5's
classical-Monero→classical Edwards-curverename per60-no-monero-legacy.mdc). Three call sites in Shekyl-first crates framed Shekyl-genesis-locked primitives as Monero-side artifacts; reframed to put Shekyl primary, with the upstream/CryptoNote provenance noted as context rather than ownership.rust/shekyl-ffi/Cargo.tomldescription:"FFI bridge between C++ Monero core and Rust modules"→"FFI bridge between Shekyl's C++ core and Shekyl's Rust crates". The C++ daemon is Shekyl's (forked-and-renamed); the FFI does not bridge to upstream Monero.rust/shekyl-crypto-pq/src/derivation.rstest-helper varint comment:// Monero varint encoding→ "Shekyl wire varint (7-bit continuation, CryptoNote-style; same shape as upstream Monero's varint, but Shekyl-genesis-locked)". The varint format is the standard 7-bit-continuation shape inherited from CryptoNote, not a Monero-specific construct.rust/shekyl-crypto-hash/src/lib.rsmodule doc-comment:Keccak-256 hashing matching Monero/Shekyl's cn_fast_hash→Keccak-256 hashing for Shekyl's cn_fast_hash primitive (byte-identical to upstream Monero's; that compatibility is incidental to the genesis-locked Shekyl spec, not a Monero-compatibility requirement).
Out of scope: legitimate provenance pointers (
monero-oxide'shash_to_point, fork-attribution license headers inshekyl-scanner, "Monero mainnet" empirical comparisons,60-no-monero-legacy.mdcexclusion notices documenting what Shekyl deliberately rejects from Monero) are preserved as-is — those describe the fork relationship correctly. The earlier M3a Commit 5 sweep cleared the design-doc misframings; this pass closes the remaining Rust-side residue. -
KeyEnginetrait surface andLocalKeysin-process implementor introduced (Stage 1 PR 3 — M3a; the third trait-boundaries PR perdocs/V3_ENGINE_TRAIT_BOUNDARIES.md§2.3). The M3a slice of the five-PR Stage-1 PR 3 migration (M3a–M3e perdocs/completed/STAGE_1_PR_3_MIGRATION_PLAN.md§3) lands aspub(crate)onshekyl-engine-core. M3a is the architectural foundation against which the "secrets confined to engine" structural property activates at M3d's merge; M3a itself delivers no user-visible behavior change. The trait ownsAllKeysBlobprivately and exposes a workflow-shape surface (no per-output secret material crosses the trait boundary).pub(crate) trait KeyEngineinengine::traits::key. Four workflow-shaped methods perdocs/completed/STAGE_1_PR_3_KEY_ENGINE.md§4:account_public_address(&self) -> &AccountPublicAddress(sync borrowed read);derive_subaddress(&self, idx, purpose) -> Result<SubaddressFor, Self::Error>(sync, two purposes —Auditreturns the classical Edwards-curve(spend_pk, view_pk)pair,Recipientreturns the encoded address + hybrid KEM PK pair);try_claim_output(&self, input) -> impl Future<Output = Result<OutputClaimResult, Self::Error>> + Send(async; bundles X25519 view-tag pre-filter, hybrid decap, HKDF chain, key-image computation, deterministicOutputHandlederivation behind a single trait boundary);sign_transaction(&self, tx) -> impl Future<Output = Result<TxSignatures, Self::Error>> + Send(async; resolves per-input handles to per-output spending material and produces hybrid signatures + FCMP++ witnesses). The associatedtype Error: Into<KeyEngineError>lets orchestration code propagate uniform errors regardless of implementor.pub(crate) struct LocalKeysinengine::local_keys. OwnsAllKeysBlobprivately; cachesAccountPublicAddressand pre-computes(view_scalar, spend_public)cryptographic forms at construction; guards a reverse-lookup subaddress registry underRwLock(theLocalLedgerprecedent for&selfasync with synchronous interior mutation). Real implementations ofaccount_public_address,derive_subaddress(_, Audit), andtry_claim_output; named-infrastructure-gap stubs forderive_subaddress(_, Recipient)andsign_transaction. Constructors:from_keys_blob(keys, network)(production) and#[cfg(test)] from_test_seed(seed)(raw32 testnet derivation for unit/integration fixtures); 11 tests cover cached-address stability, audit-derivation determinism, recipient-stub validation, claim happy path, deterministic- handle property, varyingtx_hash, other-wallet rejection, unregistered-subaddress rejection, register-then-claim sequence, andsign_transactionstub validation.- Two named-infrastructure-gap
KeyEngineErrorvariants:RecipientSubaddressKemKeygenNotImplemented(per-subaddress hybrid X25519+ML-KEM-768 keygen,shekyl_crypto_pq::subaddress::derive_subaddress_kem_keypair, is unbuilt; lands perdocs/completed/STAGE_1_PR_3_KEY_ENGINE.md§6.4 / §3.1.3) andSignTransactionTraitSurfaceIncomplete(TxToSign's public-on-chain per-input data and FCMP++ tree-branch context are PR-5-pinned forward-declared; the bridge toshekyl_tx_builder::sign_transactionlands when thePendingTxEnginePR finalizes the shape). Both variants are#[non_exhaustive]-shaped accretions; existing call sites stay source-compatible as the surface evolves. OutputHandlenewtype +derive_output_handleinshekyl_crypto_pq::handle. 16-byte opaque reference deterministically derived via cSHAKE256 overview_secret || tx_hash || output_index_le8with customization"shekyl/output-handle-v1"perdocs/completed/STAGE_1_PR_3_KEY_ENGINE.md§7.12. The deterministic-handle pathway (Round 4 pre-flight closure of §7.11=(3)) replaces the originally-considered cachedHandleTabledata structure: re-derivation at spend time is cheap (one cSHAKE256 invocation) and dissolves the A6 (memory pressure) and Pattern-5 (concurrent-access) Round-3 attack-surface clusters by construction. Reference vectors locked in the module's test substrate.KeyImagenewtype inshekyl_crypto_pq::key_image. 32-byte canonical compressed Ed25519 encoding ofI = x · H_p(O); the per-output public on-chain double-spend identifier. Carries the same privacy-correlation discipline asOutputHandle(truncatedDebugexposing the first two bytes only; noDisplay; noZeroizebecause key images are publicly derivable from on-chain data). Per.cursor/rules/18-type-placement.mdc,KeyImageis transform-shaped — defined by its derivation function — so it lives with the function rather than with any state-shaped consumer that happens to store it.ViewSecretnewtype inshekyl_crypto_pq::keys.#[repr(transparent)]32-byte wrapper preserving the bit-for-bit FFI layout invariant withshekyl_ffi::ShekylAllKeysBlob.view_sk: [u8; 32]. Manual truncatedDebug; structuralZeroizeOnDrop. WrapsAllKeysBlob::view_sk; downstream call sites consume the canonical bytes via.as_canonical_bytes(). The remainingAllKeysBlobtyped-wrapper migration (spend_sk→SpendSecret,view_pk→ViewPublicKey,spend_pk→SpendPublicKey) lands as a separate short-lived branch between M3a and M3b.- Subaddress derivation primitives relocated to
shekyl_crypto_pq::subaddress. Classical Edwards-curvesubaddress_derivation_scalarandsubaddress_keys(formerly methods onshekyl_scanner::ViewPair) move to a dedicated module per the path-stateless discipline (extension to the stateless-actor framing): paths from trait surface to cryptographic primitive must be stateless end-to-end, not just at their endpoints. The module is positioned to also house the futurederive_subaddress_kem_keypair(per-subaddress hybrid X25519- ML-KEM-768 keygen, §6.4) when its infrastructure lands —
the canonical home for all Shekyl subaddress derivation.
ViewPair::subaddress_keysis preserved as a thin call- through;ViewPair::subaddress_derivationwas deleted (no live caller after the relocation, per.cursor/rules/15-deletion-and-debt.mdc).SubaddressIndex::to_canonical_bytesaccessor and thePRIMARYconstant added toshekyl_engine_state::SubaddressIndexper.cursor/rules/18-type-placement.mdc: state-shaped types whose serialization is cryptographically load-bearing carry a single canonical-bytes accessor at the type definition; the cryptographic functions take pre-converted bytes rather than the typed index.
- ML-KEM-768 keygen, §6.4) when its infrastructure lands —
the canonical home for all Shekyl subaddress derivation.
SourceSecretsBundletransitional contract type inengine::traits::key. Documents the per-input secret materialKeyEngine::sign_transactionneeds —(spend_key_x, spend_key_y, commitment_mask, combined_ss, output_index), eachZeroizing-wrapped — independent of where the secrets originate. The bundle's shape is stable across the migration (M3a populates fromTransferDetails's legacy fields; M3b+ derives internally from(view_secret, source_ciphertext, output_index)); only the source evolves. Localizing the M3b churn to bundle-population sites (rather than across the trait surface and every implementor) is the load-bearing property of this transitional field.
Property-delivery framing: M3a alone does not activate the "secrets confined to engine" property —
TransferDetailsstill carries its 5 secret-bearing fields, and the bridge reads from them transitionally. The property activates at M3d's merge perdocs/completed/STAGE_1_PR_3_MIGRATION_PLAN.md§4.1, when those fields are deleted. M3a is what makes the activation possible: theKeyEnginetrait is the boundary the property eventually attaches to, and the deterministicOutputHandleis the stateless-shape that replaces a per-call handle table by re-deriving spending material at spend time.Post-merge fix-ups against the M3a PR's review feedback (PR #32 Copilot review, landed before merge):
- Redacted
Debugon secret-bearing message shapes.SourceSecretsBundle,TxInputSigningContext, andTxToSigneach now carry a manualDebugimpl (noderive(Debug)) redacting the fourZeroizing<…>secret fields under[REDACTED]. Per35-secure-memory.mdc,Zeroizing<T>: Debugdelegates toT: Debug, so derivingDebugon a secret-bearing struct prints raw secret bytes throughtracingfields, panic backtraces, ordbg!()calls. Three new sentinel-byte tests inengine::traits::key::testspin the redaction. - PRIMARY special-cased in
derive_subaddress(_, Audit). The encoded primary address packs the wallet's base keys (spend_pk = D,view_pk = a*G) intoclassical_address_bytesdirectly, and the reverse-lookup registry pre-registerskeys.spend_pkagainstSubaddressIndex::PRIMARY. The trait method previously routedPRIMARYthroughsubaddress_keys, returning(D + m_0*G, a*(D + m_0*G))— a different point that matched neither the encoded address nor the registry. Special- casingidx.is_primary()to return the base account keys aligns the trait with the encoded address; foridx >= 1, the per-index derivation is unchanged. Newderive_subaddress_primary_audit_returns_base_account_keystest pins the contract; docstrings onshekyl_engine_state::SubaddressIndex,shekyl_crypto_pq::subaddress, and thesubaddress_keysprimitive itself updated to spell out the special-case truth. - Hard-coded pinned vector for
subaddress_derivation_scalar. The priorderivation_scalar_pinned_vectortest re-ran the samekeccak256_to_scalarprimitive on both sides of the equality, so any drift inside that primitive flowed through both arms. Replaced with a true known-answer test (32-byte expected vector hard-coded for(view = 0x0102_0304_0506_0708, idx = 1)) plus a renamed formula-lock companion test that retains the prior coverage. The pair fails in different classes of regression and pins both the spec output bytes and the implementation composition. - Type-placement rule corrected.
.cursor/rules/18-type-placement.mdcnamedSubaddressIndex's home asshekyl-engine-core(twice); the type actually lives inshekyl-engine-state. Updated.
-
LedgerEnginetrait extracted;Engine<S, D>parameterized overL: LedgerEnginewith defaultLocalLedger(Stage 1 PR 2, the second trait-boundaries PR perdocs/V3_ENGINE_TRAIT_BOUNDARIES.md§2.2). The Phase 2aLedgerEngineslice of the Stage 1 trait-extraction work lands aspub(crate)onshekyl-engine-core. The PR's primary surface — theLedgerEnginetrait, theLocalLedgeraggregate, and theEngine<S, D, L>/OpenedEngine<S, D, L>parameterization. The new type parameters carry default arguments (D = DaemonClient, L = LocalLedger) so non-test consumers continue to nameEngine<S>/OpenedEngine<S>exactly as before; the default-argument shape preserves the names of the public types, not every method signature underneath them. The one observable public-API signature change isEngine::ledger(), which now returnsLedgerReadGuard<'_>(a wrapper aroundRwLockReadGuard<'_, LedgerState>) instead of&WalletLedger;LedgerReadGuardderefs toWalletLedger, so call-style read access (engine.ledger().balance(), etc.) is source-compatible. Code that named the previous return type explicitly (let r: &WalletLedger = engine.ledger();) or stored the method as a function item must update — see theEngine::ledger()doc-comment inrust/shekyl-engine-core/src/engine/mod.rsfor the explicit upgrade path. The PR's lifecycle threaded three pre-flight doc-only spec amendments (PRs #22, #23, #25) before the implementation work began — seedocs/completed/STAGE_1_PR_2_LEDGER_ENGINE.md§1.1 / §2.2 for the discipline pattern.pub(crate) trait LedgerEngineinengine::traits::ledger. Post-Phase-0c four-method surface:synced_height(&self) -> u64,snapshot(&self) -> LedgerSnapshot,balance(&self) -> BalanceSummary(sync, infallible reads), andapply_scan_result(&self, ScanResult) -> Result<(), RefreshError>(async, mutating; signalsRefreshError::ConcurrentMutationfor the §5.2 retry contract). The async&selfmutation is enabled by interiorRwLock<LedgerState>per §2.2's Round 3 disposition; this is the Stage-4-correct call shape, landed Stage-1-early so the actor cutover becomes a no-op for this concern.LedgerErroris reserved as an empty starter type for Phase-2a-specific error variants the trait does not currently emit.pub struct LocalLedger { state: RwLock<LedgerState> }inengine::local_ledger.LedgerStatebundlesWalletLedger+LedgerIndexes(the two fields previously held flat onEngine); reservations stay onEnginefor now and migrate toLocalPendingTxwhen thePendingTxEnginePR ships. The aggregate ispub(not the originally-plannedpub(crate)) because Rust requires every default type parameter on apubtype to be at least as visible as the type itself; the traitLedgerEngineitself stayspub(crate)per §1.4 of the contract. Seedocs/completed/STAGE_1_PR_2_LEDGER_ENGINE.md§3.4 for the visibility-lift rationale.Engine<S, D: DaemonEngine = DaemonClient, L: LedgerEngine = LocalLedger>andOpenedEngine<S, D, L>. The ledger component becomes a third generic parameter with a default that preserves the existing concrete-typed shape for production callers, while making the ledger surface substitutable for hybrid tests. The trait-dispatch shape monomorphizes away as expected, but the parameterization intentionally pairs with theLocalLedgerinterior-mutability refactor below; the measured iai-callgrind cost of the combined change onengine_trait_bench_ledger_synced_heightis+390%(10 → 49 instructions, sourced entirely from theRwLock::read()acquisition inLocalLedger::read(), not from trait dispatch). Per thedocs/V3_ENGINE_TRAIT_BOUNDARIES.md§3.3.1 disposition (a) — intrinsic to Stage 1's interior- mutability shape and retiring at Stage 4 when Path B replacesRwLock<LedgerState>withArc-published snapshots for read paths — the cumulative-delta breach is acknowledged as structural rather than as a regression to optimize within PR 2; full reasoning indocs/PERFORMANCE_BASELINE.md'sengine_trait_bench_ledger_synced_heightcumulative-delta footnote. Eachpubitem bounded by thepub(crate)LedgerEnginetrait carries an#[allow(private_bounds)]annotation paralleling theDaemonEngineannotations from PR 1; both clear at Stage 4 when both traits promote topubper §1.4.- Refresh path migrated to
&selfinterior mutation.Engine::synced_heightnow dispatches throughLedgerEngine::synced_height;Engine::apply_scan_result,Engine::refresh, andEngine::refresh_withflip from&mut selfto&self; the producer taskrun_refresh_task's outerArc<RwLock<Engine>>write-lock guard becomes a read-lock per the §3.3 over-serialization framing. The synchronous wrappersrefresh/refresh_withretain theirLocalLedger-specialized impl block because the trait methodapply_scan_resultisasync fnand the sync entry points useLocalLedger::write()directly without a Tokio runtime in scope (queued at V3.x indocs/FOLLOWUPS.mdfor full sync-wrapper generalization).Engine::start_refreshandrun_refresh_taskare generalized overL: LedgerEngine, sufficient for the hybrid retry test to dispatch through the trait againstMockLedger. MockLedgerdeterministic in-memoryLedgerEngineimplementor inengine::test_support. HoldsWalletLedger+LedgerIndexes+ a queued-failure pump (ConcurrentMutation) + aChaCha20Rngreserved for future RNG-driven fixtures. Constructors mirror PR 1'sMockDaemon:with_seed(master, ROLE_LEDGER),with_seed_and_state, plus aqueue_concurrent_mutationhelper for failure injection.ROLE_LEDGERwas reserved in PR 1'stest_support.rsand is now consumed.Engine::replace_ledger<L2: LedgerEngine>(self, ledger: L2) -> Engine<S, D, L2>mirrorsEngine::replace_daemonfrom PR 1.#[cfg(test)] pub(crate)for now; retires alongside the Stage 4 trait-promotion / production-constructor generalization at V3.2 per thedocs/V3_ENGINE_TRAIT_BOUNDARIES.md§1.2 row.- Hybrid retry test
hybrid_apply_scan_result_retries_on_concurrent_mutation— end-to-end coverage of the §5.2 retry contract viaMockLedger.queue_concurrent_mutation. PR 1 covered the §5.2 happy path (hybrid_linear_scan_5_blocks_advances_synced_ height); PR 2 covers the failure-path retry contract; PR 3+ pick up the remaining §5.2 properties under the "each per-trait PR exercises one §5.2 property predecessors have not yet covered" template pinned indocs/completed/STAGE_1_PR_2_LEDGER_ENGINE.md§2.3. engine_trait_bench_ledger_balancecriterion + iai-callgrind bench pair underrust/shekyl-engine-core/benches/, gated on the existingbench-internalsCargo feature. Measures theLedgerEngine::balancetrait method against a 1024-TransferDetailsstate-populated fixture (LocalLedger::populate_for_benchinjects state through abench-internals-only escape hatch; production state remains behind the trait-dispatched mutating path). Theengine_trait_bench_ledger_synced_heightpair from Stage 0 PR-2 carries forward and gains a cumulative-delta row at the PR-tip SHA8efae3a40per §3.3.1 of the trait-boundaries spec. Frozen-baseline source, iai-callgrind gate metric, iai informational metrics, criterion metrics, and capture- environment cross-references forengine_trait_bench_ledger_balance(instructions=20580 on a 1024-TransferDetailsfixture) are now transcribed intodocs/PERFORMANCE_BASELINE.mdfrom N=3-invariant CIworkflow_dispatchruns25307774464,25307777614,25307781436against PR-tip8efae3a40, following the "do-not-transcribe-laptop-captures" discipline established during Stage 0 PR-2. The PR-tip SHA8efae3a40includes two preparatory script commits (80d913ea2: extendBENCHESrow format to thread cargo--features;8efae3a40: append the balance bench row with:bench-internals) that landed after the design doc's nine- commit plan to surface the new bench to the rolling-baseline harness.
-
DaemonEnginetrait extracted;Engine<S>parameterized over the daemon implementor (Stage 1 PR 1, the first trait-boundaries PR perdocs/V3_ENGINE_TRAIT_BOUNDARIES.md§2.5). The Phase 2aDaemonEngineslice of the Stage 1 trait-extraction work lands aspub(crate)onshekyl-engine-core. The PR's primary surface — theDaemonEnginetrait and theEngine<S, D>/OpenedEngine<S, D>parameterization — ispub(crate)and only visible to crate-internal callers; existing public types (Engine,OpenedEngine,DaemonClient, the lifecycle / refresh / pending re-exports inlib.rs) keep their existing shapes for non-test consumers via theD = DaemonClientdefault. The one externally-visible surface change is the removal of the previously-publicDaemonClient::inner()accessor (called out under "Removed" below); cross-workspace audit found zero remaining callers, and the functionality is preserved viaDaemonClient's directRpcimpl.pub(crate) trait DaemonEngine: Rpc + Clone + Send + Sync + 'staticinengine::traits::daemon.type Error: Into<IoError>. Stage 1 surface per §2.5: two method signatures (get_fee_estimates,submit_transaction) defined asimpl Future(the in-trait-async stable form) so the trait is dyn-incompatible by design and every consumer monomorphizes against a concreteD. Method bodies onDaemonClientaretodo!()stubs pending Phase 2a fee-policy / submit-policy work; the trait surface is what's load-bearing for this PR.#[non_exhaustive] FeeEstimates { economy, standard, priority: FeeRate }and#[non_exhaustive] enum TxSubmitOutcome { Submitted { hash }, AlreadyKnown { hash } }colocated with the trait. Both types arepub(crate)and grow additively; Phase 2a may extendFeeEstimateswithestimated_block_height/estimation_timestampetc. andTxSubmitOutcomewith richer dedup context without breaking callers.Engine<S, D: DaemonEngine = DaemonClient>andOpenedEngine<S, D: DaemonEngine = DaemonClient>. The daemon component becomes a generic parameter with a default that preserves the existing concrete-typed shape for production callers (shekyl-cli,shekyl-engine-rpc, the forthcoming Rust JSON-RPC server), while making the daemon-touching surface substitutable for hybrid tests. The parameterization compiles to identical code via monomorphization; expected iai-callgrind delta onengine_trait_bench_ledger_synced_heightis 0% (10 → 10 instructions) since the bench's call path doesn't observe the daemon parameter. Eachpubitem bounded by thepub(crate)DaemonEnginetrait carries an#[allow(private_bounds)]annotation with a centralized rationale on theEnginestruct definition; the annotations clear at Stage 4 when the trait promotes topubperV3_ENGINE_TRAIT_BOUNDARIES.md§1.4.DaemonClientnow implementsRpcdirectly by delegating each method to its innerSimpleRequestRpc. The previousDaemonClient::inner()accessor is removed; in-tree callers (engine::refresh::*) bind againstDaemonEngineorRpcinstead of reaching through to the wrapped transport.From<RpcError> for IoErrorlands inengine::errorto satisfyDaemonEngine::Error: Into<IoError>for theDaemonClientimpl.MockDaemon(renamed fromMockRpc) extends to a fullDaemonEngineimplementor inengine::test_support. Addssubmit_transactiondeduplication by deterministic tx hash,get_fee_estimatesreturning a fixed snapshot (configurable viaset_fee_estimates), fee-error queueing, submit-error queueing, and thewith_seed/with_seed_and_chainconstructors that carry aChaCha20Rngreserved for future RNG-driven affordances per §6.2 (fee jitter, synthetic-fork randomization) — held but not yet consumed at this PR's contract surface. Failure-injection contract fidelity per §6.1 is exercised by a new test suite in the same module (deterministic submit hashing across clones, submit dedup behaviour, fee-snapshot-override persistence, queued-error drain semantics).MockDaemonchain-indexing convention now matches the real-daemon protocol (chain[0]is genesis at height 0;chain[h]is the block at heighth;get_heightreturnschain.len()). The previous off-by-one convention (chain[i]was the block at heighti + 1) was a latent contradiction that surfaced as soon as a hybrid test composedMockDaemonwith the production producer's range derivation. Aligning the conventions removes the bug-attractor; the existingrefresh_driver_testswere re-arithmetic'd in the same commit so the test substrate has one convention going forward.derive_seed(master: &[u8; 32], role: &[u8]) -> [u8; 32]inengine::test_support(HKDF-SHA256 perV3_ENGINE_TRAIT_BOUNDARIES.md§6.2). The first role tagROLE_DAEMON = b"role/daemon"lands in this PR; per-trait roles join as their owning trait extracts. Pinned by a fixture-based unit test so accidental changes to the role tag or KDF construction surface as test failures.#[cfg(test)] pub(crate) Engine::replace_daemon<D2>(self, daemon: D2) -> Engine<S, D2>inengine::lifecycle. Move-rebuild helper for the §6.3 hybrid-construction discipline: realEngine::createwith a dummyDaemonClientpays the lifecycle cost once (file lock, KDF, ledger init, refresh slot), thenreplace_daemon(mock)swaps in theMockDaemonfor the measured region. Test-only visibility; cleanup target is V3.2 alongside the production-constructor generalization overD: DaemonEngine(documented at the method site).- First end-to-end hybrid test under
start_refresh_integration_tests::hybrid_linear_scan_5_blocks_advances_synced_height. WiresMockDaemonas the engine's daemon component for a realstart_refreshinvocation (fresh wallet atsynced_height = 0, six-block chain at heights 0..=5), asserting (a) the producer derivesprocessed_height_range == 1..6, (b)blocks_processed == 5(post-genesis only), (c) post-refreshsynced_height() == 5, (d) the refresh slot releases within 5s ofjoin().awaitreturning. This is the §5.2 retry-contract reachability proof — the slot release timing observation is the first coverage of the success-path lifecycle for the refresh slot (the existingstart_refresh_integration_testsmodule exercises only the unreachable-daemon error path). - Closes the FOLLOWUPS.md V3.1 row "Generic
DaemonClientsoMockRpccan drivestart_refresh". The row's close-condition (handle-layer end-to-end scenarios against a synthetic block batch via a substitutable daemon transport) is satisfied by the parameterization plus the hybrid test above.
Performance gate per
V3_ENGINE_TRAIT_BOUNDARIES.md§3.3.1: theengine_trait_bench_ledger_synced_heightcumulative-delta row for this PR's tip is captured via GHAworkflow_dispatch(N=3 invariance) and appended todocs/PERFORMANCE_BASELINE.mdin a follow-up commit on this branch before merge per the "do-not-transcribe-laptop-captures" discipline established during Stage 0 PR-2.
Removed
-
Chaingen-dependent C++ test surface (
tx_validation,fcmp_tests,staking). Test hygiene Δ1 (2026-05-05) deletestests/core_tests/{tx_validation,fcmp_tests,staking}.{cpp,h}(~2200 lines, 32 registered tests + 7 already-disabled struct decls), thechaingen_main.cppregistrations, and the dead helpersapply_fcmp_pipeline/construct_fcmp_tx/construct_fcmp_staked_txinchaingen.cpp(no callers remain after the test deletion). Root cause: the chaingen synthetic-block mining infrastructure (MAKE_GENESIS_BLOCK,REWIND_BLOCKS_N,MAKE_NEXT_BLOCK) produces v1 coinbase transactions thatcryptonote_format_utils.cpp:295rejects under v3-from-genesis ("Shekyl requires tx version >= 3"); no chain ever materializes on the synthetic side, sofill_tx_sources_and_destinationsreturns no spendable outputs and every test that needs to construct a user transaction fails at chain setup. The CI baseline previously flagged 19 failures (cluster C); a full survey (this PR) confirmed the same root cause hits all 32 chaingen-dependent tests includinggen_fcmp_tx_valid. The invariants those tests covered migrate to Rust perdocs/FOLLOWUPS.md— three target-V3.x entries (tx-validation, FCMP++ tx-pool, staking lifecycle), each landing with the corresponding Rust port of the daemon-side validation path. Per.cursor/rules/20-rust-vs-cpp-policy.mdc, tx validation defines a cryptographic contract → Rust. Per.cursor/rules/15-deletion-and-debt.mdc"default: delete," dead code goes; the V3.1 disposition that previously deferred this work to "wallet2 hardening / V3.2 wallet2 removal" is closed by this deletion. Closesdocs/CI_BASELINE.mdcluster C. -
DaemonClient::inner()accessor inengine::daemon. The method exposed the wrappedSimpleRequestRpcso callers could invokeRpcmethods through it; with the Stage 1 PR 1 parameterization,DaemonClientimplementsRpcdirectly and the indirection is dead. Cross-workspace audit (shekyl-core,shekyl-gui-wallet,shekyl-dev,shekyl-web,shekyl-mobile-wallet,monero-oxide) found zero remaining callers; per15-deletion-and-debt.mdc"default: delete" and the no-#[deprecated]-without-deletion-target rule, the accessor is removed outright rather than retained as a deprecation shim. Any downstream caller can replaceclient.inner().get_height()withclient.get_height()(theRpcsupertrait is in scope whereverDaemonClientis) with no functional difference.
Changed
-
Rust workspace clippy and rustfmt CI gates in
.github/workflows/build.yml(Rust: audit, test, determinismjob, immediately aftercargo audit). Two gates added:cargo fmt --all -- --check— fails CI on any unformatted Rust file across the 14-crate workspace.cargo clippy --workspace --all-targets --keep-going -- -D warnings— fails CI on any clippy finding of any severity. The workspace already configured many lints at deny-level viarust/Cargo.toml[workspace.lints.clippy](let_underscore_must_use,cast_possible_truncation,uninlined_format_args, et al.);-D warningsextends enforcement to the default-warn lints (clone_on_copy,type_complexity,dead_code,bound_in_more_than_one_place, …).
The pre-existing fmt and clippy debt was discharged in this PR's preceding commits before the gates were wired:
cargo fmt --allover 15 Rust files (mechanical import-sort and module-declaration reordering, zero behavior change).- 12 machine-applicable clippy auto-fixes (9
clone_on_copyderef + 3uninlined_format_argsinlines). - 19
let_underscore_must_usecures via destructuring assignment (let _ = expr;→_ = expr;) at best-effort channel-send and join-drain sites. - 7 substantive clippy findings cured with per-site rationale:
bound consolidation in
run_refresh_task,usize::try_fromat the test-loop cast site,RefreshHandleFixturetypedef, and per-item#[allow(dead_code)]on the Phase 2a-stubDaemonEnginetrait surface.
-
.cursor/rules/15-deletion-and-debt.mdc"While we're here" carve-out. New paragraph in the rule clarifying that the "while we're here is the enemy" prohibition does not preclude the disciplined practice of leaving files you are already editing for substantive reasons in fmt-clean and clippy-clean shape. The carve-out distinguishes:- Undisciplined "while we're here" creep (still prohibited): fixing arbitrary out-of-scope issues in unrelated files.
- Disciplined "leave the file you touched in good shape" (now explicitly permitted): mechanical fmt/clippy cleanup within the substantive-edit set such that the post-PR file is fmt-clean and clippy-clean.
The cleanup-PR pattern this project ran for Stage 1 PR 1's fmt-debt is now a one-time discharge, not a recurring practice. Going forward, every file your PR touches is fmt-clean and clippy-clean by the time the PR lands; mechanical findings in files your PR does not otherwise touch remain out-of-scope.
-
docs/CONTRIBUTING.mdRust style and lints section. New section between "CI baseline" and "Branch protection ondev" documenting the two new gates, the workspace-vs-per-item-vs- module suppression hierarchy ([workspace.lints.clippy] allowinrust/Cargo.tomlfor project-wide misleading lints;#[allow(lint_name)]with one-line rationale comment for site-specific suppressions matching the existing project convention; module-level allows reserved for explicit reviewer sign-off), and the carve-out reference. The "Status checks must pass" bullet under "Branch protection ondev" was updated to enumerate the two new gates explicitly.Discipline reversal recorded for future readers: from this PR forward, the previous practice of noting "pre-existing fmt debt in <files> is unmodified per the deletion-and-debt rule" is no longer applicable. Fmt-clean is the gate, not a per-PR option to defer.
-
Swatinem/rust-cache@v2replacesactions/cache@v5in therust-audit-and-testCI job in.github/workflows/build.yml. The prior cache strategy had three documented waste modes measured against dev tip1155c1abe:- ~8m44s post-job cache UPLOAD on every run, regardless of
whether the cache key changed (see
docs/CI_TIMING_BASELINE.md"Per-step breakdown").actions/cache@v5re-uploads the full path set when the cache key differs from what was restored;Swatinem/rust-cache@v2writes deltas only. - No
rustcversion component in the cache key, so a toolchain bump (e.g. 1.94.0 → 1.95.0 as occurred mid-cycle on theubuntu-latestrunner) would have silently restored a 1.94-builttarget/. Swatinem's default key includesrustc --version. - No
~/.cargo/bincaching, socargo install cargo-audit --lockedrecompiled from source every run (~2m34s). Swatinem caches~/.cargo/binby default; combined with--lockedidempotency, the install becomes a few-second metadata check on cache hits.
The
install cargo-auditstep also moved from pre-checkout (where the cache had no chance to populate~/.cargo/bin) to immediately after the Swatinem step, so the cache restore reaches it first.Measured impact (GHA run id
25265761303,chore/ci-cache-tighteningbranch tip911989b24, toolchain 1.95.0; full breakdown indocs/CI_TIMING_BASELINE.md):- Post-run cache UPLOAD: 8m 44s before → 1m 30s cold, 0s hot. Structural; reliably reproduces every run.
install cargo-audit: 2m 34s before → 0s on hot-cache. Structural; reliably reproduces every hot-cache run.- Rust job total wall clock: 48m 22s before (run
25263753443, dev tip1155c1abe) → 37m 24s cold, 35m 57s hot. Headline numbers are noisy becausecargo testswings ±~3m run-to-run independently of the cache (24m 20s cold vs 27m 16s hot on the same SHA). The structural cache savings above are the durable component of the wall-clock delta.
The PR scope is intentionally tight per the
tight_then_iteratedisposition (2026-05-02). APT package caching, extending Swatinem to the C++ build matrix's Rust half (Ubuntu 22.04,Ubuntu 24.04,Arch Linux), ccache effectiveness audits, andcargo-binstallmigration are enumerated as deferred follow-ups indocs/CI_TIMING_BASELINE.md"Out of scope". Each of those is a >1 commit change with its own baseline-then-after capture cycle and lands as its own PR after the Swatinem deltas are observed and documented. - ~8m44s post-job cache UPLOAD on every run, regardless of
whether the cache key changed (see
-
docs/CI_TIMING_BASELINE.mdintroduced to record CI wall-clock per job per dev tip, anchored on the metric being recorded (job-level wall clock, not step durations) so deltas across caching changes are reproducibly comparable. The document captures thechore/ci-cache-tighteningbaseline before/after pair and is the going-forward home for similar captures (CI cache strategy changes, runner-image upgrades, toolchain bumps that affect compile time, etc.). Per91-documentation-after-plans.mdc, this file lives underdocs/rather than scratch so future readers don't have to re-derive baselines fromgh runlogs.
Fixed
-
CI Post Run cleanup no longer surfaces
##[error]ENOENTonrust/target/tests/targetfor theRust: audit, test, determinismjob. TheSwatinem/rust-cache@v2post-run cleanup walker (src/cleanup.tscleanProfileTarget) treats anytarget/subdirectory namedtestsas akaos/macrotest/trybuildnested-workspace layout and recursively cleans bothtests/target/andtests/trybuild/. The recursivecleanTargetDircalls are not awaited, so async ENOENT rejections on missing paths escape the synchronoustry/catchand surface as##[error]ENOENT: opendir rust/target/tests/targetannotations in the run summary. The job concludes success (the action continues), but the annotation pollutes the run summary and obscures real errors.Why we hit it:
rust/shekyl-logging/tests/trybuild.rsusesdtolnay/trybuild, which createsrust/target/tests/trybuild/. We do not usekaos/macrotest, sorust/target/tests/target/never gets created — the walker tries it anyway. Confirmed against Swatinem/rust-cache#144 (open since 2023; the user-proposedif (e.code === "ENOENT") continue;patch never landed).Workaround: a defensive
mkdir -p rust/target/tests/targetstep runs as the last pre-cleanup step in the job, ensuring the walker'sopendircall succeeds and finds an empty directory to clean. Cache cost: a single empty directory entry, negligible. The new step's comment documents the upstream issue, the removal condition (delete the step in the same PR that bumps the action pin once Swatinem merges either the ENOENT-skip patch or addsawaitto the recursivecleanTargetDircalls), and the dependency chain (shekyl-loggingtrybuildtest →target/tests/trybuild/→ walker →target/tests/target/ENOENT). Files touched:.github/workflows/build.ymlin theRust: audit, test, determinismjob (one new step afterdeterminism check). -
Workspace clippy gate green on Rust toolchain 1.95.0. Three newly-deny-able clippy 1.95 findings cured with mechanical, behavior-identical fixes after the toolchain on the
ubuntu-latestGitHub Actions runner advanced past 1.94.0 (which is what the precedingchore/workspace-fmt-clippy-baselinePR was triaged against). Without this fix thecargo clippy --workspace --all-targets --keep-going -- -D warningsgate added in that PR rejects every push todev.clippy::useless_conversion(×3 in vendoredrust/shekyl-oxide/):for (a, b) in xs.into_iter().zip(ys.into_iter())→for (a, b) in xs.into_iter().zip(ys).Iterator::zipaccepts anyIntoIterator, so the inner.into_iter()was redundant. Sites:rust/shekyl-oxide/crypto/generalized-bulletproofs/src/inner_product.rslines 216 and 220 (BP++ inner-product reductiong_bold/h_boldrecursion).rust/shekyl-oxide/shekyl-oxide/fcmp/bulletproofs/src/plus/weighted_inner_product.rsline 380 (verifier folding loop over commitment pairs(L_i, R_i)).
clippy::unnecessary_sort_by(×2 in Shekyl-nativeshekyl-scanner):rust/shekyl-scanner/src/coin_select.rslines 114–115. Both calls sort descending by the second tuple element; rewrote tosort_by_key(|b| std::cmp::Reverse(b.1))per clippy's suggestion. Behavior-identical sort key, no change in coin-selection ordering.
Vendored-divergence framing (in keeping with
10-shekyl-first.mdc): the vendored copies underrust/shekyl-oxide/are already Shekyl-modified relative to themonero-oxidefork pin (UPSTREAM_MONERO_OXIDE_COMMIT=3933664, sync 2026-04-25); a prior commit (44fe03453 chore: resolve all clippy warnings across the Rust workspace) rewroteinner_product.rswith +360/-332 against upstream for clippy compliance under toolchain 1.94. There is no upstream fix to cherry-pick — the same pattern exists at the same lines in upstream (monero-oxidecrypto/generalized-bulletproofs/src/inner_product.rs:204,208), last touched 2025-08-30, and would fail the same lint under clippy 1.95. This PR continues the precedent of treatingrust/shekyl-oxide/as Shekyl-customized vendored code rather than a frozen mirror.Affected-crate test runs locally (release profile, toolchain 1.95.0):
Crate Tests passing generalized-bulletproofs(with--features tests)5 / 5 shekyl-bulletproofs5 / 5 shekyl-scanner47 / 47 (1 ignored, pre-existing) Local
cargo clippy --workspace --all-targets --keep-going -- -D warningson toolchain 1.95.0 returns exit 0 after the fixes.
Changed (BREAKING)
-
Wallet → Engine rename across Rust workspace (decision log "Wallet → Engine rename", 2026-04-27). Mechanical rename of the domain orchestrator type and its supporting crates and modules to consistently use "engine" terminology. The on-chain consensus rules and wire formats are unaffected; this is a source-only API churn.
- Crates renamed. Workspace members and on-disk paths:
shekyl-wallet-core→shekyl-engine-core,shekyl-wallet-state→shekyl-engine-state,shekyl-wallet-file→shekyl-engine-file,shekyl-wallet-prefs→shekyl-engine-prefs,shekyl-wallet-rpc→shekyl-engine-rpc. Theshekyl-cli,shekyl-ffi,shekyl-scanner,shekyl-tx-builder,shekyl-daemon-rpc,shekyl-fcmp,shekyl-crypto-pq,shekyl-proofs,shekyl-address,shekyl-shard-visual, and themonero-oxidefamily (shekyl-oxide) are unchanged. - Module renamed.
shekyl-engine-core::wallet→shekyl-engine-core::engine. The module re-exports retain their semantics through the new path. - Types renamed. Orchestrator-shaped types now use
Engine*:Wallet<S>→Engine<S>,WalletSignerKind→EngineSignerKind,WalletCoreError→EngineCoreError,OpenedWallet→OpenedEngine,WalletCreateParams→EngineCreateParams. Domain-shaped types that name file format primitives or generic envelope concepts (WalletFile,WalletLedger,WalletPrefs,WalletEnvelopeError,WalletOutput) are intentionally retained — they describe a user's set of secrets, not the orchestrator. - CLI surfaces.
shekyl-cliuser-facing strings, help text, REPL prompts (shekyl-cli [engine]>), and command names (engine_inforeplaceswallet_info) now use "engine" terminology throughout per Option α. The--wallet-dir/--wallet-fileflags are renamed to--engine-dir/--engine-file. - Filesystem layout. Default home directory subtree
~/.shekyl/wallets/is renamed to~/.shekyl/engines/. The.walletand.wallet.keysfile extensions are retained so that existing tooling and the file format documentation indocs/WALLET_FILE_FORMAT_V1.mdstay valid. - What is not renamed in this release.
- FFI C ABI symbols.
shekyl_wallet_*#[no_mangle]exports and theShekylWalletopaque-handle struct retain their names. The internal Rust types backing those handles are renamed; the C ABI is held stable until the C++wallet2.cppretirement work in V3.2 lets us cut both at once. See FOLLOWUPS V3.2. - C++ JSON-RPC method names.
wallet_*JSON-RPC method strings exposed by the C++shekyl-wallet-rpc.exebinary are not renamed here. They are deleted, not aliased, when the Rust-native JSON-RPC server lands as part of Phase 4b's Shekyl-native RPC method-set work in V3.2. See FOLLOWUPS V3.2. - C++ binary names (
shekyl-wallet-rpc,shekyl-wallet-cli,shekyl-wallet-bench) and references to them in.github/workflows/build.yml,scripts/bench/, and stress-net harnesses. Tied to the same C++ retirement work.
- FFI C ABI symbols.
- Migration guidance. No on-disk migration code is shipped or
needed pre-V3 launch (per
15-deletion-and-debt.mdc). Pre-launch users re-sync from genesis. Tooling that depends on the renamed Rust crates updates[dependencies]paths and import paths in one mechanical pass; the FFI C ABI and JSON-RPC wire surfaces are intentionally unchanged.
- Crates renamed. Workspace members and on-disk paths:
Added
-
Engine::refreshdriver andproduce_scan_resultproducer (Phase 2arefresh_scan_loopbundle, Branch 1). Theshekyl_engine_core::engine::refreshmodule ships the snapshot-merge-with-retry sync driver that replaces the standaloneshekyl-scanner::sync::run_sync_loop. Public surface:Engine::refresh(&mut self, opts: &RefreshOptions, runtime: &tokio::runtime::Handle) -> Result<RefreshSummary, RefreshError>— synchronous entry point onEngine<S>. Captures aLedgerSnapshotof the wallet's current(synced_height, reorg_blocks)under a brief read borrow, drops the borrow, drives the async producer onruntime, and merges the result back viaapply_scan_result_to_stateunder&mut self. OnRefreshError::ConcurrentMutationthe snapshot is re-taken and the call retries up toRefreshOptions::max_retries.produce_scan_result(rpc, scanner, &LedgerSnapshot, height_range, cancel) -> Result<ScanResult, ProduceError>—pub(crate)async producer that fetches blocks via theRpctrait, scans them withshekyl_scanner::Scanner, detects reorgs by comparingheader.previousagainst the snapshot'sreorg_blocks(with afind_fork_pointwalk on mismatch), and returns a typedScanResultenvelope rather than mutating wallet state in place. Reorgs surface asScanResult::reorg_rewind: Some(_); the merge applies the rewind atomically before applying forward-progress events.LedgerSnapshot { synced_height: u64, reorg_blocks: ReorgBlocks }— minimal read-only view of the pieces of(LedgerBlock, LedgerIndexes)the producer needs to detect reorgs and resume scanning. Cloned (notArc-wrapped) per the snapshot benchmark inrust/shekyl-engine-core/benches/refresh_snapshot.rs, which measures clone cost across realistic reorg-window sizes so any futureArcswitch has an empirical baseline.RefreshOptions { max_retries: u32 }— caller-supplied knobs for the snapshot-merge retry loop. Default8; rationale on the bound is in the decision-log entry "Snapshot-merge-with-retry semantics forWallet::refresh" (2026-04-26).#[non_exhaustive]so Branch 2 can add the cancel-token / progress-channel / batch-size knobs without a breaking change.RefreshSummary { processed_height_range, blocks_processed, transfers_detected, key_images_observed, stake_events, reorg: Option<RefreshReorgEvent>, merge_attempts }— caller-visible result of a successful refresh.#[non_exhaustive];stake_eventsis reserved for Phase 2b's richer event vocabulary and is always0today.RefreshError— typed failure surface:ConcurrentMutation { wallet, result }(snapshot drifted under the producer; safe retry),AlreadyRunning(single-flight enforcement at the binary layer; reserved for Branch 2's handle path),MalformedScanResult { reason }(producer-bug signal: scan-result invariants violated; not a race),Cancelled(cooperative shutdown),Io(RPC failure surfaced fromProduceError::MaxRetriesExhausted). The variant set is#[non_exhaustive].
The driver is the snapshot-merge realization of the cross-cutting locking decision: queries take
&self, mutations take&mut self, and refresh threads the long-running scan between borrow points so the wallet is never held across anawait. The contract is locked indocs/V3_WALLET_DECISION_LOG.md"Wallet::refreshsnapshot-merge-with-retry" (2026-04-26), "MalformedScanResult: producer-bug signal vs.ConcurrentMutation" (2026-04-26), and "Retireshekyl-scanner::sync::run_sync_loop(Phase 2a/4b boundary)" (2026-04-27).The
RefreshHandleasync surface (cancel-on-drop, watch-basedRefreshProgress,AlreadyRunningenforcement,start_refreshspawning) lands in Branch 2 of the bundle (immediately below); this branch is the synchronous entry point and the producer / merge contract that the handle wraps.Test coverage lives in
rust/shekyl-engine-core/src/engine/refresh.rs'smod tests(producer-side: smoke / linear-scan / reorg-shallow / reorg-deep / reorg-at-tip / RPC-failure-fetch / RPC-failure-tip / scanner-failure / cancellation-mid-scan / cancellation-between-blocks / empty-range / range-validation; driver-side: round-trip, reorg-merge, retry-on-concurrent-mutation, retry-budget-exhausted, malformed-scan-result-bypass-retry, cancellation-end-to-end, no-progress-when-tip-equal, reorg-rewind-then-apply). TheMockRpctest scaffold andmake_synthetic_blockhelper live inrust/shekyl-engine-core/src/engine/test_support.rsfor deterministic fault injection across producer and driver suites. -
Engine::start_refreshasync refresh handle (Phase 2arefresh_scan_loopbundle, Branch 2). Theshekyl_engine_core::engine::refreshmodule ships the cancel-on-drop / one-at-a-time / progress- channel handle that wraps the snapshot-merge driver from Branch- The handle spawns the long-running scan onto a tokio runtime the caller does not have to manage, and threads cancellation and progress through typed channels. Public surface:
Engine::start_refresh(self_arc: Arc<tokio::sync::RwLock<Self>>, opts: RefreshOptions) -> Result<RefreshHandle, RefreshError>— async constructor onEngine<S>. Claims aRefreshSlotunder a brief read borrow, spawns a producer task, and returns a handle observing the running task. A second call while a handle is alive returnsRefreshError::AlreadyRunning. TheArc<RwLock<Engine<S>>>shape is the transitional shared- handle realization of the message-passing boundary decided in 2026-04-27 — Engine binary boundary: pure message-passing over shared handle; the actor migration replaces the parameter without changing the handle's external surface.RefreshHandle— RAII handle for the running refresh. Methods:progress() -> watch::Receiver<RefreshProgress>(clonable observer of phase / height / blocks-processed / blocks-total updates),cancel()(idempotent; fires the sharedCancellationToken),is_running() -> bool(non- blocking poll of the producer'sJoinHandle::is_finished),async fn join(self) -> Result<RefreshSummary, RefreshError>(push-completion via internaloneshot; consumes the handle).Drop for RefreshHandleis cancel-only — slot release lives on producer task exit, not on handle drop, so the cancel contract isDrop-scoped while the slot is self-healing across success / error / cancellation paths.RefreshProgress { height, blocks_processed, blocks_total, phase: RefreshPhase }—#[non_exhaustive]snapshot delivered through atokio::sync::watchchannel. Per-attempt semantics:blocks_totalis the per-retry total, not a cumulative running count. The watch channel is seeded byEngine::start_refreshwith the wallet's currentsynced_height(and zeroed counters) so subscribers observe a baseline matching the wallet state before the producer publishes its first per-attempt update.RefreshPhase { Scanning, Merging, Retrying, Cancelled }— coarse-grained producer state.Scanningcovers fetch + scan of a per-block batch;Mergingcovers the brief write-lockedapply_scan_resultcall;Retryingis published when the merge returnedConcurrentMutationand the loop is about to retake the snapshot;Cancelledis published before the handle's completiononeshotfiresErr(Cancelled).RefreshOptionsextended with no new fields in Branch 2;max_retries(Branch 1) is the only public knob.#[non_exhaustive]so future progress / batching knobs do not break call sites.RefreshError::AlreadyRunningbecomes load-bearing in this branch (Branch 1 reserved the variant); other variants propagate unchanged.
Test coverage lives in three new modules:
mod refresh_handle_tests(six unit tests pinning the handle's channel-shaped surface in isolation: progress baseline, progress propagation, cancel + is_running flip, join success, join error, dropped-sender →MalformedScanResult),mod refresh_slot_tests(four unit tests pinning single-flight semantics: claim-when-unheld, claim-fails-when-held, release-on- guard-drop, clone-shares-flag), andmod start_refresh_integration_tests(three integration tests against the real engine + unreachable- daemon:start_refreshpropagatesIoError::Daemonviajoin, concurrentstart_refreshreturnsAlreadyRunning, drop releases the slot for a subsequentstart_refresh). Apub(crate) fn for_test(...)constructor onRefreshHandleis the testability seam that lets the surface tests run without spinning up anEngine<S>.The decision-log scope-closing entry is 2026-04-27 —
RefreshHandle(Phase 2a Branch 2) ships transitionalArc<RwLock<Engine>>under Path B; the upstream handle-shape entry is 2026-04-25 —RefreshHandle: cancel-on-drop RAII, one-at-a-time, scanner checkpoints between blocks. Wider scenario coverage ofstart_refreshagainst synthetic block batches lands whenDaemonClientis generic (deferred outside Branch 2; tracked under V3.1 indocs/FOLLOWUPS.md). -
Engine::create/Engine::open_full/Engine::change_password/Engine::closelifecycle methods onshekyl-engine-core(Phase 1lifecycletask). The newshekyl_engine_core::engine::lifecyclemodule composesshekyl-engine-file,shekyl-crypto-pq::account::rederive_account,shekyl-engine-prefs,shekyl-engine-state::WalletLedger, andshekyl-engine-state::LedgerIndexesinto theEngine<S>orchestrator's open / create / rotate / close surface. Public API:Credentials<'a>— forward-compatible authentication parameter. V3.0 has a privatepassword: &'a [u8]field reachable throughCredentials::password_only(&[u8])andCredentials::password(); V3.1 addsauthenticator: Option<AuthenticatorRequest<'a>>andCredentials::password_with_authenticator(pwd, auth)without breaking existing call sites. Seedocs/V3_WALLET_DECISION_LOG.md"Wallet authentication: V3.0 password-only; MFA is V3.1 via format-version bump" (2026-04-26) for the API shape rationale.OpenedEngine<S>typed-sum return foropen_full.Loaded(Engine<S>)indicates the persisted ledger file decoded cleanly;Restored { wallet, from_height }indicates the keys file was intact but the ledger file was missing or unreadable — the wallet was reconstructed against an empty ledger anchored atfrom_height = restore_height_hintand the caller must drive a refresh to rebuild state. Seedocs/V3_WALLET_DECISION_LOG.md"Wallet::open_full: lost-state surfacing via typedOpenedWalletsum" (2026-04-26).EngineCreateParams<'a>(9 public fields) andCapabilityInput<'a>::Full { master_seed_64, seed_format }forEngine::create. ViewOnly / HardwareOffloadCapabilityInputvariants are deferred alongside the matchingopen_*bodies; the FULL variant ships end-to-end. A#[cfg(test)] EngineCreateParams::for_test_full(base_path, password, master_seed_64)helper pins all eight non-essential fields to known-good defaults for unit-test fixtures; production callers (CLI / RPC) construct the struct literal so the field set is explicit at every call site.Engine::create(params) -> Result<Engine<SoloSigner>, OpenError>— delegates toWalletFile::createwith derivedDerivationNetwork/SeedFormat, runsrederive_accountto populateAllKeysBlob, cross-checksblob.classical_address_bytesagainst the envelope'sexpected_classical_address(failure →KeyError::PublicBytesMismatch), initializesWalletLedger::empty()andLedgerIndexes::empty(), persists initial prefs viaWalletFile::save_prefs, and assembles theEngine<SoloSigner>instance.Engine::open_full(base_path, &credentials, network, daemon, overrides) -> Result<OpenedEngine<SoloSigner>, OpenError>— opens the envelope (mappingWalletEnvelopeError::InvalidPasswordOrCorrupttoOpenError::IncorrectPassword,RequiresMultisigSupporttoOpenError::RequiresMultisig, and capability / network mismatches to the corresponding typed variants), enforces FULL-only on this entry point (OpenError::CapabilityMismatchif the disk envelope is ViewOnly or HardwareOffload), runs the same rederive + public-bytes-cross-check sequence ascreate, surfaces tampered prefs as a structuredtracing::warn!and falls back to defaults perdocs/WALLET_PREFS.md §5's advisory failure policy, rebuildsLedgerIndexesfrom the persistedLedgerBlock, and returnsLoadedorRestored { from_height }based on theWalletFile::openoutcome.Engine::open_view_only(...)/Engine::open_hardware_offload(...)— signature-only stubs that returnOpenError::CapabilityNotYetImplemented { capability }pending the matchingshekyl-crypto-pqAllKeysBlobconstructors. The error variant is deletion-tracked at the code site and indocs/FOLLOWUPS.mdV3.0 → "View/HW lifecycle bodies inshekyl-engine-core". Seedocs/V3_WALLET_DECISION_LOG.md"Wallet<S>lifecycle: capability scoping for V3.0" (2026-04-26) for the stub-shape rationale.Engine::change_password(&old, &new, new_kdf) -> Result<(), OpenError>— delegates toWalletFile::rotate_password, mappingWalletEnvelopeError::InvalidPasswordOrCorrupttoOpenError::IncorrectPassword. Available on every signer kind (FULL / ViewOnly / HardwareOffload / multisig) since the underlying envelope rewrap is capability-agnostic.Engine::close(self, &credentials) -> Result<(), OpenError>— refuses withOpenError::OutstandingPendingTx { count }whenoutstanding_pending_txs() > 0(drives cross-cutting lock 4's "no clean close while reservations are live" invariant). Otherwise saves state viaWalletFile::save_state, saves prefs viaWalletFile::save_prefs, and consumesself. The method's doc comment names the zeroization chain explicitly:WalletFile::Dropreleases the advisory lock on<base>.keys;AllKeysBlob::Dropzeroizesspend_sk/view_sk/ml_kem_dkand the public-key fields. The chain is single-level (Engine<S>.keys: AllKeysBlobdirectly, no wrapper), and the underlyingDropsemantics are tested inshekyl-crypto-pq's own unit tests.
Eleven unit tests cover the round-trip create / open path, password rotation followed by reopen-with-new-password and refusal of the old,
OpenError::IncorrectPassword,OpenError::NetworkMismatch, theRestored { from_height }lost-state path (state file deleted between create and open),OpenError::OutstandingPendingTx(close refused while a synthetic reservation is inEngine::reservations), the structuredtracing::warn!on prefs HMAC tamper events, and the typedOpenError::CapabilityNotYetImplementedreturns from the view-only and hardware-offload stubs. Theapply_scan_result_post_open_workslifecycle ↔ scan-result composition test is deferred to the Phase 2arefreshcommit where it can exercise a realScanResultagainst the lifecycle'sLedgerIndexes::rebuild_from_ledgeroutput.The lifecycle commit ships
tracing = "0.1"as a runtime dependency onshekyl-engine-core(used for the prefs-tamper warn log only) andtempfile = "3"plustokio = { version = "1", features = ["macros", "rt"] }as dev-dependencies (lifecycle tests construct on-disk fixtures and instantiate aSimpleRequestRpcagainst an unreachable URL for the dummyDaemonClient). -
Engine::build_pending_tx/submit_pending_tx/discard_pending_txthree-methodPendingTxlifecycle (Phase 1pending_txtask). The newshekyl_engine_core::engine::pendingmodule lands the runtime-only side of cross-cutting lock 4. Public surface:PendingTx { id, built_at_height, built_at_tip_hash, fee_atomic_units, tx_bytes, recipients }— the chain-state-tagged handle returned bybuild_pending_tx.tx_bytesisVec::new()in Phase 1 and is explicitly documented as Phase-2a's integration point forshekyl-tx-builder.TxRequest { recipients, priority, from_subaddress },TxRecipient { address, amount_atomic_units },FeePriority { Economy, Standard, Priority, Custom(NonZeroU64) },TxRecipientSummary,ReservationId(u64),TxHash([u8; 32])— the strongly-typed input/handle/summary newtypes.Engine::build_pending_tx(&request) -> Result<PendingTx, SendError>— selects largest-amount-first spendable outputs fromLedgerIndexes/LedgerBlock(excluding outputs already reserved by another in-flightPendingTx), captures real chain state (synced_height+block_hash_at(synced_height)), bumps a monotonicnext_reservation_id, and inserts aReservationintoEngine::reservations. Phase 1 uses a fixedSTUB_FEE_ATOMIC_UNITS = 1_000stub fee; Phase 2a will replace it with adaemon.get_fee_estimates()call.Engine::submit_pending_tx(id) -> Result<TxHash, PendingTxError>— runs the cross-cutting-lock-4 invariants (PendingTxError::TooOld { built, current, max_reorg }againstNetworkSafetyConstants::for_network(network).max_reorg_depth,PendingTxError::ChainStateChanged { height }against the storedbuilt_at_tip_hash,PendingTxError::UnknownHandlefor unknownids), and on success removes the reservation, marks each selectedTransferDetailsasspent = truewithspent_height = None(the "unconfirmed-spent" Phase-1 state, made proper in Phase 2a once daemon broadcast confirmation arrives), and returns a stubTxHashwhose first 8 bytes encode theReservationId.Engine::discard_pending_tx(id) -> Result<(), PendingTxError>— idempotent: returnsOk(())regardless of whetheridis currently recognized, releases the reservation entry so the referenced outputs become selectable by a subsequent build.Engine::outstanding_pending_txs() -> usize— count accessor used byEngine::close(lifecycle commit) to refuse closing while any reservation is active.
Reservations live exclusively on
Engine<S>as a runtime-onlyBTreeMap<ReservationId, Reservation>field alongside the existing runtime-onlyindexes: LedgerIndexes. They are not persisted inWalletLedger.bookkeeping;BOOKKEEPING_BLOCK_VERSIONdoes not change. Process crash between build and submit/discard drops reservations along with the in-memoryPendingTxhandle — which is the correct behavior, since the tx never broadcast and the outputs are correctly spendable again on next open.The full lifecycle body is exposed as
pub(crate)free helpers (build_pending_tx_in_state,submit_pending_tx_in_state,discard_pending_tx_in_state) operating on(&LedgerBlock, &mut BTreeMap<ReservationId, Reservation>, ...)so unit tests can drive the full lifecycle without standing up anEngine<S>(whose constructors land in the lifecycle commit). Twelve unit tests cover output reservation, the reserved-output filter, insufficient-funds, the no-block-yetSendError::CannotSign, all threePendingTxErrorpaths, the spent-state mutation on submit, the rebuild-after-discard path, discard idempotency on unknown handles, andFeePriority::Custompreservation.See
docs/V3_WALLET_DECISION_LOG.md"Reservation tracker: runtime-only onWallet, never persisted" (2026-04-26 sub-section of theWallet<S>struct entry) for the runtime-vs-persisted decision and the supersession of the original cross-cutting-lock-4 draft phrasing. -
shekyl_engine_core::scan::ScanResulttyped scanner-output value andEngine::apply_scan_resultmerge surface (Phase 1scan_resulttask). A newshekyl_engine_core::scanmodule defines the additive event vocabulary the Phase 2aEngine::refresh()pipeline produces from a scanner pass:ScanResult { processed_height_range, parent_hash, block_hashes, new_transfers, spent_key_images, stake_events, reorg_rewind }.DetectedTransfer { block_height, output: RecoveredWalletOutput }— the secret-bearing variant;RecoveredWalletOutputalreadyZeroizeOnDrop, so dropping the enclosingScanResultwipes PQC re-derivation material in place.KeyImageObserved { block_height, key_image }— drivesLedgerIndexes::detect_spendsper height.StakeEvent::Accrual { height, record },#[non_exhaustive]so Phase 2bStakeInstancevariants can land additively.ReorgRewind { fork_height }— drivesLedgerIndexes::handle_reorgbefore per-height events.ScanResult::empty_at(start, parent_hash)for the nothing-changed-at-tip case and tests.
The companion
Engine::apply_scan_result(&mut self, ScanResult) -> Result<(), RefreshError>lives inengine::mergeand is the only audited code path that mutates the scanner-derived slice ofWalletLedgerplusLedgerIndexesduring refresh. It enforces two snapshot-consistency invariants before applying any events, rejecting withRefreshError::ConcurrentMutationon either failure:- Start-height equality.
processed_height_range.startmust equalsynced_height + 1(orfork_heightwhenreorg_rewindis present, since the rewind setssynced_heighttofork_height - 1first). - Parent-hash chain.
parent_hashmust matchLedgerBlock::block_hash_at(start - 1), withNonematchingNoneat genesis (start == 1).
The merge runs in a fixed order: optional reorg rewind first, then per-height ingest (
process_scanned_outputs+detect_spends) driven byblock_hashessosynced_heightadvances exactly once per scanned block — even when the block had no events — then staker-pool aggregate events.Engine<S>now carriesindexes: LedgerIndexesas a direct field so the merge can mutate both the persistedLedgerBlock(viaWalletLedger.ledger) and the runtime indexes under a single&mut selfborrow without needing an inner lock. The full merge body is exposedpub(crate)asapply_scan_result_to_state(&mut LedgerBlock, &mut LedgerIndexes, ScanResult)so tests can drive it without standing up a fullEngine<S>(whose lifecycle methods land in a follow-up commit).See
docs/V3_WALLET_DECISION_LOG.md"ScanResulttype" (2026-04-25, crate location:shekyl-engine-core::scan) and "Wallet::apply_scan_resultinvariants and Wallet-sideLedgerIndexes" (2026-04-26).
Changed
-
RuntimeWalletStatefolded intoLedgerBlock+LedgerIndexes(Phase 1runtime_state_audittask). TheRuntimeWalletStatetype and the transitionalpub use ... as WalletStatere-export are deleted. Its responsibilities split along the persistence boundary:- Persisted, on-disk state —
transfers,synced_height,reorg_blocks, claim watermarks — was already covered byWalletLedger.ledger(LedgerBlock). Read-only queries (height,transfers,unspent_transfers,staked_outputs,matured_staked_outputs,locked_staked_outputs,claimable_outputs,unstakeable_outputs,spendable_outputs,block_hash_at) and transfer-only mutators (set_staking_info,update_claim_watermark,freeze,thaw,transfer_mut) move to inherent methods onLedgerBlock. - Runtime-only derived state — the
key_imagesandpub_keyslookup maps plus thestaker_poolaccrual aggregate — moves to a newpub struct LedgerIndexesinrust/shekyl-engine-state/src/ledger_indexes.rs.LedgerIndexesis never serialized, has noSerialize/Deserializederives, and is rebuilt by scanner replay at every wallet open viaLedgerIndexes::rebuild_from_ledger. Cross-cutting mutations (ingest_block,mark_spent,unmark_spent,detect_spends,set_key_image,freeze_by_key_image,thaw_by_key_image,handle_reorg,insert_accrual) take&mut self, ledger: &mut LedgerBlock, …so a single call updates ledger and indexes atomically. Invariant:LedgerIndexesis reconstructible fromLedgerBlockplus daemon block replay; this is enforced by convention (struct doc-comment) rather than by the type system.
Live wallet state behind a single mutex is the tuple
pub type LiveLedger = (LedgerBlock, LedgerIndexes)in bothshekyl-engine-rpc::scanner_stateand the (cfgrust-scanner)shekyl-scanner::syncbackground loop. Scanner-specific behavior that needsTimelocked/RecoveredWalletOutput/BalanceSummary/ClaimableInfolives in extension traits inshekyl-scanner::ledger_ext(TransferDetailsExt,LedgerIndexesExt,LedgerBlockExt); the canonicalshekyl-engine-statecrate stays scanner-free. The oldshekyl-scanner::runtime_extandshekyl-scanner::wallet_statemodules are deleted.See
docs/V3_WALLET_DECISION_LOG.md"RuntimeWalletStateaudit: full fold, derived indexes rebuilt at open" (2026-04-25); the same commit also corrects two errata in that entry: the persisted transfer path isWalletLedger.ledger.transfers(notbookkeeping.transfers), andstaker_pool's home onLedgerIndexesis now pinned explicitly. - Persisted, on-disk state —
Documentation
-
Performance baseline document restructured for per-bench frozen baselines + §3.3.1 spec amendment + responsibility- allocation and toolchain-bump policies (Stage 0 PR-B).
docs/PERFORMANCE_BASELINE.mdis rewritten from the Round 4b template stub into the per-bench frozen-baseline shape thatdocs/design/STAGE_0_HARNESS.md§4.5 operationalizes (one populated section forengine_trait_bench_ledger_synced_heightfrozen at Stage 0 PR-2's merge SHA; four deferred-bench placeholder sections forengine_trait_bench_ledger_balance,engine_trait_bench_economics_current_emission,engine_trait_bench_economics_parameters_snapshot, andengine_trait_bench_key_account_public_address, each pinned to its introducing per-trait PR per §4.6's per-bench deferred assignment). The new document shape carries: per-bench frozen-baseline source (introducing PR + merge SHA), workload class (per §4.2 hoisting rule), iai-callgrind gate metric (instructions) isolated in its own table from the hardware-dependent informational rows (l1_hits,ll_hits,ram_hits,total_read_write,estimated_cycles), criterion metrics (median_ns,std_dev_ns) with hoisting-rule note, capture-environment cross-reference (env-<short-SHA>), and a cumulative-delta table with one row representing the introducing capture itself. The threshold-of-concern disposition is restated to apply per-bench (cumulative deltas do not sum across benches) and to the iai-instructions gate metric only (criterionmedian_nsis informational and does not gate). Two new policy sections close gaps surfaced during PR-B drafting: responsibility allocation pins that the PR which pushes cumulative delta past 10% (warn) or 25% (fail) is responsible for the breach regardless of its own per-PR contribution size (closes the slow-bleed failure mode where N PRs each at +9% cumulatively breach +25%); toolchain-bump policy pins that rustc / valgrind / iai-callgrind-runner version changes during Stage 1 trigger a per-bench rebaseline (re-capture each in-scope bench at its introducing PR's tree state under the new toolchain; reset the cumulative-delta column; CHANGELOG entry; the rebaseline commit is itself a non-Stage-1 change and does not count toward any bench's cumulative-delta column). A new in-tree reference capture (docs/benchmarks/reference-captures/stage-0-pr-2-c4c-shekyl_rust_v0.json, with explanatory README) supports PR-B's review-surface verification gate against a stable in-tree artifact rather than a transient GHA artifact path.docs/V3_ENGINE_TRAIT_BOUNDARIES.md§3.3.1 Component 1 is amended to match: replaces the single-SHA / "first Stage 1 PR" / "cumulative-is-sum" framing with per-bench introducing-PR-merge-SHA framing, per-bench cumulative-delta independence, and a §4.5 back-pointer for operational details. The amendment bundles with thePERFORMANCE_BASELINE.mdrewrite per the bundling exception codified in §4.6 of the design doc (correction of existing wrong text, fully derived from already-merged design content, ~27 lines within an existing ~36-line component — above the ~15-line soft anchor but below the 50-line "structural rewrite" cutoff, with content qualifying as mechanical-derivation rather than re-framing per the codification's allowance). Numbers and in-tree iai-callgrind snapshot refresh are deferred to Stage 0 PR-2 commit 5 per the framing-vs-numbers split.FOLLOWUPS.md§"V3.0" gets two updates: the existing Stage 1 baseline-measurement row is rewritten to the per-bench framing (replacing the single-SHA / 30-day-tip language with the four-deferred-benches close-condition); a new row tracks the CHANGELOG-backfill discipline gap surfaced during PR-B (PR-A3d313256c, PR-A-extension2e5309ad3, and PR-C93d515123merged without## [Unreleased] / ### Documentationentries). The CHANGELOG-backfill row is targeted at V3.0 and can land any time before V3.0 cut. -
engine_trait_bench_ledger_synced_heightfrozen baseline transcribed (Stage 0 PR-2 commit 5). The validated CI capture values (iaiinstructions=10, hardware-dependent informational rowsl1_hits=16/ll_hits=0/ram_hits=2/total_read_write=18/estimated_cycles=86, criterionmedian_ns=0.6221/std_dev_ns=0.005864) are recorded indocs/PERFORMANCE_BASELINE.mdunder the bench's frozen-baseline source, gate metric, informational metric, and cumulative-delta tables. Theenv-0276d210capture environment is populated with the toolchain (rustc 1.95.0/cargo 1.95.0/valgrind-3.22.0/iai-callgrind-runner 0.16.1) and runner state (AMD EPYC 7763/Linux 6.17.0-1010-azure) from the GHAworkflow_dispatchrun25239954863, one of the three N=3 invariance-verification captures (runs25239954863,25239956447,25239958016) that produced byte-identical iai-callgrind output (±0% variance on the gate metric perSTAGE_0_HARNESS.md§4.4 dynamic check). The bench's "frozen at" SHA is the capture SHA0276d210e(PR-2 commit 4c, post-QBox<Engine<S>>fixture); the in-treereference-captures/stage-0-pr-2-c4c-shekyl_rust_v0.jsonremains the stable artifact citation. The four deferred bench sections (balance,current_emission,parameters_snapshot,account_public_address) are unchanged — each will be populated by its introducing per-trait PR per §4.6's per-bench deferred assignment. Closes Stage 0 PR-2's measurement work. -
Stage 1 trait-boundaries spec, Round 1 draft (
docs/V3_ENGINE_TRAIT_BOUNDARIES.md). First draft of the Stage 1 design document called for by the decision-log entry "Engine architecture: actor model with staged migration from composition" (2026-04-27) and thephase_2b_prep_stage_1_trait_boundariesplan. Pins six trait surfaces (KeyEngine,LedgerEngine,RefreshEngine,PendingTxEngine,DaemonEngine,PersistenceEngine), the composition shape (Engine<S, K, L, R, P, D, F>with default type parameters; concrete fields, generic-bounded methods, noBox<dyn>), the per-trait async story, the per-trait error model (per-trait families with a single sharedEngineErroraggregate), the test boundary unlocked byMockKeyEngine/MockDaemonEngine/ etc. (closes today's gap that there is no way to plugMockRpcintostart_refreshend-to-end), the Stage 4 transition guarantee (the trait surface in §2 does not change at Stage 4;kameoactors implement the same traits with the same signatures), the Stage 1 migration order (DaemonEnginefirst to unlock integration tests;LedgerEnginesecond; the other four in any reviewer-convenient order), and a consolidated 15-item open-questions list as the Round 2 agenda. Markdown-only; no code changes. Per.cursor/rules/20-rust-vs-cpp-policy.mdc, the document runs through 4–6 review rounds againstdevbefore any Rust lands. Round 1 draft only — open questions are written down with tentative answers, not closed. -
Engine binary boundary pinned as pure message-passing (decision log "Engine binary boundary: pure message-passing over shared handle", 2026-04-27). The post-Stage-4 binary boundary in
shekyl-engine-rpcis settled asHashMap<EngineId, ActorRef<EngineActor>>, notArc<RwLock<Engine>>. Per-engine concurrency control is thekameomailbox; the registry holds actor handles directly. The new entry documents the rationale (Shape B retired the synchronous-blocking caller; actors handle concurrency internally; kameo's API targets the wrapper-free model), the three honest costs (test ergonomics, re-entrancy discipline, pure-CPU operations on the actor-dispatch path), and the resolutions (free-function vs message boundary criterion; cross-leaf immutable-data construction-time pattern with an enumerated immutable-fields list; no-cycle DAG topology; kameo-specific constraints including issue #306 forward-chain avoidance and bounded mailboxes). The same commit amends the prior 2026-04-27 "Engine architecture: actor model with staged migration from composition" entry: the RPC boundary paragraph gains anUpdate (2026-04-27):supersession block, and Stage 4's description picks up the wrapper removal and the no-cycle-DAG / kameo-constraints / cross-leaf-immutable-data implementation requirements. A FOLLOWUPS entry under V3.0 gates Stage 2 onkameo >= 0.20.0version pin, MSRV>= 1.88verification, and a workspace-wide bounded-mailbox default. -
Phase 1 sub-decision log entries appended (Phase 1
decision_log_entriestask). Three new dated entries land indocs/V3_WALLET_DECISION_LOG.mdto lock the Phase 1 surface decisions whose defaults were taken from the Phase 0surface_decisionsreview:- "
RuntimeWalletStateaudit: full fold, derived indexes rebuilt at open" —RuntimeWalletStateceases to exist;key_images/pub_keysindexes promote into apub(crate) LedgerIndexesowned byWallet, rebuilt from the authoritative ledger at open time, never persisted. Schema unchanged. Closes theruntime_state_auditPhase 1 task and thepub use ... as WalletStatetransitional alias deletion. - "
tx_keysstorage: persist inTxMetaBlock, never re-derived" — pins the rule that per-tx randomness lives inTxMetaBlock::tx_keys: BTreeMap<TxHash, TxSecretKeys>(already shipped in schema), is never reconstructed from any other state, and thatEngine::tx_proof/Engine::reserve_proof(Phase 2) read it bytxidlookup with a typedProofError::TxKeyNotPersistedon miss. - "Daemon-side
tracinginstall:shekyl_log_install_tracing_forwarderundershekyl-logging::ffi" — locks the FFI export name, signature (pub unsafe extern "C" fn() -> i32, idempotent, returns typedALREADY_INSTALLED/NOT_INITIALIZED), home (shekyl-logging::ffi, notshekyl-daemon-rpc::ffi), and the rule thatshekyl-daemon-rpc'stracing::*call sites are kept verbatim — the forwarder routes them throughshekyl-loggingautomatically. Closes thedocs/FOLLOWUPS.mdV3.2 entry "shekyl-daemon-rpcstaticlib:tracing::*calls silently dropped" by absorption into the Phase 1 logging deliverable.
No code changes ship in this entry; each decision is realized by a subsequent Phase 1 commit (the
RuntimeWalletStatefold is the next task in line per the todo list). - "
-
Engine rename, actor-architecture, and pending-tx protocol decision-log entries appended (2026-04-27). Three new dated entries land in
docs/V3_WALLET_DECISION_LOG.mdto pin major Phase-2-and-beyond architectural commitments whose rationale must be in tree before the supporting code commits land:- "
Wallet<S>renamed toEngine<S>: privacy-correct framing for the local artifact" — pins the renaming of the orchestrator type, all related types, all crate paths (shekyl-wallet-core→shekyl-engine-core,shekyl-wallet-file→shekyl-engine-file,shekyl-wallet-state→shekyl-engine-state,shekyl-wallet-rpc→shekyl-engine-rpc,shekyl-wallet-prefs→shekyl-engine-prefs), JSON-RPC method strings (wallet_*→engine_*), CLI subcommand names, file paths (~/.shekyl/wallets/→~/.shekyl/engines/), and CLI user-facing language ("engine" used consistently in CLI help text). GUI/mobile user-facing language stays a separate marketing decision deferred to post-V3 user- interaction testing. Domain-primitive crates (shekyl-shard-visual) and binary/product crates remain as- is. The decision is realized by the immediately-following mechanical rename commit onshekyl-coredev. - "Engine architecture: actor model with staged migration from
composition" — pins the migration of
Engine<S>from composition to an actor model withkameoas the framework, over five staged actor builds plus a Stage 1 framework- agnostic preparation pass. Stage 2 introduceskameoand buildsKeyEnginefirst (smallest internal state, cleanest privacy boundary, framework-friction surfaces with bounded blast radius). Stage 3 buildsStakeEnginenative-as-actor in Phase 2b for consensus-bond responsibilities only. Stage 4 migrates remaining subsystems (DaemonEngine,PersistenceEngine,PendingTxEngine,RefreshEngine,LedgerEngine) one at a time. Stage 5 (V3.x, simulation- gated) buildsArchivalEngineas a sibling toStakeEngine(not a child) for slashing-domain integrity, failure isolation, and the Hayekian shard-market property. The entry pins the locked stage sequence end-to-end, the framework choice (kameo), the privacy benefits realized (view-key vs spend-key separation across actors becomes enforceable), the horizontal-scaling benefits enabled (V4+, stateless actor pools), and the long-tier staker upgradability shape (V5+, signed actor-patch distribution; V3 and V4 use restart-based upgrades). The entry rejects the alternatives explicitly: pure composition (privacy weaker), Stage-1-as-kameo(premature framework lock-in), single-cutover migration (review-undeliverable),ArchivalEngine-as-child-of-StakeEngine(slashing-domain integrity violation). - "Pending-tx protocol: two-phase build/submit/discard over
single-phase callback" — pins the canonical transaction-
sending API as the two-phase pending-transaction protocol
(
build/submit/discard, withinspect,adjust_fee,sign_partial,aggregate_signatures,exportas additional pending-tx operations). The single- phasesend(request, confirm_fn) -> Result<TxHash>callback model is rejected. Rationale: explicit lifecycle for multisig and air-gapped signing flows, RPC-friendly across the JSON-RPC boundary, fee-adjustment without rebuild, audit/inspect surface, recovery from partial failure.
Companion
docs/FOLLOWUPS.mdupdates land in the same commit:- V3.0 — Stage 2
KeyEnginemigration; Stage 3StakeEnginenative build; Stage 4 remaining-subsystem migrations (DaemonEngine,PersistenceEngine,PendingTxEngine,RefreshEngine,LedgerEnginein suggested order); RPC boundary refinements (idle eviction with TBD-at-implementation rationale,engine_lockJSON-RPC method, multi-engine registry, snapshot reads fromLedgerEngine, multi-peer archival routing client surface). - V3.1 — sibling resolution entry for the
assemble_tree_path_for_outputbug, locking the resolution architecture (foundation--no-prunearchival as floor; staker-distributed archival viaArchivalEngineas primary path; multi-peer routing against per-block root snapshots). The original bug entry is preserved untouched as historical record. - V3.x — Stage 5
ArchivalEnginenative build (simulation- gated); no-tradeability invariant codification placeholder cross-referencingdocs/V3_SHARD_VISUALIZATION.mdanddocs/V3_STAKER_ARCHIVAL.md. - V4+ — horizontal scaling via stateless actor pools.
- V5+ — signed actor-patch distribution over staker P2P.
The 2026-04-25 "Locking discipline:
RwLock<Wallet>overRefCell/ sharded locks / actor model" sub-section receives a one-line forward-pointer noting that it is partially superseded by the new actor-architecture entry from Stage 2 onward; lock- discipline reasoning still applies during Phase 2b composition.This commit is documentation-only. No code, schema, or protocol surface changes here. The mechanical rename commit ships separately as the immediately-following commit on
shekyl-coredev; Stage 1 and beyond ship over subsequent PRs per the locked stage sequence in the actor-architecture decision-log entry. - "
-
docs/V3_STAKER_ARCHIVAL.mdanddocs/V3_SHARD_VISUALIZATION.mdadded undershekyl-core/docs/(relocated and rescoped fromshekyl-dev/docs/V4_*). Two design documents covering the staker-distributed chain-history archival mechanism and the deterministic shard visualization surface relocate from theshekyl-devplanning workspace to theshekyl-corecanonical documentation tree, content-checked to reflect their V3 ship scope rather than the V4 ship scope they originally drafted against. Status blocks at the top of each document pin the new ship target and reference the 2026-04-27 actor-architecture decision-log entry that establishedArchivalEngineas a sibling toStakeEngineandshekyl-shard-visualas a domain-primitive library crate. The earlierdocs/V4_STAKER_ARCHIVAL.mdanddocs/V4_SHARD_VISUALIZATION.mdcopies inshekyl-core/docs/(added in commit 9dc44687d) are removed in this commit; the V3-named documents are the canonical homes going forward. The companiongit rmof the V4-named drafts fromshekyl-dev/docs/ships as a separate commit onshekyl-devdevthat references this commit's shekyl-core SHA. -
Phase 2b prep — Track 1 audit-hygiene pass (2026-04-28). Five small editorial / re-export commits close the loose ends surfaced by the Phase 2a Branch 2 audit before Stage 1 spec work begins. None of the five touch consensus, secret-handling, persisted format, or wire format; they are pure plumbing / docs / re-exports.
-
shekyl-engine-corecrate-root re-exports forRefresh*types.rust/shekyl-engine-core/src/lib.rsnow re-exportsRefreshHandle,RefreshOptions,RefreshPhase,RefreshProgress,RefreshReorgEvent, andRefreshSummaryalongside theRefreshErrorit already re-exported. Downstream callers (CLI, JSON-RPC server, benches, FFI) no longer have to reach throughengine::refresh::*. Theenginemodule itself already re-exported the full set (engine/mod.rs:168–170). -
CHANGELOG
[Unreleased]editorial sweep —Wallet→Enginerunning prose. Phase 1 and Phase 2a Branch 1 bullets (lifecycle, pending-tx, scan-result, refresh-driver, struct, module-skeleton) carriedWallet<S>/Wallet::*/OpenedWallet/WalletSignerKind/WalletCreateParams/shekyl_engine_core::wallet::*references that pre-dated the 2026-04-27 rename bullet at the top of[Unreleased]. The sweep normalizes the running prose. Decision-log title citations and the rename bullet's mapping enumeration are intentionally preserved verbatim — the citeddocs/V3_WALLET_DECISION_LOG.mdentries still carry their historical titles. -
docs/FOLLOWUPS.mdV3.1 row added —transfer_detailsRust migration..cursor/rules/15-deletion-and-debt.mdccitestransfer_detailsRust migration as V3.1 scope; the row now exists. Rewrites each C++ consumer ofstruct transfer_details(balance, output selection, key-image / spend tracking, payment-id surface, password rotation, persistent wallet-cache I/O) to driveshekyl-engine-state::TransferDetailsthrough FFI, then deletes the C++ struct fromsrc/wallet/wallet2.handsrc/wallet/wallet_rpc_server_commands_defs.h. Closes either at V3.1 or by superseding deletion in the V3.2wallet2.cppretirement. -
docs/FOLLOWUPS.mdV3.2 row added — "Re-examine/FIiso646.handrct::→ct::deferrals." Reconciles a dead citation indocs/STRUCTURAL_TODO.md:17–18, 37–38. Both deferrals rest on the same upstream-cherry-pick-risk framing the STRUCTURAL_TODO calls "largely notional"; the V3.2 row pins per-item disposition rules (/FIiso646.h:/permissive-vs. mechanical replacement vs. stay-on-workaround;rct::→ct::: confirm or compress the V4 target). -
Engine::refreshcancellation contract pinned in the docstring atrust/shekyl-engine-core/src/engine/refresh.rs(lines 1815–1827 in the pre-edit revision). Sync path stays cancel-internal (the token is created fresh per call and never fires); async path (Engine::start_refreshreturningRefreshHandle) owns cooperative cancellation. The split is deliberate, not a TBD: threading a token through every sync caller is design churn for no win, and the async surface already exists for callers that need shutdown.
Audit reference:
.cursor/plans/phase_2b_prep_stage_1_trait_boundaries_0d37a30e.plan.mdTrack B items 1–5. Track 2 (Stage 1 trait-boundaries spec, V3.2) begins after this hygiene pass lands. -
Removed
-
shekyl-scanner::syncmodule andshekyl-scanner::rust-scannerCargo feature retired (Phase 2arefresh_scan_loopbundle, Branch 1). The standalone background-sync surface (run_sync_loop,LiveLedger,SyncProgress,SyncError) and its feature flag are deleted in favor of theshekyl-engine-core::Engine::refreshdriver.shekyl-scannerbecomes a pure scanning library —Scanner, extra-field parsing, KEM rederivation, theLedgerBlock/LedgerIndexesextension traits, balance, and coin selection — and drops itstokio/tokio-utiloptional dependencies along with the feature.shekyl-engine-rpc::rust-scanneris not affected by this change; that feature gates a JSON-RPC-side(LedgerBlock, LedgerIndexes)cache (scanner_state::LiveLedger, a local type alias unrelated to the deleted scanner-side alias) which retires in Phase 4b alongsideshekyl-engine-rpc's Rust cutover. Seedocs/V3_WALLET_DECISION_LOG.md"Retireshekyl-scanner::sync::run_sync_loop(Phase 2a/4b boundary)" (2026-04-27) for the rationale and Phase boundary. Thesync_bookkeepingtest module inshekyl-scanneris retained: it exercises the(LedgerBlock, LedgerIndexes)state-management primitives (progress monotonicity, reorg handling, spend-detection tracking) that the producer side ofEngine::refreshnow drives, and remains load-bearing regardless of who owns the outer loop. -
rust/shekyl-ffi/src/wallet_ledger_ffi.rsdeleted as a Phase 5 pre-emption. The typed cache-handle FFI surface from sub-commit 2l.a —ShekylTransferDetailsC/ShekylBlockchainTipC/ShekylReorgBlockEntryC/ShekylSubaddressRegistryEntryC/ShekylSubaddressLabelEntryC/ShekylAddressBookEntryC/ShekylTxKeyEntryC/ShekylTxNoteEntryC/ShekylTxAttributeEntryC/ShekylScannedPoolTxEntryC/ShekylSyncStateScalarsCand theirshekyl_wallet_{get,set,free}_*trios plusshekyl_wallet_ledger_preflight— is gone. The corresponding declarations insrc/shekyl/shekyl_ffi.hare stripped; the reservedSHEKYL_WALLET_ERR_BLOCK_NOT_HYDRATED(codepoint 29) retires alongside the surface that produced it.save_as(the in-scope C-ABI export fromwallet_file_ffi.rs) and its refusal codes (SAVE_AS_CROSS_FILESYSTEM/SAVE_AS_TARGET_EXISTS) remain unchanged. Theshekyl-primitivesmain- and dev-dep are also removed fromrust/shekyl-ffi/Cargo.toml; the only consumer was the deleted file'sCommitmentreconstruction path.Caller evidence (commit message body). Pre-flight
git grepagainst*.cpp/*.cc/*.h/*.hppfor every export of the deleted surface returned onlysrc/shekyl/shekyl_ffi.hitself (the prototypes that this commit removes). Zero.cppconsumers ever materialized — the original consumer (wallet2_handle_views.h/.cpp) was scheduled but never written, and Phase 5 will delete the enclosingwallet2.cppshim wholesale. Fullgit greptranscript pinned in the deletion commit's message body for reproducibility.Decision rule. This deletion establishes the Phase 5 pre-emption rule in
docs/V3_WALLET_DECISION_LOG.md: an individual Phase 5 inventory item may be deleted early when (1) zero current.cppcallers, (2) grep evidence in the deletion commit's message body, and (3) atomic update ofdocs/FOLLOWUPS.md/ Phase-5-inventory metadata in the same commit. Pre-empting items with surviving callers is not acceptable. The Decision Log entry locks the rule so future pre-emptions follow a precedent rather than an ad-hoc precedent.Inventory hygiene.
docs/FOLLOWUPS.md(sub-bullet "Phase 5 inventory pre-emptions" under the wallet2.cpp absorption entry) records this file as already pre-empted; the eventual Phase 5 commit's deletion list excludes it. The pre-existing clippy lints in the now-deleted file (as u8casts, explicititer()loop,_keep_importsarg count) close by absorption — the file holding them no longer exists.
Changed
-
Subaddress namespace flattened to
SubaddressIndex(u32)across the wallet stack and the typed-ledger FFI surface (Phase 1 of the shekyl-v3-wallet-rust-rewrite plan,primitivestask).SubaddressIndexis now au32newtype withindex == 0reserved for the primary address; the legacy{account, address}pair is gone everywhere —WalletLedger,BookkeepingBlock::subaddress_registry/subaddress_labels.per_index, scanner outputs, transfer records,RuntimeWalletState::filter, and the typed-ledger FFI inrust/shekyl-ffi/src/wallet_ledger_ffi.rs. Account-level concepts inherited from wallet2 (AccountTags, thetag_descriptions/account_tagsFFI trios) are removed wholesale; the Decision Log entry "Subaddress hierarchy: flat, no account level" pins the rationale (most users use one account; account-level tags were wallet2 baggage; multi-wallet-file isolation is genuinely stronger than account-level subaddresses). A separateSubaddressLabels::primaryslot is gone too — the primary label is theindex == 0entry ofper_indexlike every other label.FFI surface delta (this commit).
shekyl_ffi.hmirrors the Rust:ShekylSubaddressRegistryEntryCandShekylSubaddressLabelEntryCcarry a singleindex: u32field (sizes 36 and 24 respectively, no trailing pad — there are zero.cppcallers in tree, so preserving the legacy stride for hypothetical future callers would be a defensive measure for nobody); theShekylTagDescriptionEntryC/ShekylAccountTagAssignmentEntryCtypedefs and theirstatic_asserts, plus theshekyl_wallet_{get,set,free}_{tag_descriptions,account_tags,primary_label}prototypes, are removed. The FFI filewallet_ledger_ffi.rsitself is scheduled for outright deletion in the immediate follow-up commit (Phase 5 pre-emption); this commit lands the field-rename half of the migration so the deletion commit is a one-concern review.Behavioral delta.
shekyl_wallet_set_subaddress_registrynow rejects an entry withindex == 0by returningSHEKYL_WALLET_ERR_LEDGER. The primary address is reconstructed from the wallet keys at every load and is not registry-managed; an attempted insert at index 0 is structurally impossible rather than benign overwrite. wallet2 silently accepted such inserts; the V3 surface fails loudly. Belt-and-suspenders unit testwallet_ledger_ffi::tests::registry_set_rejects_index_zeropins the contract.On-disk schema. All three persisted-block version constants are bumped from
1to2:BOOKKEEPING_BLOCK_VERSION(the direct field-shape changes —subaddress_registry/subaddress_labels.per_indexflatten andaccount_tagsremoval),LEDGER_BLOCK_VERSION(transitive — everyTransferDetailsinLedgerBlock::transfersnow carries the flattened newtype), andWALLET_LEDGER_FORMAT_VERSION(transitive — the bundle's serialized bytes shift wherever any nestedSubaddressIndexorSubaddressLabelsappears). The strict pairing of "snap drift ↔ paired version-constant bump" is enforced by theci/schema-snapshotworkflow perdocs/MID_REWIRE_HARDENING.md§3.4 and.cursor/rules/42-serialization-policy.mdc; the gate caught the original commit shipping only the bookkeeping bump, and the missing two were folded in atop the existing branch rather than rewriting history. Legacy v1 ledgers have no live readers — pre-V3 launch,rm -rf ~/.shekylis the migration path per.cursor/rules/15-deletion-and-debt.mdc. Thebookkeeping_block.snap/ledger_block.snap/wallet_ledger.snapschema fixtures are regenerated; theSubaddressIndexshape went from a two-field struct to aNewtypeStruct(u32), andBookkeepingBlock::account_tagsis gone.JSON shape factoring. Transfer records expose subaddress indices as
{"index": u32}(bare form, no label); address-list responses expose them as{"index": u32, "label": Option<String>}(joined form, label looked up at handler time). Decision Log entry "Subaddress JSON shapes: two schemas, no label join in transfer records" pins the factoring for Phase 4b OpenAPI work.
Added
-
shekyl-engine-core::Engine<S>struct +DaemonClientthin wrapper (Phase 1 of the shekyl-v3-wallet-rust-rewrite plan, cross-cutting locks 1, 3, 4 type-layer realization). Lands theEngine<S: EngineSignerKind>struct itself with its full dependency graph wired in:file: shekyl_engine_file::WalletFile,keys: shekyl_crypto_pq::account::AllKeysBlob,ledger: shekyl_engine_state::WalletLedger,prefs: shekyl_engine_prefs::WalletPrefs,daemon: DaemonClient,network: Network,capability: Capability, plus_signer: PhantomData<S>for compile-time signer-kind dispatch.networkandcapabilityare cached fromWalletFile's region 1 (which is write-once aftercreate) so the hot accessors are infallible and O(1). Read-only accessors (network(),capability(),file(),ledger(),prefs(),daemon()) plus apub(crate) keys()for in-crate sign / proof code paths. RedactedDebugimpl:keysprints as<redacted: AllKeysBlob>,ledger/prefsprint as<…>,fileanddaemondelegate to their own already-redacting impls. NoDropimpl onEngine<S>itself:AllKeysBlobandWalletFileeach ship their ownDropfor the secret bytes / KEK / advisory lock; composing types that already wipe correctly is sound, and a wrapperDropwould risk shadowing the inner ones. NewDaemonClientthin wrapper aroundshekyl_simple_request_rpc::SimpleRequestRpcinsulatesEngine's public API from the transport choice and gives Phase 2a a single audited site forget_infonetwork verification,get_fee_estimatesfee-priority resolution, and tx submission. The six lifecycle methods (create,open_full,open_view_only,open_hardware_offload,change_password,close),RefreshHandle,PendingTx, andScanResulteach land in their own follow-up commits on this same Phase 1 branch. Cargo dependency graph:shekyl-crypto-pqis now a non-optional dependency ofshekyl-engine-core(themultisigfeature flag previously gated it; withkeys: AllKeysBlobon the struct it is mandatory regardless of feature). Full rationale and field-by-field justification recorded indocs/V3_WALLET_DECISION_LOG.md§"Wallet<S>struct shape and accessor surface". -
shekyl-engine-core::enginemodule skeleton (Phase 1 of the shekyl-v3-wallet-rust-rewrite plan, cross-cutting locks 2, 4, 5, 6, 7, 8 type-layer realization). New modulerust/shekyl-engine-core/src/engine/ships the type-layer foundations of the V3 wallet orchestrator without yet introducing theEnginestruct itself: per-domain error enums (OpenError,RefreshError,SendError,PendingTxError,KeyError,IoError,TxError) with the plan-locked variants pinned by name (OpenError::NetworkMismatch,RefreshError::ConcurrentMutation,PendingTxError::TooOld,PendingTxError::ChainStateChanged,TxError::DaemonFeeUnreasonable, etc.); a re-export ofshekyl_address::Network(the fourthFakechainvariant lands in a separate scoped commit on the same branch); a re-export ofshekyl_engine_file::Capability(canonical spelling — the plan's "CapabilityMode" reference is satisfied); and a sealedEngineSignerKindtrait withSoloSignerZST as the V3.0 default. V3.1'sMultisigSigner<N, K>will join behind the existingmultisigCargo feature without changing call sites.#[from]impls for upstream errors (WalletFileError,CryptoError,WalletLedgerError, etc.) are deliberately deferred to the lifecycle / refresh / send commits that introduce the call sites needing them, so an#[from]impl never exists without a caller. Full rationale recorded indocs/V3_WALLET_DECISION_LOG.md§"Per-domainWalleterror enums + sealedWalletSignerKind". -
shekyl-engine-state::LocalLabelandSecretStr<'a>(Phase 1 of the shekyl-v3-wallet-rust-rewrite plan, cross-cutting lock 9 type-layer realization). Locally-sensitive UTF-8 wrappers for every user-supplied string the wallet persists but never transmits — address-book descriptions, subaddress labels, transaction notes.LocalLabelisZeroizing<String>with redactingDebug/Display("<redacted N bytes>"); no derivedSerialize/Deserialize. Persistence routes through the explicitserde_helpers::local_labeladapter, which is wire-byte-identical to a plainString(testserde_helpers::tests::local_label_postcard_wire_matches_plain_stringpins this), so the upcoming bookkeeping_block / tx_meta_block retypes will not bumpBOOKKEEPING_BLOCK_VERSIONorTX_META_BLOCK_VERSION. Borrowed in-process inspection goes throughLocalLabel::expose() -> SecretStr<'_>, whose onlyDisplay/Debugoutput is the redaction marker; callers that need raw bytes callSecretStr::as_str()explicitly so the call site is the audit point. Full rationale (including why the value-typedSecretStr<'a>shape rather than the literal&SecretStrshorthand from the decision log —unsafe_codeis forbidden workspace-wide) recorded indocs/V3_WALLET_DECISION_LOG.md§"LocalLabel/SecretStrtyping for locally-sensitive UTF-8".
Changed
-
monero-oxidevendor-bump87acb57→3933664(PR 0.6 of the shekyl-v3-wallet-rust-rewrite plan, closing Operation A of themonero-oxideun-pin question). Updatedrust/shekyl-oxide/UPSTREAM_MONERO_OXIDE_COMMITfrom87acb57e0c3935c8834c8a270bd3bdcbbe36bcde(sync_date 2026-04-06) to3933664d0851871c976f07298b862373d1c6fec0(sync_date 2026-04-25), the current Shekyl fork tip onShekyl-Foundation/monero-oxidefcmp++. No vendored source files changed. Of the five fork commits between the two pins, the only ones with code-content deltas (182b648Cargo profiles + base58 decoder hardening) touchedshekyl-oxide/wallet/base58/, a Monero-shaped wallet path that is not vendored in shekyl-core per60-no-monero-legacy.mdc— Shekyl uses native Bech32m viashekyl-addressinstead. The umbrellashekyl-oxide/Cargo.tomlis byte-identical between the vendored copy and fork tip;182b648's Cargo profile changes live in the fork's workspace-rootCargo.toml, which we do not vendor either. Workspace grep formonero_base58 | shekyl-oxide.*base58 | ::base58::returns zero matches acrossrust/, confirming thatshekyl-address(Bech32m via thebech32crate) and no other Shekyl crate imports the fork's base58 module. The hardening itself is strictly more restrictive —checked_addoverflow detection plus non-canonical-encoding rejection — so even a hypothetical downstream consumer would only see additionalNonereturns, never differentSome(_)payloads. Verification perdocs/SHEKYL_OXIDE_VENDORING.md:cd rust && cargo build --locked -p shekyl-fcmpclean,cd rust && cargo test --locked --workspace900 passed, 0 failed, 6 ignored (exit 0).ninja shekyldskipped because PR 0.6 does not touch the C++ side anddocs/SHEKYLD_PREREQUISITES.mdalready certifies the C++ daemon as ready. The.github/workflows/shekyl-oxide-divergence.ymlCI guard now compares against the new pin and reports zero divergence until the fork advances again. Operation B (40-commit fork ↔ upstream merge, including the cypherstackgeneralized-bulletproofs-fixaudit response and the VeridiseHelioseleneField::invertcorrectness cluster) remains a separate V3.1.x follow-up perdocs/FOLLOWUPS.md§ "V3.1+ — Legacy C++ → Rust rewrite scope" and is unaffected by this PR. Half-day review gate (PR 0.4 / 0.5 findings, FOLLOWUPS V3.1+ rewrite interactions, cross-cutting locks confirmation, un-merged-upstream impact on Phase 1 Wallet API shape) cleared cleanly before this PR; conclusions recorded indocs/V3_WALLET_DECISION_LOG.md. With PR 0.6 merged, Phase 0 of the V3 wallet rewrite is complete (six PRs for six PRs); Phase 1 (Wallet API + cross-cutting locks) is now unblocked. Audit docdocs/MONERO_OXIDE_VENDOR_STATUS.mdamended with a "PR 0.6 vendor-bump execution (2026-04-25)" section recording the metadata-only finding so future readers don't replay the base58-content review against vendored paths that don't have it. -
shekyl-engine-file::WalletFileHandle→WalletFile(PR 0.2 of the shekyl-v3-wallet-rust-rewrite plan). Mechanical rename across all call sites inshekyl-engine-file,shekyl-engine-prefs,shekyl-ffi, and the C FFI doc-comment insrc/shekyl/shekyl_ffi.h. No ABI change (the C-ABI symbols use theshekyl_wallet_*prefix, not the Rust type name). Frees theEngineidentifier for the Phase 1shekyl-engine-core::Engineorchestrator and aligns the file-orchestrator type name with what it actually is — envelope, atomic IO, advisory locking, payload framing. Rationale and decision archive indocs/V3_WALLET_DECISION_LOG.md("Wallet stack greenfield Rust rewrite", 2026-04-25).
Documentation
-
shekyldPhase 0 prerequisites audit (PR 0.3 of the shekyl-v3-wallet-rust-rewrite plan). New filedocs/SHEKYLD_PREREQUISITES.mdconsolidating the audit of three daemon-side prerequisites against the rewrite plan's later phases:- Instant-mining regtest mode (Phase 6 prereq):
PRESENT —
--regtest --offline --fixed-difficulty 1+generateblocksJSON-RPC works as inherited from Monero; V3-specific caveats documented (FCMP++ tx-type andcurve_tree_rootheader checks bypassed onFAKECHAIN, reference-block age rules still enforced). No daemon change required. get_fee_estimate(s)RPC (Phase 2a prereq): PRESENT as singularget_fee_estimatereturning a positional 4-elementfeesvector matching HF 2021-scaling tiers; no name-keyed buckets on the wire — priority-name binding is wallet-side. Decision-log entry adjusted: wallet supplies the names, daemon supplies the numbers. No daemon change required.- Fee policy / rules version exposure:
ABSENT entirely — no
fee_version/fee_policy_idonget_fee_estimate, onget_info, or as a separate RPC. Filed as a V3.1 daemon-side follow-up; not a Phase 0 blocker. The rewrite's Phase 2a builds a forward-compatible client that consumes the field gracefully if it appears later.
Phase 6 and Phase 2a unblocked against the existing daemon surface.
- Instant-mining regtest mode (Phase 6 prereq):
PRESENT —
-
monero-oxidevendor freshness audit (PR 0.4 of the V3 wallet rewrite plan,docs/MONERO_OXIDE_VENDOR_STATUS.md). Point-in-time (2026-04-25) record of where the vendoredshekyl-oxidesnapshot (87acb57e) sits relative to the Shekyl fork tip (Shekyl-Foundation/monero-oxidefcmp++3933664d, +5 commits, all non-crypto) and the original upstream (monero-oxide/monero-oxidefcmp++0e438ae, +40 commits since the 2025-11-22 merge base, including the cypherstackgeneralized-bulletproofs-fixaudit response, the VeridiseHelioseleneField::invertcorrectness cluster, and a major upstream restructure that the fork has not adopted). The doc is a freshness audit only — it does not re-vendor or un-pin. The actual un-pin / merge-from-upstream operation is a separate plan; this audit produces its input queue (substantive upstream commits the fork is missing) and baseline (the eight Shekyl-only fork commits, of which only416d8d1rename and87acb57extra leaf scalars are crypto-substantive). Audit lifecycle: append-only — refresh runs add a new dated section rather than editing in place, so the rewrite plan's Phase 0 record stays intelligible after the un-pin lands. -
Mid-rewire hardening plan (
docs/MID_REWIRE_HARDENING.md) amended in §3.1 and §4.3. §3.1 updated to reflect the architecturally honest scope for the C++ baseline capture: path relocated totests/wallet_bench/(repo convention for benchmarks;src/is product code), coverage reduced to three of the Five with explicit per-benchmark C++/Rust availability table and the daemon-coupling rationale spelled out for the two Rust-only paths (scan_block_K,transfer_e2e_1in_2out). §4.3 gained a "Benchmarks Rust-only by necessity" subsection capturing the asymmetry so the bench-comparison script (§3.3) and the PR-comment format can handle it deterministically rather than treating missing C++ numbers as a regression. The acknowledgment is explicit: two paths have no pre-deletion C++ baseline and will never have one; regression detection across the rewire for those paths relies on the Rust rolling baseline plus human order-of-magnitude sanity, not on a pre-deletion comparator. -
Mid-rewire hardening plan (
docs/MID_REWIRE_HARDENING.md). New design spec pinning the eight-commit instrumentation pass that lands between the Rust-side wallet-file FFI (commits2a…2k.4, merged) and the C++ consumer rewire (commits2k.5aonward, deferred). Covers: Google Benchmark C++ baseline capture against the existingwallet2.cpphot paths; criterion + iai-callgrind Rust benchmark harness mirroring the same five paths; GitHub Actions CI integration with bidirectional thresholds forcrypto_bench_*(any drift is suspicious — constant-time property defense) and slowdown-only thresholds forhot_path_bench_*; rolling baseline on a dedicatedbench-baselinebranch;postcard-schemasnapshot files with CI-enforcedblock_versionbump on every drift; ripgrep + allowlist secret-wipe discipline forshekyl-engine-stateblocks;WalletLedger::check_invariants()with five cross-block tripwires and a newWalletFileError::InvariantFailed { invariant, detail }variant; adversarial wallet-file corpus covering the three capability- mode attack shapes (tamper-in-place, declared-FULL-with-VIEW_ONLY- shape, declared-VIEW_ONLY-with-trailing-bytes); proptest fuzz harness on stable plus checked-in (non-CI)cargo-fuzztargets. Also captures the dual-path output-equivalence requirement for2k.5b…2las a structural commit-message template line, not a reviewer convention. No code or CI changes in this commit — spec only; the eight follow-up commits each cite a section.
Added
-
Mid-rewire benchmark warning window (commit 2k.c of the wallet-state-promotion plan,
docs/MID_REWIRE_HARDENING.md§3.3.1). Closes the structural-noise loophole that the 2k.a / 2k.b dual-stack rewire would otherwise punch through theci/benchmarksgate. New sentinel filedocs/benchmarks/MID_REWIRE_WARNING_WINDOW.activetoggles warning-only mode — when present, thefail job on threshold tripstep in.github/workflows/benchmarks.ymldowngrades the would-be::error::annotation to a::warning::and exits 0, preserving the upstreamcompare/ PR comment /profile-on-failobservability chain without blocking merges. Policy paragraph inMID_REWIRE_HARDENING.md§3.3.1 pins why the window is needed (pre-rewire baseline vs. post-rewire gate calibration vs. structurally-slower-during-dual-stack middle state), how the sentinel beats workflow-level flags / Actions secrets / branch-name matching on grep discoverability and git-authored toggle trail, and when it must close (2m-cache commit, with a mandatory post-rotation ofbench-baseline). The sentinel path is included in the workflow'spaths:filters for bothpull_requestandpushtriggers, so opening and closing the window self-triggers the gate. Reviewers still see every delta and every samply profile during the window; what they lose is the automated merge block, which would otherwise fire on structural noise the rewire is expected to produce. -
2k.b — refuse legacy
store_keyswrites on SHKW1 wallets (commit 2k.b of the wallet-state-promotion plan,.cursor/plans/wallet-state-promotion_ab273bfe.plan.md§2k.b). Installs the keys-layer fault line inwallet2::store_toso SHKW1-backed wallets cannot silently corrupt their on-disk file by falling back to the legacystore_keysJSON path. The two triggers that would otherwise reach the legacy save branch — save-as (pathdiffers from the currentm_wallet_file) and password change (force_rewrite_keys=true, as routed fromwallet2::change_password) — now throw a typedtools::error::wallet_shkw1_operation_unsupportedbefore any wallet-state mutation (notrim_hashchaincache touch, noprepare_file_namespath rewrite, no cache serialization). Both flows require FFI that doesn't exist yet (shekyl_wallet_save_as,shekyl_wallet_rotate_password) and land in 2l alongside the cache-side rewire. The commonstore()→store_to("", "")path (same file, no forced keys rewrite) is not refused — it never touches the keys file, and its cache save still works through the legacyshekyl_encrypt_wallet_cachepath until 2l. Callers audited:wallet2::change_password(exposed viawallet2_ffi.cppandwallet_rpc_server.cpp) and directstore_to(path, pw)invocations intests/wallet_bench/andtests/unit_tests/wallet_storage.cpp— all refused for SHKW1-backed wallets during the 2k.a → 2l window, revalidated in the rewrite-testing phase.wallet_errors.hhierarchy extended with the newwallet_logic_errorsubclass carrying both the operation name and the keys file path for UX rendering. Verified locally: full shekyl-core C++ rebuild clean acrosswallet,daemon,shekyl-engine-rpc,unit_tests,core_tests,functional_tests; no new lints introduced. -
2k.a — rewire
wallet2load/verify/rewrite onto the SHKW1 handle (commit 2k.a of the wallet-state-promotion plan,.cursor/plans/wallet-state-promotion_ab273bfe.plan.md§2k.a). The keys-side half of the wallet2 → Rust rewire.wallet2::load_keysnow magic-sniffs viashekyl_wallet_keys_inspect; on an SHKW1 match it routes throughshekyl_wallet_open, gates before any secret material leaves Rust on capability (tools::error::wallet_keys_unsupported_capability) and derivation network (tools::error::wallet_keys_wrong_network), then extracts only the 64-byte master seed into a scrubbing file-localTransitionalRederivationInputsRAII wrapper (epee::mlocked<tools::scrubbed_arr<uint8_t, 64>>).m_account.load_from_shkw1rebuilds every derived field (classical SK/PK, view SK/PK, ML-KEM decap key, account address) from the seed;m_account.forget_master_seedimmediately scrubs the C++ copy (Option β — theShekylWallethandle is the single in-memory source of truth for the master seed post-load). An AAD-bound address-match sanity check againstShekylWalletMetadata::expected_classical_addresscatches corruption, HKDF policy drift, and handle-repoint bugs via a distincttools::error::wallet_keys_aad_address_mismatch;init_typeandset_createtimeland atomically with the handle-stash onm_shekyl_wallet.wallet2::load_keys_bufrefuses SHKW1 inputs witherror::wallet_internal_error— the envelope requires the file-lock path and cannot be driven through a raw buffer. Bothverify_passwordoverloads route SHKW1 verification throughshekyl_wallet_keys_openwith a sizing probe for the capability payload; the instance overload runs the same address-match sanity check against the opened handle's metadata so a future migration tool that repointsm_keys_filewithout re-opening the handle surfaces as a typed error rather than silently returning keys from the wrong handle. The static overload logs an L1 warning if a caller passesno_spend_key=false(no in-tree caller does today; the log guarantees any future regression trips test output).wallet2::rewritebecomes a logged L1 no-op for SHKW1 wallets — settings writes land in 2k.b'sstore_torewire.wallet2::deinitresetsm_shekyl_walletbeforem_account.deinit()so the Rust handle's final state write runs while C++ secrets are still live, and the C++ wipe happens after the handle drops. Three new typed refusals insrc/wallet/wallet_errors.hdiscriminate structural failure modes (wrong network vs. AAD-bound cryptographic inconsistency vs. unsupported capability) so CLI, wallet RPC, and tests can render targeted messages without parsing log strings. Security invariants: the 64-byte master seed lives in C++ only for the duration ofload_from_shkw1, undermlock; the address-match check fires before any scalar is materialized in C++;xor_with_key_stream/rederive_from_master_seed/decryptare all length-gated, so the post-scrub empty vector state is a no-op everywhere it's read. Verified locally: full shekyl-core C++ rebuild clean acrosswallet,daemon,shekyl-engine-rpc,unit_tests,core_tests,functional_tests;cargo check -p shekyl-engine-file -p shekyl-fficlean. Test regeneration / wallet2 fixture migration deferred to the rewrite-testing phase per the user-approved scope split. -
Region-2 parser fuzz harnesses (commit 8 of the mid-rewire hardening pass,
docs/MID_REWIRE_HARDENING.md§3.8). Closes the gap the adversarial corpus (commit 7) structurally cannot cover: the corpus pins specific typed refusals against specific malformations it was written to check, which says nothing about byte patterns nobody thought to enumerate. Newrust/shekyl-engine-state/tests/fuzz_region2.rsis a stable-Rust proptest harness that drives randomized input intoWalletLedger::from_postcard_bytes— the canonical region-2 decoder used by the wallet-file orchestrator — and asserts the single load-bearing property: the parser never panics and always terminates with a typed result (eitherOk, or one of the four enumeratedWalletLedgerErrorvariants). Five strategies at 128 cases each cover every relevant mutation shape: point mutation of a valid empty bundle, truncation, random byte insertion, random byte deletion, and entirely-random bytes up to 4 KiB. The error-classification match inassert_typed_or_okis deliberately exhaustive with distinct classification tags per arm, so adding a newWalletLedgerErrorvariant without updating the harness is a compile-time error — the harness stays in lockstep with the error taxonomy mechanically rather than culturally. Total wall-clock is ≈0.06 s per run (three orders of magnitude under the plan's 30 s-per-PR exit criterion); cases = 640 total (128 × 5), comfortably inside the plan's ~500-iteration budget. Companion local-only coverage-guided harness atrust/shekyl-engine-state/fuzz/: a minimalfuzz_target!wrappinglet _ = WalletLedger::from_postcard_bytes(data), excluded from the workspace via newexclude = ["shekyl-engine-state/fuzz"]inrust/Cargo.tomlso stable CI never tries to resolvelibfuzzer-sys. Runnable locally withcargo +nightly fuzz run region2_parser; its README documents the two-condition graduation plan (nightly stabilisation OR mainnet-freeze proximity) and why nightly is not in CI today. The harness is kept trivial by design so that it cannot itself panic and mask a parser regression. Verified locally: 96 existingshekyl-engine-stateunit tests remain green; 5-test proptest harness passes in 0.06 s;cargo check --workspace --testson stable ignores the fuzz crate entirely; clippy is clean with-D warnings; fmt is clean. -
Adversarial wallet-file corpus (commit 7 of the mid-rewire hardening pass,
docs/MID_REWIRE_HARDENING.md§3.7). Locks in the "every layer refuses with a typed error, not a panic or a silent fallback" posture at the integration boundary. Newrust/shekyl-engine-file/tests/adversarial_corpus.rsdrives 16 programmatic attack shapes throughWalletFile::openand asserts the exactWalletFileErrorvariant each one must surface: envelope header attacks on.wallet.keys(wrong magic →UnknownMagic, truncated header →FileTooShort,file_version = 0xFF→FormatVersionTooNew, region-1 ciphertext bit flip →InvalidPasswordOrCorrupt); envelope header attacks on.wallet(wrong magic, futurestate_version, region-2 ciphertext bit flip →StateSeedBlockMismatchas currently mapped, cross-wallet companion swap →StateSeedBlockMismatch); SWSP frame attacks (BadMagic,UnsupportedPayloadVersion,BodyLenMismatch);WalletLedgerbody attacks (bundleformat_versionbump →UnsupportedFormatVersion, per-blockblock_versionbump →UnsupportedBlockVersion, truncated postcard →Postcard); the cross-block invariant gate from commit 6 (INV_TX_KEYS_NO_ORPHANS→InvariantFailed); and a wiring assertion that capability-shape mismatches (plan rows B / C) flow through the existing envelope-levelCapContentLenMismatch { mode, len }variant unchanged — the plan's proposed newCapabilityPayloadMismatchwas dropped on review becausevalidate_cap_contentinshekyl-crypto-pq::wallet_envelopealready enforces the entire intended(mode, cap_content_len)shape, and adding a second variant with identical semantics would duplicate the gate. The corpus is programmatic rather than binary-pinned: each test builds a real wallet pair viaWalletFile::create(...), then performs narrow byte surgery (on ciphertext-protected regions via the publicshekyl_crypto_pq::wallet_envelope::seal_state_filehelper) so it stays green across future format-field renames and AEAD parameter changes. Newdocs/WALLET_FILE_FORMAT_V1.md§2.5 writes up the capability decode posture the corpus enforces — mode first, thencap_content_len, then per-capability interpretation, each step refusing rather than tolerating — so reviewers encountering a "why no new variant?" test can follow the trail. Newrust/shekyl-engine-file/tests/fixtures/adversarial/holds a README + one.mdper attack row documenting the construction and the rationale behind each typed refusal (including the deliberateregion-2-bit-flip → StateSeedBlockMismatchcollapse rather thanInvalidPasswordOrCorrupt, which the envelope cannot distinguish from a seed-block-tag mismatch without running the full region-2 verification twice). Verified locally: all 16 corpus tests pass; the rest of theshekyl-engine-filesuite remains green; clippy clean with-D warnings; fmt clean. -
WalletLedger::check_invariants()aggregator-level gate (commit 6 of the mid-rewire hardening pass,docs/MID_REWIRE_HARDENING.md§3.6). Closes the gap that neither single-block schemas (commit 4) nor the zeroizing-field grep (commit 5) structurally cover: a.walletbundle whose every block decoded cleanly and whose every field is correctly wrapped can still be semantically impossible (a scanner tip below a recorded transfer; a key image shared between two transfers; an orphan per-tx secret whose transaction has been garbage-collected from every live reference). Newrust/shekyl-engine-state/src/invariants.rsowns the closed set of five cross-block invariants with stable machine-readable names:tip-height-not-below-transfer,tx-keys-no-orphans,subaddress-registry-dense,reorg-trail-monotonic,spent-state-consistent. Each check is O(n) in the number of transfers or map keys with a singleHashSet<[u8; 32]>allocation, well under 100 µs for a 10 k-transfer bundle. NewWalletLedgerError::InvariantFailed { invariant, detail }variant carries the stable name plus a pointed diagnostic ("missing minor index 3 in [1, 4]" rather than "file is corrupt"), which flows throughshekyl-engine-file'sWalletFileError::Ledgerby existing#[from]. Two call sites wire the checks in:WalletLedger::from_postcard_bytesruns them after the per-block version gates pass (typed refusal on load), andWalletLedger::preflight_saveruns them ahead of everysave_stateinshekyl-engine-file/src/handle.rs—debug_assert!in debug so a runtime-induced invariant break aborts tests loudly, typedErrin release so a user save never panics mid-write. Two invariants (subaddress density, key-image uniqueness) replace the plan's §3.6spent_imagesandtransfer_indexproposals with shapes that match the actual blocks (BookkeepingBlock::subaddress_registryandTransferDetails::key_image— there is no separate spent-image set and no transfer-index join); the plan explicitly sanctions such adjustment on landing, and the machine-readable names are chosen to outlive any future shape refactor. Verified locally: 16 unit tests (one positive + at least one negative per invariant, plus alternate reference paths for I-2 proving a pool- or pending-referenced tx passes) all pass; the pre-existing 96-testshekyl-engine-statesuite and 51-testshekyl-engine-filesuite remain green; clippy clean with-D warnings; fmt clean. -
Zeroizing-field grep + allowlist CI guard (commit 5 of the mid-rewire hardening pass,
docs/MID_REWIRE_HARDENING.md§3.5). Closes the gap that the wire-schema snapshot from commit 4 structurally cannot cover:Zeroizing<[u8; 32]>and[u8; 32]produce byte-identical postcard output, so unwrapping a zeroize wrapper leaves the snapshot green while silently breaking the runtime secret-wipe contract. Newscripts/ci/check_zeroize.shwalksrust/shekyl-engine-state/src/**/*.rsand emits every[u8; N]orVec<u8>field declaration: production code only (#[cfg(test)]modules and everything past the first#[cfg(test)]in a file are elided), with paren-depth tracking across multi-linefnsignatures sopub fn new(x: [u8; 32], …)parameters are not mistaken for struct fields, and with standard filters on//,///,use,type,impl,let,for,match,->, andassertlines. Every hit must either carry aZeroizing<...>/SecretKey<...>wrapper on the same line (auto-pass, no allowlist entry needed) or be enumerated verbatim —<relative-path>|<normalized decl>— inrust/shekyl-engine-state/.zeroize-allowlist. The allowlist is bi-directional: a new unwrapped field with no entry fails withFATAL: unwrapped byte-shaped field(s) without allowlist entry, and an allowlist line whose field no longer exists fails withFATAL: stale allowlist entry — field no longer exists, so the file cannot rot with ghost entries that would silently re-admit a future field of the same spelling. Initial allowlist encodes 27 deliberate public-bytes entries across six files (bookkeeping_block,ledger_block,payment_id,runtime_state,sync_state_block,transfer,tx_meta_block), grouped by category with per-entry comments: (a) public chain hashes (tip/reorg/creation-anchor/pending-tx/reference-block), (b) public key-image markers onTransferDetails, (c) 32-byte map keys keying per-tx metadata (tx hashes are public lookup handles; values that carry secrets, likeTxSecretKey, are wrapped on their own line), (d) the clearPaymentId([u8; 8])handle (obfuscation is applied by the tx-builder, not the storage type), (e) FCMP++path_blob: Vec<u8>(public-input proof bytes; leaks anonymity-set choice but not spender secrets), (f) mirror-struct schema fields onTransferDetailsSchema/TxSecretKeySchemathat exist only to drive thepostcard_schema::Schemaderive and never allocate at runtime, (g)runtime_state.rsin-memory indexes that are rebuilt fromLedgerBlockon every load and never persisted. New.github/workflows/zeroize-check.ymlruns the script on PRs intodevthat touch the wallet-state source tree, the allowlist, the script itself, or this workflow. Policy captured in.cursor/rules/42-serialization-policy.mdc's enforcement section (§3.4 schema snapshot + §3.5 zeroize grep together form the mechanical half of the wire-format and secret-wipe discipline). Verified locally: script exits 0 on the current tree ("33 candidate field(s) scanned, all wrapped or allowlisted"); the three failure modes — adding an unwrappedscratch_field: [u8; 32], adding a stale allowlist entry, unwrapping anOption<Zeroizing<[u8; 32]>>toOption<[u8; 32]>— each produce the expected pinpoint error. -
Wire-schema snapshot + paired
block_versionCI guard (commit 4 of the mid-rewire hardening pass,docs/MID_REWIRE_HARDENING.md§3.4). Converts theblock_versiondiscipline from cultural invariant (previously policed only by reviewer attention and the prose rule in.cursor/rules/42-serialization-policy.mdc) into a mechanical check that fires on every PR. Adds apostcard-schema = "0.2"dependency toshekyl-engine-state(pinned at the same major as the on-diskpostcard = "1"wire-format crate, stable schema representation), derivespostcard_schema::Schemaon every persisted block (WalletLedger,LedgerBlock,BookkeepingBlock,TxMetaBlock,SyncStateBlock, plus the nestedBlockchainTip,ReorgBlocks,FcmpPrecomputedPath,SubaddressLabels,AddressBookEntry,AccountTags,TxSecretKeys,ScannedPoolTx,SubaddressIndex,PaymentIdtypes), and hand-rollsSchemafor the two leaf types whose fields use#[serde(with = "…")]helpers the derive macro cannot introspect (TransferDetails,TxSecretKey). The hand-rolled impls use the mirror-struct pattern: a compile-onlyTransferDetailsSchema/TxSecretKeySchemathat mirrors the wire layout withVec<u8>for byte sequences, then liftsNamedType.tyout of its derivedSchemaimpl under the domain-facing type name. This is wire-identical to the original types (both produce length-prefixed byte sequences under postcard) but participates inpostcard-schema'sNamedTypetree, which is the load-bearing part of the check.rust/shekyl-engine-state/src/schema_snapshot.rsis a new test module that renders each block'sNamedTypetree as pretty JSON (viaOwnedNamedType—NamedTypeholds&'staticreferences thatserde_jsoncannot roundtrip through) and diff-compares against a committed.snapfile underrust/shekyl-engine-state/schemas/. Seven tests: one per block (5) plus a self-parseability roundtrip guard and a canonicality check on the schemas-dir path. RunningUPDATE_SNAPSHOTS=1 cargo test -p shekyl-engine-state schema_snapshotregenerates; running without the env var asserts. Mismatches print a line-oriented unified diff, name the file that moved, and spell out the three-step fix (bump the constant, regenerate, review)..github/workflows/schema-snapshot.ymlwires two jobs. The first runscargo test -p shekyl-engine-state schema_snapshot --no-fail-fastagainst the PR head. The second diffs the PR against thedevmerge-base and, for every.snapthat changed, insists that both (a) the paired source file was touched, and (b) thepub constline that declares the matching version constant appears on either side of the file's unified diff. Pairing is canonical in both the workflow (PAIRSarray) and theschema_snapshot.rsmodule docs:wallet_ledger.snap ↔ WALLET_LEDGER_FORMAT_VERSION,ledger_block.snap ↔ LEDGER_BLOCK_VERSION,bookkeeping_block.snap ↔ BOOKKEEPING_BLOCK_VERSION,tx_meta_block.snap ↔ TX_META_BLOCK_VERSION,sync_state_block.snap ↔ SYNC_STATE_BLOCK_VERSION. Workflow paths filter is scoped to the wallet-state crate plus the workflow file itself, so unrelated PRs skip the job entirely. Design choices surfaced in §3.4: (a) the snapshot is schema JSON, not postcard bytes — a hex diff is opaque to a reviewer, whereas aNamedTypediff names every field and spells out itsDataModelType; (b) the schema-stability contract leans onpostcard-schema's SemVer (pinned0.2), because theNamedTyperepresentation is part of the crate's public API; (c) the mirror-struct pattern is preferred over upstream-patchingpostcard_schemato understand#[serde(with)]because it is local, reviewable, and does not couple us to an upstream release cadence. Exit criteria met: five snapshot files exist, the assert-test passes on a clean checkout, a deliberate field rename produced a unified diff pointing at the exact node (verified locally against a scratch#[serde(rename = "restore_height")]onSyncStateBlock::restore_from_height), and the workflow's grep-logic dry-run correctly accepts apub const … = N → N+1diff and rejects source-file edits that leave the declaration line untouched. -
CI benchmark gate — iai-callgrind per-PR + rolling baseline on
bench-baseline(commit 3 of the mid-rewire hardening pass,docs/MID_REWIRE_HARDENING.md§3.3). Newci/benchmarksworkflow (.github/workflows/benchmarks.yml) running on PRs intodev(the gate) and pushes todev(the rolling-baseline updater). On a PR:ubuntu-latestruns the full five-bench iai-callgrind harness viascripts/bench/capture_rust_baseline.sh(~8-10 min, cached cargo registry + target dir), diffs the resultingshekyl_rust_v0.jsonagainst the tip of the orphanbench-baselinebranch'sbaseline.jsonviascripts/bench/compare.py, and upserts a Markdown PR comment viascripts/bench/post_comment.py. Threshold table enforced mechanically:crypto_bench_*±5% warn / ±15% fail (bidirectional — speed-ups are suspicious on constant-time paths too),hot_path_bench_*+5% warn / +15% fail (slowdown-only), missing-bench-in-PR = fail. On any fail a second job re-runs the criterion sibling of the tripped bench undersamply recordand uploads aprofile.jsonartifact for flamegraph review. Bootstrap: the first PR before thebench-baselinebranch exists gets abootstrap-pendingcomment and the gate passes; the first subsequent push todevcreates the branch with a bot-authored orphan commit. Design choices documented in §3.3 "Implementation notes": (a) Tier 1 only — criterion wall-clock numbers are rendered in the comment as an informational table but do not trip the gate (the Tier 2 upgrade to dedicated-runner wall-clock is tracked in §6.1); (b) C++ Google Benchmark is not wired in this commit because onlyBM_balance_computeships live on the C++ side and it is wall-clock (same Tier-2 bucket as criterion); (c) the gate diffs againstbench-baseline/baseline.jsondirectly rather than re-running the bench on the baseline commit, because iai-callgrind instruction counts are machine-independent for deterministic code (Valgrind VEX IR, not native cycles) — saves ~8 min of CI per PR and the rolling baseline is always at most one dev-merge cycle stale. The compare report schema (shekyl_rust_v0_compare_v1) is its own versioned envelope so a future schema bump on the capture side does not silently drift the comparator. Companion documentation:docs/benchmarks/README.mdgains a full "CI integration" section with per-PR flow, threshold routing, rolling-baseline semantics, and a "When a gate trips" triage runbook. Permissions are scoped per-job (read-only at top level;pull-requests: writeonly on the comment-posting job;contents: writeonly on the baseline-updater job), using the defaultGITHUB_TOKEN— no PAT, no self-hosted runner, no secret provisioning required. -
Provisional laptop-captured
shekyl_rust_v0baseline (follow-up to hardening-pass commit 2). The harness commit's CHANGELOG entry deferred the frozenshekyl_rust_v0.json+shekyl_rust_v0.iai.snapshotto a reference-machine capture. To unblock commit 3 (CI threshold gate), those two files are landed here as a laptop capture on the commit author's host; the envelope records the exact CPU model, kernel, and toolchain (captured_on.*fields) so the "provisional" status is self-documenting. The iai-callgrind instruction-count columns are stable across back-to-back runs on that host (the §3.2 determinism criterion is met), so the baseline is a valid slowdown detector for same-host re-captures; the criterion wall-clock columns are soft numbers that CPU frequency scaling and background load will drift, and the reference-machine re-capture will overwrite them. Schema is stable across the swap (shekyl_rust_v0), so commit 3's comparison script does not need to branch. The capture-script probe foriai-callgrind-runneris also fixed in the same landing: the tool's--versionflag exits 1 outside the cargo-bench handshake protocol, so the envelope'siai_callgrind_runner_versionfield was previously"unknown"; it now resolves viacargo install --listwith a fallback through the runner's own error banner.docs/benchmarks/README.mdgains a "Provisional laptop baseline" subsection naming the policy relaxation and the exit condition for it. -
Rust wallet-state benchmark harness — criterion + iai-callgrind (commit 2 of the mid-rewire hardening pass,
docs/MID_REWIRE_HARDENING.md§3.2). Five hot paths from the §3.1 list, each shipped with acriterionbinary (wall-clock, Tier-2 metric) and aniai-callgrindsibling (deterministic instruction-count + cache- miss metrics, Tier-1 metric that CI will gate on in commit 3):shekyl-engine-state::{ledger, balance},shekyl-engine-file::open,shekyl-scanner::scan_block,shekyl-tx-builder::transfer_e2e. Naming convention enforced:crypto_bench_*(bidirectional ±5% warn / ±15% fail) for anything touching curve25519, ML-DSA-65, Argon2id, or ChaCha20- Poly1305;hot_path_bench_*(slowdown-only) for postcard serde, balance compute, and scanner bookkeeping. All ten harnesses compile undercargo check --benches, run locally undercargo bench -p <crate> --bench <name>, and — on a host withvalgrind+iai-callgrind-runneronPATH— produce byte-identical instruction counts across back-to-back runs (§3.2 exit criterion). One deliberate deviation from production code is documented: thetransfer_e2e_iaibench bypassesHybridEd25519MlDsa::signand inlines the two sign steps withfips204::ml_dsa_65::try_sign_with_seed+try_keygen_with_rng(seeded)because the production wrapper'sOsRngdraws inside ML-DSA-65 keygen + rejection-sampling loop produced ~16% instruction-count variance on the sign call and ~66% variance once keygen was accounted for, both violating the determinism criterion. The FIPS-204 deterministic variant exercises the identical signing primitives (same NTT, same rejection predicates, same packing); the criterion sibling preserves the randomized production path so the human-facing wall-clock number is honest. Known gap: the fullsign_transactioncall including the FCMP++ membership proof is not benched, because a deterministic curve-tree path fixture keyed to a synthetic tree root is its own scope of work; the manifest §6.1 tracks this and names the un-gap conditions for a futureshekyl_rust_v1schema bump. Companion artifacts:docs/benchmarks/shekyl_rust_v0.manifest.md(per-bench operation lists, fixture shapes, six documented known gaps, apples-to-oranges notes against the C++ baseline),scripts/bench/capture_rust_baseline.sh(reference-machine capture wrapper — sibling ofcapture_cpp_baseline.shfrom commit 1 — emits a schema-versionedshekyl_rust_v0.jsonenvelope with toolchain + host CPU + git-rev metadata alongside a rawshekyl_rust_v0.iai.snapshottext artifact),docs/benchmarks/README.mdupdated with a "Capturing the Rust baseline" section and the shipped file-layout listing. Workspace impact is dev-dep-only:criterion+iai-callgrindland as[dev-dependencies]on the four crates that own a bench (shekyl-engine-state,shekyl-engine-file,shekyl-scanner,shekyl-tx-builder); theshekyl-scannerbench gains a self-referentialshekyl-scanner = { path = ".", features = ["test-utils"] }dev-dep soWalletOutput::new_for_test+RecoveredWalletOutput::new_for_testare available in the bench without exposing them to downstream consumers. The frozenshekyl_rust_v0.jsonis captured on a reference machine by the commit author and landed as a follow-up — this commit ships the harness, not the numbers, because the reference machine is part of the measurement (same discipline as commit 1). -
Wallet2 C++ baseline benchmark harness (
tests/wallet_bench/, commit 1 of the mid-rewire hardening pass,docs/MID_REWIRE_HARDENING.md§3.1). Google Benchmark v1.9.1 harness fetched viaFetchContent, opt-in behind-DBUILD_SHEKYL_WALLET_BENCH=ON(OFF by default so normal contributors do not pay the cold-build cost). Of the five hot paths identified in §3.1, one ships live on this tree (BM_balance_compute, N ∈ {100, 1000, 10000}, O(n)balance()iteration over a seeded synthetic transfer set) and two are scaffolded-but-gated withstate.SkipWithError(...)(BM_open_cold,BM_cache_roundtrip): those two depend onwallet2::generate→store_to→loadround-tripping, which is broken on this tree and reproduced by the already-failing unit testwallet_storage.store_to_mem2file. Root-causing the wallet2 regression is the work scope of hardening-pass commits2l/2m-keys/2m-cache; patching it here would violate the "clear separations" invariant. Un-skipping is a one-line change in each bench function when those commits land. Fixtures use a pinned seed (0xBEEFF00DCAFEBABE) so two runs produce byte-identical inputs; the bench defines its ownwallet_accessor_testintests/wallet_bench/bench_fixtures.h(matching the existing friend declaration insrc/wallet/wallet2.h, disjoint from the same-named class intests/core_tests/wallet_tools.h— the two headers are never included in the same TU) with a minimal surface:m_transfersget,get_cache_file_data,load_wallet_cache. Two of the Five (scan_block_K,transfer_e2e_1in_2out) ship only in the Rust harness from commit 3.2: wallet2's scanner and FCMP++ proof paths are daemon-coupled and have no hermetic provisioning path; the architecturally honest move is to acknowledge the gap indocs/MID_REWIRE_HARDENING.md§3.1 and §4.3 rather than reimplement daemon-side synthetic-tree logic in code that is deleted in 2m-cache. Companion artifacts:docs/benchmarks/wallet2_baseline_v0.manifest.md(prose manifest: every operation in each live bench's hot loop, every I/O boundary, apples-to-oranges notes against Rust, and the un-skip criteria for the two gated paths),docs/benchmarks/README.md(capture procedure + baseline-update policy),scripts/bench/capture_cpp_baseline.sh(reference-machine capture wrapper emitting a schema-versioned JSON envelope with toolchain + host CPU + git-rev metadata),tests/wallet_bench/README.md(local build + run instructions + known gaps). The frozenwallet2_baseline_v0.jsonis captured on a reference machine by the commit author and landed as a follow-up — this commit ships the harness, not the numbers, because the reference machine is part of the measurement. -
Boost
program_optionslink-time dep onlibcommon(src/common/CMakeLists.txt).removed_flags.cppcallsboost::program_options::error_with_option_name::get_option_name(), which inlinesget_canonical_option_nameand therefore requires thelibboost_program_optionssymbol to resolve at link time (libcommon.sois linked with-Wl,--no-undefined). The dep was missing sinceremoved_flagslanded and only surfaced during a clean rebuild triggered by the benchmark harness above. Fix is a one-linePRIVATE ${Boost_PROGRAM_OPTIONS_LIBRARY}insrc/common/CMakeLists.txt. No behavior change outside CMake.
Chore
-
Workspace
cargo fmt --allbaseline (PR 0.5 of the V3 wallet rewrite plan,.cursor/plans/shekyl_v3_wallet_rust_rewrite_3ecef1fb.plan.mdPhase 0). Five files (rust/shekyl-ffi/src/wallet_file_ffi.rs,rust/shekyl-ffi/src/wallet_ledger_ffi.rs,rust/shekyl-scanner/benches/scan_block.rs,rust/shekyl-tx-builder/benches/transfer_e2e.rs,rust/shekyl-engine-file/src/handle.rs) had accumulated hand-edited formatting drift before this plan started;cargo fmt --all --checkflagged them ondev. Mechanical, fmt-only run; no logic, behaviour, or API change. Lands before Phase 1 begins so subsequent rewrite PRs can usecargo fmt --all --checkas a cheap branch-hygiene signal without wading through pre-existing drift. Drift cause was hand-edits bypassing fmt (verified:git log --followon each file shows the drifting hunks were introduced under the samerustfmttoolchain in use today), so unconditionalcargo fmt --allis the correct fix — no#[rustfmt::skip]warranted. -
Phase 0 PR 0.6 planning + FOLLOWUPS scope adjustments (
chore/phase0-pr06-vendor-bump-planning). Split themonero-oxidere-pin question into two distinct operations and scoped them differently:- Operation A — vendor-bump
87acb57→3933664(fork tip). Mechanical, cheap, none crypto-substantive except182b648's base58 decoder hardening. Added as PR 0.6 to Phase 0 of the V3 wallet rewrite plan. Total Phase 0 grows from five PRs to six. - Operation B — un-pin / 40-commit upstream merge. Stays as a
V3.1.x peer plan, not scoped to Phase 0. The active correctness
bug
00bafcf(HelioseleneField::invertVeridise edge case) does not change this assessment: the bug exists today ondev, it is below the wallet stack's API surface, and the rewrite's Phase 1 API shape does not depend on it. The un-pin runs in parallel with rewrite Phases 1–3 if bandwidth allows.
Plan adjustments (
.cursor/plans/shekyl_v3_wallet_rust_rewrite_3ecef1fb.plan.md): (1) new PR 0.6 section with cost-ceiling discipline (bail out if base58 review or workspace verification surfaces concerns); (2) half-day review gate expanded from one item to five (PR 0.4 vendor status, PR 0.3 daemon-side findings, FOLLOWUPS V3.1+ section, cross-cutting locks confirmation, and new item 5 confirming whether un-merged-upstream commits affect Phase 1 Wallet API shape); (3) Phase 1 logging deliverable now absorbs the daemon-side staticlibtracingsilently-dropped follow-up — the same subscriber init solves both the wallet stack and the daemon staticlib in one deliverable; (4) Phase 5 commit message inventory now explicitly closes two V3.2 follow-ups (shekyl-clikey image binary format — no Monero binary-format port;wallet_tools.cppmixin/decoy — swept withtests/unit_tests/wallet*.cpp); (5) Phase 3b deliverables flag an optional--format=qr-chunkson the typed bundles for air-gapped UX, replacing the V3.2 hex-blob QR follow-up; (6) bumped Phase 0 PR count in the Branching cadence section.FOLLOWUPS adjustments (
docs/FOLLOWUPS.md): the V3.1+ section gains an at-a-glance index table (absorbed / closed-by-Phase-5 / cross-linked / independent) used by review-gate item 3; themonero-oxideun-pin entry rewritten to describe Operation A vs Operation B with cross-links in both directions; the three V3.2 entries that get explicit closure (shekyl-cli key image binary,wallet_tools.cppmixin, daemon staticlibtracing) carry inline closure notes pointing to the rewrite phase that absorbs or closes them; the V3.2 hex-blob QR entry annotated to die with the hex format in favour of the typed bundles.Decision-log entry (
docs/V3_WALLET_DECISION_LOG.md): new entry "monero-oxide re-pin: split into Operation A (Phase 0) and Operation B (un-pin V3.1.x plan)" pinning the rationale for why the active correctness bug doesn't force Operation B into Phase 0, and naming the alternatives considered (fold both into Phase 0, defer both to V3.1.x, fold Operation A into PR 0.4) and why each was rejected.No code changes in this PR — planning + cross-link maintenance only. PR 0.6 (the actual vendor-bump) lands in a subsequent PR.
- Operation A — vendor-bump
-
Phase 0 audit cleanup (
chore/phase0-audit-cleanup). Three small follow-ups surfaced by the post-merge comprehensive audit ofdevagainst the V3 wallet rewrite plan's Phase 0 expectations: (1) consolidated the duplicate### Documentationheading under[Unreleased]that was a rebase artefact across PR 0.2 / PR 0.3 / PR 0.4 — three entries moved up into the canonical section, no content lost; (2) added a back-link indocs/SHEKYLD_PREREQUISITES.mdpointing forward to the two consumingdocs/V3_WALLET_DECISION_LOG.mdentries (positional fee mapping,fee_policy_versionabsence) and the daemon-side V3.1 follow-up indocs/FOLLOWUPS.md, so the audit's downstream consumers are reachable from the audit doc itself; (3) fixed a pre-existingclippy::needless_returnlint inrust/shekyl-engine-file/src/handle.rs::is_cross_device_error(introduced under commit2l.a, not by Phase 0) for readability. Recorded a follow-up indocs/FOLLOWUPS.mdnoting that the workspace as a whole is notclippy --workspace -- -D warningsclean (shekyl-fficarries ~12 inherited warnings from its FFI shape) and that a dedicated cleanup pass + CI gate belongs to V3.1.x.
Fixed
-
shekyl_account_public_address_checkargument-order mismatch between Rust definition and C-side declaration (Track 0a CI triage, 2026-04-28). The Rust definition inrust/shekyl-ffi/src/account_ffi.rstakes(pqc_pk_ptr, view_pk_ptr); the C header insrc/shekyl/shekyl_ffi.hdeclared(view_pub_ptr, pqc_public_key_ptr), and the one C++ caller insrc/cryptonote_basic/cryptonote_basic_impl.cppfollowed the wrong order. Every decode therefore ran the FIPS-203 well-formedness check on garbage bytes, surfacing in CI as 14uri.*unit_tests failures with the log linecn: Address failed v1 canonical invariant check (view_pub <-> X25519 prefix or malformed ML-KEM-768 encapsulation key). Introduced in commit0092a8da1("ffi,cryptonote_basic: pin m_pqc_public_key format and publish v1 account FFI"); reacheddevonly at thefeat/wallet-account-rewiremerge30db140fe(2026-04-22). The Rust unit tests atrust/shekyl-ffi/src/account_ffi.rs:954,975use the correct(pqc, view)order and never caught the C-side divergence. Per.cursor/rules/10-shekyl-first.mdc, Rust is the source of truth; the fix aligns the C header and the C++ caller. Two files touched, no fixture regeneration; the previously-failing 14uri.*tests are themselves the regression test (FAIL → PASS). Local verification: 858/870 unit_tests passing after the fix (was 854/870), the 2 remaining failures arewallet_storage.{store_to_mem2file, change_password_mem2file}tracked indocs/CI_BASELINE.mdCluster B anddocs/FOLLOWUPS.md(V3.1, wallet2 hardening-pass close). -
CI baseline established as
docs/CI_BASELINE.md(Track 0e CI triage, 2026-04-28). Records the documented list of known-failing C++ tests with diagnoses, close conditions, and FOLLOWUPS row pointers (Cluster A —uri.*, fixed; Cluster B —wallet_storage, deferred to V3.1 wallet2 hardening-pass; Cluster C —core_tests gen_*, deferred to V3.1 chaingen-harness rewrite or V3.2wallet2.cppremoval; Cluster D —shekyl-oxide divergencecanary, currently green). The document also pins the interimshekyl-oxidedivergence-sync policy (explicit trust assumption + spot-check discipline scaling with window size) and the pre-enforcement noise-floor rule that reviewers apply today: any failure outside the documented list blocks PR merges todevuntil investigated, with mechanical enforcement (a required-status-check on the failing-test set) tracked separately as a follow-up. Linked fromdocs/CONTRIBUTING.mdunder "CI baseline"; CI status is contributor surface, not first-impression surface, so the link does not appear in the top-level README. The full Track 0 plan (CI triage ahead of audit hygiene and Stage 1 spec) is the source of these entries. -
apply_scan_result_to_statestrict-contract enforcement (Phase 2arefresh_scan_loopbundle, Branch 1). Closes the PR #16 Copilot-review finding tracked indocs/FOLLOWUPS.mdV3.0 → "apply_scan_resultstrict-contract enforcement (refresh commit)" (now retired to Recently resolved). The merge inrust/shekyl-engine-core/src/engine/merge.rspreviously had two defensive-coding gaps:block_hasheswas collected viaBTreeMap::insert, silently overwriting duplicate height entries instead of rejecting them.new_transfers/spent_key_images/block_hashesentries with heights outsideprocessed_height_rangewere silently dropped at scope end (the per-heightBTreeMap::removeloop consumed only in-range entries; out-of-range residue fell off the stack uninspected).
Both are producer-bug signals, not concurrent-mutation races.
apply_scan_result_to_statenow pre-validatesblock_hashesfor length-matches-range, in-range, no-duplicates, every covered height present, and post-loop drains the per-height per-hash maps to assert no out-of-range residue remains. Contract violations surface as the newRefreshError::MalformedScanResult { reason: &'static str }variant; this is distinct fromRefreshError::ConcurrentMutation(which signals "the wallet moved under the producer; safe to retry") because a malformed scan result indicates the producer itself is broken and retry cannot help. Decision Log entry "MalformedScanResult: producer-bug signal vs.ConcurrentMutation" (2026-04-26) pins the boundary. New tests:block_hashes_length_mismatch,block_hashes_duplicate_height,block_hashes_out_of_range,block_hashes_missing_height,transfer_out_of_range_block_height,key_image_out_of_range_block_height. -
shekyl-engine-stateledger/ledger_iaibenches: pinBlockchainTip.synced_heightto the synthetic transfers' maxblock_height. The benches underrust/shekyl-engine-state/benches/ledger.rsandrust/shekyl-engine-state/benches/ledger_iai.rswere authored againstWalletLedger::empty()(commita9a81a17e) before invariant I-1 (tip-height-not-below-transfer) was wired intoWalletLedger::from_postcard_bytesby hardening-pass commit 6 (def7d3379, "feat(wallet-state): WalletLedger::check_invariants").build_ledgerwas inheritingtip.synced_height = 0from the empty constructor while the synthetic transfers carriedblock_height ∈ [1_000, 1_000 + N), so the deserialize half of the round-trip panicked withWalletLedgerError::InvariantFailed { invariant: "tip-height-not-below-transfer", … }on every iteration. The fix reconstructs theLedgerBlockwithtip.synced_height = max(transfers[*].block_height)(and a non-Nonetip_hash) so the fixture is invariant-coherent before postcard sees it. The outdateddocs/FOLLOWUPS.mdentry that claimed four iai-callgrind targets failed to compile against the post-RuntimeWalletStatefold has been replaced with a re-review entry capturing the actual finding (see "Phase 1 bench harness re-review post-RuntimeWalletStatefold (April 26, 2026)"). All ten core benches undercapture_rust_baseline.shnow build and smoke-run cleanly. -
source archiveCI job: pingit describeto release tags (v*). The branch-archival policy in.cursor/rules/06-branching.mdcrule 5 has accumulated sevenarchive/<branch>-<date>annotated tags since 2026-04-13 (four of them on 2026-04-25, on commits that are merge-ancestors ofdev). Thesource-archivejob in.github/workflows/build.ymlwas calling plaingit describe, which returns the closest reachable tag. Once anarchive/*tag became the closest tag todev,VERSION="shekyl-$(git describe)"started resolving to e.g.shekyl-archive/phase0-pr06-oxide-vendor-bump-2026-04-25, whose/was interpreted as a directory bygit-archive-all, failing with[Errno 2] No such file or directory: '…/shekyl-archive/<branch>-<date>.tar'. The job had failed on every push for ~2 hours before this fix, including PR #16's source-archive run. Fix is a one-line filter (git describe --match 'v*') that ignores branch-archival tags and keepsVERSIONshaped likeshekyl-vX.Y.Z-N-gSHA. Verified locally:git describe origin/devreturnsarchive/phase0-pr06-oxide-vendor-bump-2026-04-25(broken),git describe --match 'v*' origin/devreturnsv3.1.0-alpha.3-135-g39981643f(correct). No behavior change for branches with av*tag in their ancestry, which is every branch offdevsince the first release tag.
Security
-
rand0.8.5 → 0.8.6 inrust/Cargo.lock(RUSTSEC-2026-0097 / GHSA-cq8v-f236-94qc, severity Low). The advisory describes an unsoundness inThreadRng::TryRngthat can produce aliased mutable references — Undefined Behaviour — when all of the following hold simultaneously: (a) thelogandthread_rngfeatures are enabled, (b) a customlog::Loggeris installed, (c) the custom logger callsrand::rng()/rand::thread_rng()and anyTryRng(formerlyRngCore) method on it, and (d)ThreadRngreseeds while called from inside the logger.This bump is defense-in-depth, not active-vulnerability fix: the project's custom logger lives in
shekyl-loggingand a workspace-wide audit confirmed it does not callrand::rng()orrand::thread_rng()from any logger code path. The exploit precondition (c) is therefore not reachable from current shekyl code. The bump still lands so that future logger work does not accidentally reach into the unsoundness window.Application is a one-line
Cargo.lockchange (cargo update --precise 0.8.6 -p [email protected]) plus the cascading edge updates in seven downstream consumers' dependency blocks (monero-rpc-utils,chacha20poly1305,shekyl-crypto-pq,shekyl-engine-core,shekyl-fcmp,shekyl-staking,fcmp_pp). No source changes; the workspace constraints (rand = "0.8", caret-bounded) accept the bump without Cargo.toml edits.The companion advisory
RUSTSEC-2026-0097against the fuzz-only lockfile (rust/shekyl-crypto-pq/fuzz/Cargo.lock) is intentionally not addressed in this PR. That lockfile is stale relative to the workspace (path-dep version markers still atv2.0.0pre-Wallet→Engine rename); a precise rand bump there cascades into ~50 lines of unrelated lockfile refresh churn. The exploit precondition is equally unreachable from fuzz harness code, and the cleanup belongs with the next routine fuzz-Cargo.lock hygiene pass rather than slipped into this focused security bump.cargo auditexits clean against the bumped lockfile (the RUSTSEC entry no longer matches any resolved version); Cargo.toml constraints unchanged;cargo check --workspace --tests,cargo fmt --all -- --check, andcargo clippy --workspace --all-targets --keep-going -- -D warningsall exit 0.Two further open dependabot alerts on
Shekyl-Foundation/shekyl-coreare stale and self-clear on the nextmainrescan:rustls-webpkiGHSA-82j2-j2ch-gfr8 (already at the patched0.103.13in the workspace lockfile) andcryptography(pip) CVE-2026-39892 (the alert points attools/reference/requirements.txt, which no longer exists in the repo). Neither requires a code change.
Documentation
-
Stage 1 PR 4 (
RefreshEngine) — Round 5 substrate-decision amendment (no-Mock substrate for C6). (feat/stage-1-pr4-refresh-engine, 2026-05-20). Doc-only amendment todocs/design/STAGE_1_PR_4_REFRESH_ENGINE.mdlanded mid-Phase-1 between C5β (legacy producer scaffolding deletion) and C6 (test substrate). The Round 4 §7.X C6 plan ("MockRefreshtest substrate; mirrorsMockDaemon/MockLedgerfrom PR 1 / PR 2") is stale prose from before PR 3 §2.1.2's Mock-X rejection landed; buildingMockRefreshwould re-instantiate the parallel-implementation anti-pattern PR 3 rejected as a category and compound the Mock-X debt thatdocs/FOLLOWUPS.mdalready scheduled to be paid down. The amendment dispositions:-
C6 replaces
MockRefreshwithFaultInjecting<R: RefreshEngine>. Composable wrapper around the productionLocalRefresh(landed at C4); queuesRefreshError::Cancelled/Io/InternalInvariantViolationfor failure injection at the trait boundary; composes against any current or futureRimplementor without per-impl parallel-Mock proliferation. -
Retroactive Mock-X cleanup of
MockLedgerlands in PR 4 C6β (not deferred to PR 5). Extracts the existingMockLedgerbody intoFaultInjecting<L: LedgerEngine>; addsLocalLedger::from_test_blocks(...)constructor replacing the parallel-implementationMockLedger::new(...)surface. CurrentMockLedgeris structurally already aFaultInjecting<LocalLedger>-shaped wrapper (delegating to the canonicalapply_scan_result_to_state); the cleanup is mostly extraction-and-rename, not a re-implementation. Closesdocs/FOLLOWUPS.mdlines 578–604. -
MockDaemon→TestDaemonrename lands in PR 4 C6γ alongside C6β. Mechanical rename only — the structural shape is already correct (alternative real implementation serving canned / cached test responses without network connectivity); only theMocknaming was the bug. Closesdocs/FOLLOWUPS.mdlines 606–620.
The amendment is not a round reopening per the §7 amendment framing: it does not revisit any trait-surface contract pin, attack-surface disposition, or commit-decomposition ordering decision; it replaces stale C6 substrate prose with the binding no-Mock shape PR 3 §2.1.2 settled. The α-disposition, the F1–F13 dispositions, and the C0–C5 / C7 / C8 commit prose are all unchanged.
The no-Mock rationale is re-iterated explicitly in §6 of the design doc (new "Test-substrate discipline — no-Mock substrate inheritance from PR 3 §2.1.2" subsection) and in the §7.X C6 prose, naming the five failure modes the Mock-X pattern instantiates: (1) attack surface from test-only types in production code; (2) conflation of test-controlled inputs to real implementations with substitute implementations; (3) inherited-Monero pattern that has produced real bugs in the inherited codebase; (4) foreclosure of composition with future trait implementors; (5) tests verifying against fake semantics rather than real semantics, degrading the coverage claim.
Rationale anchor:
16-architectural-inheritance.mdc§"cost-benefit-defer-to-later anti-pattern" names the architectural-integrity-now disposition as the default for security-load-bearing substrate work pre-genesis;15-deletion-and-debt.mdcpre-genesis discount applies. Cross-references:docs/completed/STAGE_1_PR_3_KEY_ENGINE.md§2.1.2 (Mock-X rejection rationale + five named failure modes), §2.1.5 (four-pattern pre-flight checklist future per-trait PRs inherit).Files touched (doc-only):
docs/design/STAGE_1_PR_4_REFRESH_ENGINE.md(Status banner; new §6 no-Mock substrate inheritance discipline subsection; test-substrate preservation list rewritten; §7.X C6/C7/C8 prose updated),docs/FOLLOWUPS.md(two retroactive Mock-X cleanup entries pinned to PR 4 C6β/C6γ; fix the prior bug that called PR 4PendingTxEngine— PR 4 isRefreshEngine; PR 5 isPendingTxEngine), and this CHANGELOG entry. -
-
**Stage 1 PR 4 (
RefreshEngine) — Round 5 sub-pin extension- amendment-layering coherence pass (F-Mock-1 through F-Mock-8
- Option (i) wrapper API + two-enum architecture pin).**
(
feat/stage-1-pr4-refresh-engine, 2026-05-20). Doc-only follow-up to the Round 5 substrate-decision amendment above. Same-day review pass surfaced eight Mock-X-substrate findings (F-Mock-1 through F-Mock-8) on the Round 5 amendment, then ran an amendment-layering coherence pass against the post-Round-5 substrate to surface forward-pointer gaps and paradigm-language conflations. The pass landed four substantive sharpenings, four minor audit-trail notes, a new §6.1 "Test-substrate paradigm pin" subsection, and a new §6.1.1 "Two-enum architecture (RefreshEngine-specific positive pattern)" sub-section pinning the producer-internal / trait-surface error-enum split as a positive architectural reference and forward-template for future per-trait PRs. None reopen any Round 1–4 disposition or the Round 5 amendment itself; the sub-pin refines the Round 5 C6 substrate so the Phase 1 author implements against an explicit pin rather than reverse-engineering from tests.
Substantive dispositions (F-Mock-1 through F-Mock-4 + Option (i) wrapper API + two-enum architecture).
-
F-Mock-1 —
cfg-gating symmetry (Option (a)). All four C6 surfaces (FaultInjecting<R: RefreshEngine>,Engine::replace_refresh,FaultInjecting<L: LedgerEngine>,LocalLedger::from_test_blocks) are gated uniformly#[cfg(any(test, feature = "test-helpers"))]. Thetest-helpersfeature is introduced as part of C6α's scope per the F-Mock-7 disposition, with a rationale comment matching the existingbench-internalsprecedent atrust/shekyl-engine-core/Cargo.tomllines 223–227. -
F-Mock-2 —
FaultInjectingqueue contract. Wrapper- internal queue (not actor mailbox) holdingRefreshErrorvalues directly per Option (i) below. Contract: FIFO ordering;queued_failures(&self) -> usizedrain inspector per the existingMockLedger::queued_failuresprecedent;debug_assert!-on-Drop for non-empty queue (panic-on-leftover in test/debug builds); reentrance pops the head per the "pop head if non-empty" semantics. -
F-Mock-3 + F-Mock-3-sharpening + Option (i) wrapper API. The wrapper carries
type Error = RefreshError(notR::Error) and queuesRefreshErrorvalues directly, uniform across allR. Cross-wrapper symmetry justifies the choice:FaultInjecting<L: LedgerEngine>must queueRefreshErrorby trait necessity (perengine/traits/ledger.rs:270–273—apply_scan_resultreturnsResult<(), RefreshError>with noSelf::Errorindirection), soFaultInjecting<R>queuingRefreshErrormatches.Empirical variant enumeration (per source). Of the six
RefreshErrorvariants atengine/error.rs:148, three are reachable from aRefreshEngineimpl'sSelf::Errorvia theFromconversion:Cancelled(unit),Io(IoError)(payload), andInternalInvariantViolation { context: &'static str }(payload constructed at theFromimpl site perengine/local_refresh.rs:368–384). Three are orchestrator-constructed only:MalformedScanResult { reason }(constructed exclusively by the merge layer atengine/merge.rs:315–451when scan-result internal-shape invariants fail — superseding the doc's prior framing that grouped it withCancelled/Ioas trait-reachable),ConcurrentMutation { wallet, result }(constructed at the merge gate), andAlreadyRunning(constructed at the binary-layer single-flight). Under Option (i) direct injection the wrapper can inject any of the six variants into the orchestrator surface; forInternalInvariantViolationboth direct injection (testing producer-returned-then-orchestrator-propagated path) and cause injection (driving causes throughFaultInjecting<LocalLedger>::queue_concurrent_mutationper F-Mock-2 to exhaust the retry budget at orchestrator- side construction sites inengine/refresh.rs) are legitimate test classes. -
F-Mock-4 —
MockLedger-structurally-already-FaultInjectingverification gate anchored. The Round 5 amendment's load-bearing claim ("currentMockLedgeris structurally already aFaultInjecting<LocalLedger>-shaped wrapper") is anchored to source atengine/test_support.rs:773–812:MockLedger::apply_scan_result(line 792) pops fromconcurrent_mutation_queue(line 794); on empty-queue, delegates to the canonicalapply_scan_result_to_state(line 810). Future re-readers don't have to re-verify; C6β scope is bounded as anticipated.
Two-enum architecture pin (§6.1.1). The
RefreshEnginetrait carries a deliberate two-enum architecture worth pinning as a positive architectural reference and forward- template for future per-trait PRs. Producer-internalLocalRefreshErrorispub(crate), unit-variant-only by convention, four variants (Cancelled,Io,Malformed,Internal). Orchestrator-facingRefreshErrorispub, payload-bearing throughout. TheFromimpl boundary atengine/local_refresh.rs:368–384is where payload information is constructed or discarded. The architectural cleanness this delivers — payload guarantees enforced by the type system at the conversion boundary, not by convention at every producer return site — makes the trait surface auditable in a way single-enum architectures cannot match. The pattern is shape-applicable to traits whose canonical method signatures returnResult<_, Self::Error>withSelf::Error: Into<OrchestratorError>; it is not load-bearing for traits whose canonical method signatures returnResult<_, OrchestratorError>directly (per theLedgerEngineprecedent). Per-trait PR pre-flight checks include "does this trait have an impl-sideSelf::Errorindirection, and if so, is the producer-internal enum unit-variant-only?" as a substrate-application check alongside the four-pattern no-Mock pre-flight per PR 3 §2.1.5.Test-substrate implications (two test classes named explicitly). Two test classes follow from the two-enum architecture, both load-bearing for C6α's smoke-test coverage:
-
Class 1 — wrapper-based trait-surface tests. Tests use
FaultInjecting<R: RefreshEngine>to injectRefreshErrorvalues directly (per Option (i) wrapper API); verify the orchestrator handles each variant correctly. Lives in C6α's newfault_injecting_refresh.rstest module plus the trait-dispatchedEngineintegration tests. Sub-properties: empty-queue passthrough; single-injection- then-delegation; multi-injection FIFO ordering; queue-drain-on-teardown (with Drop-timedebug_assert!#[should_panic]separately verified). -
Class 2 — From-conversion tests against
LocalRefresh. Tests driveLocalRefreshdirectly via thepub(crate)producer-internal surface to produce eachLocalRefreshErrorvariant; verify theFrom<LocalRefreshError>impl produces the correctRefreshErrorvariant. Lives inlocal_refresh.rs's existing tests module per thelocal_refresh_error_maps_to_refresh_errortest precedent; sibling to Class 1, not a replacement because the wrapper bypasses theFromconversion by injectingRefreshErrordirectly under Option (i).
Amendment-forward-pointer convention (recorded as meta-discipline). The coherence pass surfaced the pre-Phase-0c forward-pointer gap as a recurrence pattern — the same class of finding F-Mock-3 surfaced from one angle, present at three sites (the Status banner's Round 2 reframe paragraph; §3.1's two-channel error surface prose; §4 Phase 0c's inline comment) all carrying the Round 2 reframe's "unit-variant-only; no payload of any kind" framing that the Phase 0c amendment later refined. Three additive forward-pointers added at those sites preserve each round's historical record (what was decided at that round) while resolving the ambiguity (what the current binding contract is). The convention is recorded as a meta-discipline alongside
21-reversion-clause-discipline.mdc's named-criteria principle: any future amendment that narrows or refines an earlier round's contract lands its own forward-pointer at the earlier site. The two disciplines are complementary — reversion-clauses make rejection- dispositions readable across substrate changes; forward-pointers make narrowing-amendments readable across layered rounds. Both are about making layered prose readable across time.Minor dispositions (F-Mock-5 through F-Mock-8). F-Mock-5 adds an explicit C6β migration table mapping
MockLedger's four public test-affordance methods (with_seed,with_seed_and_state,queue_concurrent_mutation,queued_failures) to their post-migration homes and corrects the prior "replacesMockLedger::new(...)" prose error (the constructor iswith_seed/with_seed_and_state, notnew). F-Mock-6 adds a Phase 1 author commit-message-template note to C6γ enumerating theMockDaemontest affordances surviving the rename unchanged. F-Mock-7 confirms thetest-helpersfeature does not currently exist inCargo.tomland pins the introduction as part of C6α's scope. F-Mock-8 enumerates C6α smoke-test property classes by name across the two test-class structure above.V3.1 ledger-generator FOLLOWUPS entry (sub-pin extension Decision 4: coordinated
TestLedgerBuildersubstrate design). The three V3.x invariant-test FOLLOWUPS entries (tx-validation, FCMP++ tx-pool, staking lifecycle atdocs/FOLLOWUPS.mdlines 2411–2438) share a common test-infrastructure need beyond what PR 4 C6β'sLocalLedger::from_test_blockscovers. The sub-pin lands a new V3.1 substrate-design FOLLOWUPS entry pinning the coordinated-design disposition: build oneTestLedgerBuilder/TestBlockBuilder/TestTransactionBuildersubstrate designed before the first daemon Rust port lands (cost asymmetry from16-architectural-inheritance.mdc"cost-benefit-defer-to-later anti-pattern"); design to be forward-composable with PR 4 C6β'sLocalLedger::from_test_blockssignature; flag the structurally-valid-but-semantically-stubbed middle-ground option in the V3.1 design conversation rather than defaulting to a binary "Need A or full Need B" framing.The sub-pin extension is not a round reopening: no Round 1–4 disposition, attack-surface pin, or commit- decomposition ordering is touched; only the Round 5 C6 substrate and the layered-amendment prose are refined. α-disposition, F1–F13 dispositions, Round 5 amendment, and C0–C5 / C7 / C8 commit prose remain unchanged; the C6 sub-decomposition (C6α / C6β / C6γ) gains the F-Mock dispositions inline.
Files touched (doc-only):
docs/design/STAGE_1_PR_4_REFRESH_ENGINE.md(Status banner Round 5 sub-pin extension paragraph + coherence-pass paragraph + amendment-forward-pointer convention recording; three forward-pointer additions at the layered-amendment sites; new §6.1 paradigm pin + §6.1.1 two-enum architecture pin; §6 preservation listFaultInjecting<R>/FaultInjecting<L>/TestDaemonentries updated; §7.X C6α wrapper-definition / F-Mock-3-sharpening / F-Mock-2 queue contract / F-Mock-7test-helpersfeature / F-Mock-8 smoke-test prose all updated; §7.X C6β migration table added; §7.X C6γ commit-message template note added),docs/FOLLOWUPS.md(new V3.1 coordinatedTestLedgerBuildersubstrate-design entry), and this CHANGELOG entry. -
Stage 1 PR 3 (
KeyEngine) M3a pre-flight closures landed. The four openSTAGE_1_PR_3_KEY_ENGINE.mddispositions Round 4 deliberately deferred — the handle-model emergent attack surface Round 3 surfaced — closed as a coupled disposition cluster:- §7.11 (handle persistence) = option (3) deterministic from
ciphertext. Handle is
cSHAKE256(view_secret || tx_hash || output_index_le_bytes(8))with customization"shekyl/output-handle-v1", 16-byte output. The Round-3 lean toward (1) ephemeral was amended; the four-question coupled cluster collapses from this one disposition. - §7.12 (handle unforgeability / A7) = cSHAKE256-based
deterministic derivation. A7 closes by construction
(cSHAKE256 with
view_secretin the input phase is a PRF inview_secretunder standard assumptions). Implementation crate:sha3 = "0.10"(already a workspace dep) with thezeroizefeature flag enabled, givingSha3Statewipe-on-drop discipline structurally per35-secure-memory.mdc. - §7.10 (memory-pressure / A6) = dissolved by §7.11=(3). No table; no growth target; no eviction policy.
- §7.13 (concurrency / Pattern-5) = dissolved by §7.11=(3). No shared mutable state; pure per-call sponge-state mutation only.
STAGE_1_PR_3_MIGRATION_PLAN.md§3.1 amended to cite the closures and revise the M3a scope (noHandleTabledata structure;derive_output_handlepure function instead;source_ciphertext+output_handleadded toTransferDetailsat M3b alongside the legacy fields, with legacy fields removed at M3d). M3a feat branch cleared to cut. Documentation-only change; no code shipped. - §7.11 (handle persistence) = option (3) deterministic from
ciphertext. Handle is
[3.1.0-alpha.5] - 2026-04-22
Security
-
Retired 32-bit build targets (
v3.1.0-alpha.5, Chore #3). Shekyl is now 64-bit only, on security grounds — not on maintenance grounds. Shekyl's Post-Quantum primitives —fips203(ML-KEM-768) andfips204(ML-DSA-65), consumed on the hot path byshekyl-crypto-pqandshekyl-tx-builder— state their constant-time guarantees against native 64-bit arithmetic. On 32-bit targets the compiler lowersu64operations through compiler-emitted libgcc helpers (__muldi3,__udivdi3,__ashldi3) with no constant-time guarantee, plus variable-latencyu64multiply on common 32-bit ARM cores (Cortex-A series). That is a CT violation introduced by the code generator, not the source — exactly the class source-level CT audits cannot catch. KyberSlash (Bernstein et al., 2024) demonstrates remote-timing key recovery against ostensibly constant-time Kyber implementations broken by non-CT division; the Cortex-M4 Kyber timing-attack line (2022–2024) is supporting context. The X25519+ML-KEM hybrid does not save us: "hybrid is secure if either half is secure" protects against algorithmic breaks, not side-channel breaks — if ML-KEM leaks its secret via timing on 32-bit, X25519 is offline-attackable against captured ciphertexts with unlimited attacker time. FCMP++ proof generation has not been audited for constant-time properties on 32-bit targets, and Shekyl will not take responsibility for that audit across all 32-bit toolchains we would otherwise ship (policy framing, not speculation).MDB_VL32(LMDB's 32-bit paged-mmap mode) and thesrc/crypto/slow-hash.c32-bit software fallback are untested consensus-adjacent storage and PoW paths respectively.32-bit Shekyl wallet users were at meaningfully elevated risk of key extraction compared to 64-bit users; supporting the platform was a tacit lie about the security posture of users on it. This is the correction.
Node-only operation is also retired. A future contributor will argue "I just want to run a 32-bit pruned node on a Pi, I'm not doing wallet operations, the CT argument doesn't apply." That is partially true — node code does not touch secret PQC keys. But
MDB_VL32paging against a multi-GB chain makes sync time measured in weeks (not a supported posture), and shipping a 32-bit daemon binary creates a reasonable user expectation that wallet operation is supported, which it is not. The operational complexity of splitting "32-bit daemon supported, 32-bit wallet refused" outweighs any benefit.Four independent tripwires (defense-in-depth):
- Tripwire D —
CMakeLists.txt. C++-side configure gate:message(FATAL_ERROR …)onNOT CMAKE_SIZEOF_VOID_P EQUAL 8, placed before anyfind_package/include/add_subdirectoryso configure fails early with the CT argument in the message. Exercised on every PR todevby.github/workflows/cmake-gate-test.yml+tests/cmake-gate-test/, which drives CMake with a fake 32-bit toolchain and asserts non-zero exit, gate message + KyberSlash citation in stderr, and nofind_packagechatter (so a PR that moves the gate below a probe also fails the test). - Tripwire A —
rust/shekyl-crypto-pq/src/lib.rs. Primarycompile_error!onnot(target_pointer_width = "64"), since this crate is the ML-KEM-768 / ML-DSA-65 consumer. The gate that fires in practice on a 32-bit Rust build. - Tripwire B —
rust/shekyl-ffi/src/lib.rs. Structural-not-observable: duplicated by design to preserve the refusal at the FFI seam under a future refactor that might split this crate fromshekyl-crypto-pq. Do not delete this gate on the grounds that it "never fires" — its value is structural, not observable; see the comment block on the tripwire anddocs/audit_trail/RESOLVED_260419.md§"Chore #3". - Tripwire C —
rust/shekyl-tx-builder/src/lib.rs. Directfips204(ML-DSA-65) consumer on the transaction-signing hot path; independent of Tripwire A so a future refactor that narrows the dependency shape cannot silently drop the refusal.
Deleted, not
#if 1-ed out. Every 32-bit-conditional block removed in this chore was deleted outright. Dead#if ARCH_WIDTH == 64/#ifdef __i386__/#ifdef __arm__scaffolding invites future contributors to assume a meaningful 32-bit alternative exists somewhere and reason about it; the whole point of the retirement is to foreclose that reasoning.What went away. Build system:
cmake/32-bit-toolchain.cmake; the six 32-bitMakefiletargets that actually existed ondev(release-static-win32,debug-static-win32,release-static-linux-i686,release-static-linux-armv6,release-static-linux-armv7,release-static-android-armv7);BUILD_64/DEFAULT_BUILD_64/ARCH_WIDTH/ARM_TEST/ARM6/ARM7machinery and the Clang+32libatomicworkaround in the rootCMakeLists.txt; the-D BUILD_64=ONargument on all remaining 64-bitMakefiletargets;ARCH_WIDTH != 32conditional insrc/blockchain_utilities/blockchain_import.cpp(body retained, guard deleted);-D MDB_VL32inexternal/db_drivers/liblmdb/CMakeLists.txt(vendoredmdb.cMDB_VL32code paths are now unreachable in Shekyl builds and deliberately left unpatched in-tree — seedocs/VENDORED_DEPENDENCIES.md§"MDB_VL32— 32-bit retirement note" for the future-update drill);contrib/depends/toolchain templatei686/armv7/BUILD_64/LINUX_32branches, package recipes forboost/openssl/android_ndk/ the arch-asymmetric_cflags_mingw32+="-D_WIN32_WINNT=0x600"line inunbound.mk,README.mdhost list,.gitignorei686*/arm*entries,packages.mdexample;cmake/BuildRust.cmakeall non-64-bitCMAKE_SYSTEM_PROCESSORbranches; gitian configs (gitian-linux.yml,gitian-android.yml,gitian-win.yml) 32-bit hosts and MinGW alternatives.C/C++ conditionals:
src/common/compat/glibc_compat.cpp__wrap___divmoddi4block and__i386__/__arm__glob symver arms (plus the corresponding-Wl,--wrap=__divmoddi4linker flag in the rootCMakeLists.txt);src/crypto/slow-hash.couter guard narrowed from__arm__ || __aarch64__to__aarch64__and the 32-bit fallbackcn_slow_hash_{allocate,free}_statestubs removed;src/crypto/CryptonightR_JIT.{c,h},src/crypto/CryptonightR_template.hx86 gates narrowed from__i386 || __x86_64__to__x86_64__;src/cryptonote_basic/miner.cppFreeBSD APM gates narrowed from__amd64__ || __i386__ || __x86_64__to__amd64__ || __x86_64__;src/blockchain_db/lmdb/db_lmdb.h__arm__DEFAULT_MAPSIZEbranch removed;src/blockchain_db/lmdb/db_lmdb.cppMISALIGNED_OKgate narrowed to__x86_64only. Disambiguation:tests/hash/main.cpp:192,206<emmintrin.h>SSE-intrinsic gates are x86_64 arch gates, not 32-bit gates, and are not deleted — an earlier draft ofSTRUCTURAL_TODO.mdlumped them with the 32-bit retirement imprecisely.Rust: three
compile_error!tripwires (A/B/C, above);rust/shekyl-oxide/crypto/helioselene/benches/helioselene.rstarget_arch = "x86"branches collapsed tox86_64only.CI:
.github/workflows/depends.ymlARM v7 stub replaced with a pointer to this chore; new.github/workflows/cmake-gate-test.ymltests/cmake-gate-test/enforcing Tripwire D placement.
Docs:
README.md,docs/INSTALLATION_GUIDE.md,docs/RELEASING.md, anddocs/COMPILING_DEBUGGING_TESTING.mdare now 64-bit-only;docs/VENDORED_DEPENDENCIES.mdcarries theMDB_VL32future-update note;docs/STRUCTURAL_TODO.md§"32-bit targets cannot safely run Shekyl" is the canonical reviewer-facing copy;docs/audit_trail/RESOLVED_260419.md§"Chore #3 (v3.1.0-alpha.5) — 32-bit target retirement: security closure" carries the closure narrative.Supported architectures going forward:
x86_64,aarch64(Linux and Apple Silicon),riscv64(Gitian).armhf,armv7,armv6,i686,i386are out of scope — not deferred, not "maybe later," out of scope. Users on 32-bit hardware must not run Shekyl wallets; node operation on 32-bit hardware is not supported either. Operators on ARM32 / i686 hardware should plan a migration to 64-bit before upgrading pastv3.1.0-alpha.5.Maintenance benefits are real but secondary: every 32-bit carve-out in
STRUCTURAL_TODO.md§"bit-width carve-out without coverage" is eliminated in one chore, closing the dead-scaffolding pattern that motivated the §. - Tripwire D —
Changed
-
Shekyl Foundation institutional release-signing key adopted.
v3.1.0-alpha.5is the first release signed by the Shekyl Foundation institutional signing key (subkey fingerprint3778 B4C8 63C6 1512 B5FC 2203 6914 D748 23DD A8DC, long ID6914D74823DDA8DC; primary fingerprintF5F7 5A47 70C9 4FE1 D5A5 AE59 844E 424F 9866 4F44, long ID844E424F98664F44). The primary certification key is held offline; the signing subkey is hardware-backed (OpenPGP applet) with a two-year expiry (2028-04-18) enforcing a rotation cadence.Previous alphas (
v3.1.0-alpha.3,v3.1.0-alpha.4) were signed with Rick Dawson's personal maintainer key and remain verifiable against that key — prior signatures are not invalidated. Going forward, maintainer keys remain a valid additive fallback for release-tag signing when the institutional key is unavailable (documented exception, not default path); they continue to be the right tool for commit signing, where authorship-attribution is the question.docs/SIGNING.mdis rewritten as the canonical, self-contained reference: both key blocks inline (no loose.ascfiles), an explicit step-by-step release-tag signing ceremony with pre-flight checks, expected-output annotations, a failure-mode table, and a separate downstream-verification path.docs/RELEASING.md§3 (tag creation) now points at the SIGNING.md ceremony and captures the minimum command sequence (gpg --card-status→git tag -u 6914D74823DDA8DC -a -s …→git verify-tagbefore push) as a summary, not a replacement. Resolves thedocs/SIGNING.md§"Future: Foundation institutional signing key" deferral that had been carried forward from V3.1 on the premise that institutional signing required ceremony (offline primary, hardware-backed subkey, bounded expiry) before it added value over a plain personal-key setup; those prerequisites are now in place. -
Logging output format (breaking change, all binaries). Chore #2 of the
easylogging++retirement completes the migration started in V3.1 alpha.4:shekyld,shekyl-wallet-rpc,shekyl-cli, and every other in-tree binary now emit through the same Rusttracing-subscriberstack. The default formatter istracing_subscriber::fmt::layer, and its line shape is not byte-compatible with the vendoredeasylogging++layout it replaces:# Before (easylogging++ default format string): 2026-04-19 14:23:11.042 INFO global src/daemon/main.cpp:322 Shekyl 'Codename' (v3.1.0-alpha.3-release) # After (tracing-subscriber fmt::layer default): 2026-04-19T14:23:11.042123Z INFO global: Shekyl 'Codename' (v3.1.0-alpha.3-release)Timestamps are RFC 3339 UTC (not local time with microseconds), level tokens are full words (
ERROR/WARN/INFO/DEBUG/TRACE, not theE/W/I/D/Vsingle letters), the target appears as a structuredtarget:field, and source location (file:line) is elided by default. Log-scraping tooling that parsed the prior format byte-for-byte must be updated;docs/USER_GUIDE.md§"Logging" documents the new shape for operators. -
MONERO_LOGS→SHEKYL_LOG(env-var rename). Every in-tree consumer ofMONERO_LOGSnow readsSHEKYL_LOGinstead. This closes the C++-side half of the per-.cursor/rules/93-legacy- symbol-migration.mdcrename — Chore #1 (V3.1 alpha.4) already migrated the Rust binaries.SHEKYL_LOGaccepts the sametracing-subscriber-compatible directive grammar as Chore #1 (bare levels, per-target overrides, module-qualified targets) plus the legacy easylogging++ category grammar (net.p2p:DEBUG,wallet.wallet2:INFO, numeric0..=4presets,+/-modifiers) routed through the Rust-side translator. The legacy grammar is preserved on purpose: the ~1,345MINFO/MDEBUG/ etc. call sites insrc/andcontrib/ship category strings in that grammar, and operator runbooks doingSHEKYL_LOG='*:DEBUG,net.p2p:TRACE'must keep working with no downstream edits.Operator action required before upgrading past V3.x alpha.0: scripts, systemd units, Docker/Podman compose files, or launch plists that set
MONERO_LOGS=...will silently become no-ops. Add aSHEKYL_LOG=...line alongside eachMONERO_LOGS=...line before cutting over (both can coexist on pre-Chore-#2 builds so the rollover is safe). -
Log target separator normalized to
::. Targets that used to render in the easylogging++ output asnet.p2p/daemon.rpcnow appear asnet::p2p/daemon::rpcin everytracing-subscriber-rendered line. The FFI boundary (shekyl_log_emit/shekyl_log_level_enabledinrust/shekyl-logging/src/ffi.rs) rewrites dot-separated category names into Rust-idiomatic module-path form before handing the event to the dispatcher, matching the form the legacy-grammar translator emits into EnvFilter directives (net::p2p=trace). Without this, every category-scoped emit from the C++ shim (MCINFO("net.p2p", …),MCLOG(level, "daemon.rpc", …), …) would silently fall through to the bare default clause because EnvFilter compares target strings byte-for-byte. Operator-suppliedSHEKYL_LOGdirectives continue to accept both spellings — the legacy-grammar translator rewrites.to::on the way in, soSHEKYL_LOG='*:WARNING,net.p2p:TRACE'andSHEKYL_LOG='warn,net::p2p=trace'behave identically. Only the rendered output changes. Log-scraping pipelines that grep fortarget=net\.p2pneed to grep fortarget=net::p2p(or, per the format-break entry above,net::p2p:at the front of the fields block) instead. -
shekylddefault log sink moved to~/.shekyl/logs/. Underchore/cxx-logging-consolidation, the daemon's default--log-filepath changed from<data_dir>/shekyld.log(next to the blockchain database) to~/.shekyl/logs/shekyld.log, resolved through the Rust FFI'sshekyl_log_default_path. Testnet/stagenet/regtest runs use the suffixed base namesshekyld-testnet.log/shekyld-stagenet.log/shekyld-regtest.logso the three networks can run side-by-side without clobbering each other's log. Rotation defaults to ~100 MB × 50 archives, and the live file plus every rotated archive are forced to POSIX mode0600on Unix — operator-tunable permissions are not a supported knob. Operators who want to keep the legacy next-to-data-dir layout can pass--log-fileexplicitly; the override path is unchanged. -
CMake Python discovery modernized (Chore #3 follow-up).
include(FindPythonInterp)at the top ofCMakeLists.txtis replaced withfind_package(Python3 COMPONENTS Interpreter REQUIRED)as a single, early, authoritative discovery pass; two downstream shadowing call sites (find_package(Python3 ...)before the economics-params generator andfind_package(PythonInterp)before the tests subdir) are deleted. The legacyPYTHON_EXECUTABLEandPYTHONINTERP_FOUNDvariables are aliased post-discovery so consumers undertests/difficulty/CMakeLists.txt,tests/block_weight/CMakeLists.txt, and thecmake/CheckTrezor.cmakefallback arm continue to work without a cascading migration. Thecmake_policy(SET CMP0148 OLD)migration-debt carve-out that preserved the deprecated module on CMake ≥ 3.27 is removed in the same commit — there is no legacy module left to un-deprecate. Resolves the Copilot review comment on PR #15; addressesdocs/CHANGELOG.mdV3.1.0-alpha.3 entry's own callout of the same migration debt.
Removed
-
MONERO_LOG_FORMATenv var (no replacement). The custom format string thatMONERO_LOG_FORMATused to seed on the easylogging++ tree is no longer a tunable. Formatting is owned by the Rust subscriber's layer stack (fmt::layer, optionally stacked withtracing-subscriberfeature flags at build time), not by an operator env var. There is no V3.x alpha.0 replacement and no intent to re-add one — if you have a log-format requirement that RFC 3339 UTC does not satisfy, file an issue rather than patching the format string. -
Vendored
external/easylogging++/tree. Deleted inded9875b6. All call sites that reachedel::Logger/el::Configurations/el::base::Writeretc. directly have been rewritten to route through theshekyl_log_emit/shekyl_log_level_enabledFFI insrc/shekyl/shekyl_log.h. Theel::namespace survives only as a thin typedef-only compatibility shim incontrib/epee/include/misc_log_ex.h(el::Level,el::Color,el::base::DispatchAction) so the existingMINFO/MDEBUG/MWARNING/MCINFOmacros expand without touching the ~1,345 call sites. Closes theSTRUCTURAL_TODO.md§"Replace easylogging++ with a maintained logger" item (both chores); swept narrative indocs/audit_trail/RESOLVED_260419.md. -
src/rpc/rpc_version_str.{h,cpp}and its unit test (tests/unit_tests/rpc_version_str.cpp), inherited from Monero. The daemon constructs its own version string deterministically incmake/GitVersion.cmakefrom the annotated tag on HEAD, then emitsSHEKYL_VERSION_FULLover RPC as an opaque value. The validator regex was a Monero-era sanity check that parsed that string back against a hardcoded pattern — "protecting" consumers from a failure mode that the CMake construction logic already makes impossible.Exposed on the
v3.1.0-alpha.3tag-push CI run (#394,test-ubuntumatrix): on a tagged build,SHEKYL_VERSION_FULLresolves to3.1.0-alpha.3-release, and the regex (adapted from Monero but never taught SemVer 2.0.0 §9 dotted pre-release identifiers) rejects the dot in-alpha.3. Every tagged release using-alpha.N/-beta.N/-rc.Nnumbering would trip the same assertion — so every tagged release with this file in tree is inherently broken, which is enough of a tell that the file is wrong to have on disk.Per
.cursor/rules/60-no-monero-legacy.mdc"ask why is this here?" — this is an inherited assertion against a Shekyl-owned invariant. The invariant is enforced bycmake/GitVersion.cmake; the daemon should not re-parse its own output to re-check it.rpc_command_executor.cppkeeps the empty-string guard (if (res.version.empty())) so the CLI still reports "version not available" when the RPC response lacks a version, but no longer attempts to format-validate the string it receives.
Fixed
-
Tagged-release
ci/gh-actions/clijobs ontest-ubuntumatrix. Follows from therpc_version_strremoval above.v3.1.0-alpha.3shipped with the daemon, wallet, and source archive built cleanly, but its tag-push CI ran red on this single unit test;v3.1.0-alpha.4will be the first alpha whose tag-push CI is green end-to-end. -
Tripwire D processor regex broadened; gate-test probe assertion tightened (Chore #3 fixup). The
CMAKE_SYSTEM_PROCESSORarm of the 64-bit-only gate inCMakeLists.txtpreviously usedarmv[67]l?, which only matchesarmv[67]andarmv[67]lexactly — real toolchains also emitarmv7-a,armv7a,armv7ve,armv7hf,armv6kz,armv5te, etc., which are all 32-bit ARM profiles. Broadened toarmv[567].*so the "defense-in-depth" half of the predicate (which fires whenCMAKE_SIZEOF_VOID_Pis misreported as 8 on a 32-bit target) actually covers those variants. 64-bit names (aarch64,arm64,armv8*in AArch64 mode) remain outside the pattern by construction. Companion tightening intests/cmake-gate-test/run.sh: the probe-chatter assertion now also catches-- Performing Test ...(fromCheckCCompilerFlag/CheckCXXCompilerFlag/CheckLinkerFlag), matching the set of modules actually relocated below the gate;-- Detecting C/CXX compiler ABI infois deliberately NOT caught because those lines come fromproject()itself, which runs before the gate by construction (the gate'sCMAKE_SIZEOF_VOID_Ppredicate is populated byproject()'s own compiler probe). Resolves the second Copilot review on PR #15. -
contrib/dependsWin64 unbound build restored (Chore #3 fixup). The$(package)_cflags_mingw32+=-D_WIN32_WINNT=0x600line incontrib/depends/packages/unbound.mkwas deleted in the Chore #3 build-system commit under the mistaken framing of "arch-asymmetric 32-bit MinGW carve-out." The_mingw32suffix incontrib/dependsis the OS segment of the host triple, not an architecture gate: it matches every*-w64-mingw32host includingx86_64-w64-mingw32. Unbound 1.19.1'sutil/netevent.cusesWSAPoll/POLLOUT/POLLERR/POLLHUPunconditionally and requires_WIN32_WINNT >= 0x0600to be defined before<winsock2.h>is included; the vendoredx86_64-w64-mingw32toolchain does not default this the way MSYS2 pacman toolchains do, so the deletion broke thedepends.ymlWin64 lane (thebuild.ymlMSYS2 and MSVC lanes use different toolchain pathways and stayed green). Line restored with the scope unchanged — only one MinGW host remains after Chore #3, and the flag belongs on it.
Known regressions
MLOG_SET_THREAD_NAME(label)no longer reaches the log stream. The macro still compiles and still evaluates its argument (so-Wunused-valuestays quiet at the call sites), but the label ([SRV_MAIN]fromabstract_tcp_server2.inl,[miner N]fromminer.cpp,DLNfromdownload.cpp) does not appear in emitted events. easylogging++ used this hook to stamp a semantic label into every subsequent log line; the Rusttracing-subscriberformatter reads the OS-level thread name instead (via the platformpthread_getname_np/GetThreadDescriptionpath), and those names are not being populated in Chore #2. Restoring semantic thread labels — either by teaching the C++ shim to callpthread_setname_np+ Windows equivalents, or by routing the label through the Rust subscriber as aspanfield — is tracked as a V3.2 follow-up indocs/FOLLOWUPS.md. The impact is diagnostic only: thread-scoped log lines now show a generic thread ID instead of the human-readable label the prior format carried.
[3.1.0-alpha.3] - 2026-04-19
Added
- Release signing policy and maintainer keys (
docs/SIGNING.md). New document establishing that every release tag fromv3.1.0-alpha.3onward is a signed annotated tag created withgit tag -a -s. It records the initial maintainer signing key (Rick Dawson, ed25519FEFEC7EF9952D40C, ASCII-armored public key embedded in the doc so downstream verifiers can import it from the repo without trusting a keyserver lookup), and documents verification withgit verify-tag, the reproducible-build cross-check that tag verification does not subsume, procedures for adding new maintainer keys, rotation, retirement, revocation, key hygiene expectations (passphrase, offline revocation certificate, hardware token or encrypted storage, GitHub registration), and the rationale for GPG over SSH signing or Sigstore at this stage. Earlier alpha tags (v3.1.0-alpha.1,v3.1.0-alpha.2) predate this policy and are not signed; their authenticity is established by branch topology and reproducible Guix builds.
Changed
-
Branch policy mandates signed annotated release tags and non-fast-forward merges from
devtomain..cursor/rules/06-branching.mdcwas updated to require thatmainadvance only via a merge commit (git merge --no-ff dev, GitHub "Create a merge commit") with a signed annotated tag placed on the resulting merge commit. Fast-forward, rebase-and-merge, squash-and-merge, and force-push tomainare now explicitly forbidden. The rule cross-links todocs/SIGNING.mdat both the Hard rule 1 mention and the Release flow step 4 mention so a maintainer reading the policy lands on the signing doc. A new "Rationale (why merge commit, not fast-forward)" section was added to capture the reasoning so the decision is not re-litigated each cycle. -
docs/FOLLOWUPS.mdtracks Shekyl Foundation institutional signing key as V3.1.x+ item. Records the V3.1 decision: release signing uses maintainer keys, not an institutional Foundation key, until the Foundation has multi-maintainer operational structure (two or more active release maintainers). Cross-referenced fromdocs/SIGNING.md§"Future: Foundation institutional signing key".
Security
-
Bump
cryptographyfrom44.0.2to46.0.6intools/reference/requirements.txtto clear two Dependabot advisories indexed 2026-04-13:- GHSA-r6ph-v2qm-q3c2 (high): missing subgroup validation for SECT curves could allow a small-subgroup attack during ECDH.
- GHSA-m959-cc7f-wv43 (low): incomplete DNS name constraint enforcement on peer names.
Not exploitable against Shekyl users.
cryptographyis pulled in only bytools/reference/derive_output_secrets.py, a developer-only HKDF test-vector generator that never ships in any binary and is not on a consensus path at runtime. Inspection shows thecryptography.hazmat.primitives.{hashes,kdf.hkdf}imports in that script are unused — all HKDF logic is hand-rolled with stdlibhmac/hashlib— so the bump cannot change its output. Verified by regeneratingdocs/test_vectors/PQC_OUTPUT_SECRETS.jsonunder the new version in a clean venv; SHA-256 matches byte-for-byte (1159cb6de2ce3fa4af5d7a8f88eac71ed35c8f00ebf297a4d9259439b6477163). -
Accept seven
rand 0.8.5Dependabot alerts as risk-tolerated. GHSA-cq8v-f236-94qc ("Rand is unsound with a custom logger using rand::rng()") indexes against the five workspace crates that pinrand = "0.8"plus twoCargo.lockfiles. CVSS is 0 on all seven; the actual exploit requires callingrand::rng()(a 0.9+ thread-local RNG API that does not exist in 0.8) while a customlog::Logimplementation is installed. Shekyl usesrand::rngs::OsRngdirectly andrand_chacha::ChaCha20Rng::from_seedfor deterministic derivation, and the daemon installs no customlog::Log, so no Shekyl code path reaches the vulnerable code. Migrating torand = "0.9"cascades into bumpingcurve25519-dalek4 → 5 plus several other crypto crates; per.cursor/rules/20-rust-vs-cpp-policy.mdcthat is a planning activity with its own design doc and review cycle, tracked indocs/FOLLOWUPS.md§"rand 0.9 migration and curve25519-dalek 5 cascade" with target V3.1.x. Alerts #3 through #9 dismissed on GitHub with reason "risk tolerated" and a link to the follow-up.
Changed
-
wallet2_ffino longer carries wallet-directory state. Removedwallet2_ffi_set_wallet_dirand thewallet_dirfield onwallet2_handle. The four wallet-file FFI entry points (wallet2_ffi_create_wallet,wallet2_ffi_open_wallet,wallet2_ffi_restore_deterministic_wallet,wallet2_ffi_generate_from_keys) now take a fullwallet_pathparameter in place of the barefilenamethat was joined withwallet_dirusing a hardcoded"/"separator. Path construction was inherited Monerowallet_rpc_serverscaffolding and produced mixed-separator paths on Windows (C:\Users\x\...\...//My Wallet.keys). Callers now join paths in Rust viaPathBuf::join, which is platform-correct on every target. The legacy C++wallet_rpc_server.cppkeeps its ownwallet_dirstate and is unaffected — it does not go through the FFI. Theshekyl-cliWalletContextnow holds the directory and joins filenames before each call; theshekyl-wallet-rpcRust shim keepsServerConfig.wallet_dirfor the V3.2 cutover when its handlers will own wallet-file creation.validate_filenamewas narrowed and renamed tovalidate_wallet_path(empty-path check only) — path-component validation is the caller's responsibility now that the caller also owns the directory. -
Nightly
proptest-exhaustivejob tuned and extended todev. DroppedPROPTEST_CASESfrom1_000_000to200_000— the old value could not finish inside the 30-minute runner cap onubuntu-latest(ML-KEM-768 keygen per case dominates wall time, the run was being cancelled not failed). Raisedtimeout-minutesto180so the job has real headroom, and added a branch matrix[main, dev]with per-branch cache keys so nightly coverage tracks both active histories instead of only the default branch. Actual elapsed time is surfaced via the job's::notice::annotation so the 200k / 180m bracket can be tightened once we have real data. See.github/workflows/nightly.yml.
[3.1.0-alpha.2] - 2026-04-17
Retroactive CHANGELOG entry. The v3.1.0-alpha.2 tag was created without promoting
[Unreleased]first; the bullets below were subsequently split out from[Unreleased]during the alpha.3 release cycle. The split is based on the commit rangev3.1.0-alpha.1..v3.1.0-alpha.2; content is verbatim from the original[Unreleased]copy and has not been edited retrospectively.
Removed
- Daemonizer layer. Deleted
src/daemonizer/(POSIXfork()detach, Windows Service Control Manager registration, console-control glue) and the four thin wrapper classes insrc/daemon/(t_core,t_protocol,t_p2p,t_rpc) plus the executor shim. Background execution is now delegated to systemd (Linux), launchd (macOS), Task Scheduler (Windows), or the Tauri sidecar (GUI wallet); in-process forking and Windows service registration were untested code paths touching privilege boundaries and file-descriptor lifetimes, so their removal is a security improvement in addition to an audit-surface reduction. The removal also breaks the circular include chain wheredaemon/command_line_args.htransitively pulledwindows.hinto most of the codebase. Closes FOLLOWUPS.md §"windows-daemonizer-cleanup" and STRUCTURAL_TODO.md §"Daemonizer removal". - Daemonizer CLI flags:
--detach,--pidfile,--install-service,--uninstall-service,--start-service,--stop-service,--run-as-service. Bothshekyldandshekyl-wallet-rpcaccept these only long enough to print a migration message pointing at platform service managers (seesrc/common/removed_flags.{h,cpp}, markedTODO(v3.2)for deletion alongside theshekyl-wallet-rpcRust cutover).--non-interactiveis preserved in both binaries.
Changed
- Daemon orchestration class renamed.
daemonize::t_daemonis nowdaemonize::Daemoninshekyld, andshekyl-wallet-rpc's unrelated inline class is nowWalletRpcDaemon. The two binaries no longer share a type name, clarifying audit scope and the V3.2 Rust cutover plan. - Default data directory resolution moved to
src/common/. The admin-vs-userCSIDL_*branching formerly indaemonizernow lives incommon/daemon_default_data_dir.{h,cpp}, preserving the exact pathshekyldresolved before V3.1. Pinned by a newdaemon_default_data_dirunit test so a future refactor cannot silently point operators at an empty data directory. - MSVC CI job now builds
--target daemon walletinstead of just--target wallet, matching what the GUI wallet release workflow actually compiles. Future MSVC regressions in daemon code will be caught in shekyl-core CI rather than surfacing in the GUI wallet release after an hour of compilation.
Fixed
- Fixed probabilistic flake in
shekyl-crypto-pq::multisig_receiving::tests::scan_wrong_participant_ciphertext_fails. The view tag hint is a single byte by design (fast scanner pre-filter), so a wrong-ciphertext decapsulation had ~1/256 chance of producing a hint that collided with the published one, causing the test's rejection assertion to fail. Test now retries keypair generation (bounded to 64 attempts) until the wrong-ciphertext hint actually differs, so the rejection path is exercised deterministically. No protocol or code change; scan semantics are unchanged. - Made all
src/daemon/headers self-contained for MSVC portability:protocol.h(6 missing includes),p2p.h(2),daemon.h(2),rpc.h(2). These headers relied on include ordering from their callers, which GCC/Clang tolerated but MSVC rejects. - Fixed
#ifdefinsideMERROR()macro argument incore_rpc_server.cpp(undefined behavior, C2059 on MSVC). Replaced with literal function name. - Explicitly captured
handshakein lambda inabstract_tcp_server2.inl(C3493 on MSVC). - Explicitly captured
credits_per_hash_thresholdin lambda incore_rpc_server.cpp(C3493 on MSVC). - SFINAE-constrained
network_addresstemplate constructor innet_utils_base.hto prevent MSVC eager instantiation (C2039).
[3.1.0-alpha.1] - 2026-04-15
First public alpha release. First green CI in repository history.
This release establishes the Shekyl versioning scheme: software versions
follow SemVer independently per repo; the protocol version is a separate
integer (protocol_version = 3). See docs/VERSIONING.md for the full
scheme. The version jump from prior tags (v3.0.x-RC series) to 3.1.0
reflects the addition of FROST-style multisig to the feature set.
Highlights
-
FCMP++ end-to-end test suite passing. The full prove-sign-verify pipeline works across C++ and Rust via FFI, validated by 10-iteration randomized round-trip tests and C++ unit tests on Ubuntu 22.04/24.04, Arch Linux, macOS, and Windows.
-
Five FCMP++ integration bugs fixed. Root causes documented in
docs/FOLLOWUPS.mdaudit trail: FFI depth/layers off-by-one, branch extraction loop bound, missing point-to-scalar conversion, leaf count off-by-one, key image y-normalization breaking batch verification. Additionally, a sixth bug (FFI depth-to-layers convention ambiguity) was found and fixed during CI stabilization. -
V3.1 multisig protocol specified and implemented. FROST-style coordinator-less multisig with hybrid PQC signing, specified in
docs/PQC_MULTISIG.mdand wire format indocs/SHEKYL_MULTISIG_WIRE_FORMAT.md. 93 unit tests, 19 integration tests, 11 fuzz harnesses. -
Versioning scheme established.
docs/VERSIONING.mddefines SemVer for software versions and a separate integer protocol version.SHEKYL_PROTOCOL_VERSIONconstant added tocryptonote_config.h, exposed via--versionoutput and/get_infoRPC.
Unreleased
✨ Added
-
PQC Multisig V3.1: equal-participants protocol implementation. Full implementation of the coordinator-less multisig protocol as specified in
PQC_MULTISIG.md. Key components:MultisigKeyContainerv1.1 withspend_auth_versionfield andmultisig_group_idv1.1 (includes version byte)rotating_prover_index: cryptographic hash-based prover assignment- 8 HKDF-derived key/nonce labels for domain-separated derivation
construct_multisig_output_for_sender,scan_for_multisig_output,validate_multisig_output_i7for output lifecycleGriefingTracker: per-output cost bounding for invalid outputsshekyl1mBech32m address format with file-based handling and 3-representation fingerprintSpendIntent: 14-check validation pipeline (structural, temporal, chain state, balance)ProverOutput,SignatureShare,ProverReceipt: prover and signing flow types with equivocation detection- Honest-signer invariants I1–I7 enforcement
MultisigEnvelopewith 11 message types and AEAD encryption (ChaCha20-Poly1305 with HKDF-derived keys)- Per-intent state machine (8 states: Proposed → Broadcast + terminal)
HeartbeatTracker: liveness, censorship, and sync anomaly detectionCounterProof: 8-rule chain evidence verification for counter recovery- C++
tx_extratags 0x08, 0x09, 0x0A for multisig metadata - FFI:
shekyl_pqc_verify_with_group_idfor defense-in-depth - Consensus: scheme_id consistency enforcement across transaction inputs
-
PQC Multisig V3.1: GUI components (shekyl-gui-wallet). 7 React components for the multisig UX:
FingerprintBadge: grouped hex fingerprint with copy and metadataProverView: per-participant prover assignment breakdownLossAcknowledgment: mandatory 1/N loss checkboxAddressProvenance: fingerprint history with change detectionRelayConfig: multi-relay management with operator diversityViolationAlert: I1–I7 violation display with auto-abortSigningDashboard: real-time intent state with sign/veto actions
-
PQC Multisig V3.1: test infrastructure.
- 93 unit tests across all V3.1 modules
- 19 integration tests (functional, adversarial, determinism)
- 4 cross-platform determinism canaries with pinned byte prefixes
- 11 fuzz harnesses (wallet-core) covering serialization, encryption, state machine, validation, and verification
- Criterion benchmarks for intent_hash, encryption, serialization, fingerprint computation, and assembly consensus
-
docs/MULTISIG_OPERATIONS.md: end-user operations guide covering group setup, receiving, spending, recovery, relay configuration, and security considerations. -
docs/AUDIT_SCOPE.md: expanded to include V3.1 multisig attack surface (KDF, prover assignment, invariants, AEAD, CounterProof, griefing defense). -
docs/SHEKYL_MULTISIG_WIRE_FORMAT.md: standalone portable wire format spec for the V3.1 multisig protocol. Covers MultisigEnvelope binary layout, SpendIntent canonical serialization, 11 message type discriminants, AEAD parameters (ChaCha20-Poly1305 with HKDF-SHA256), DecryptedPayload encoding, chain state fingerprint computation, file transport conventions, and conformance requirements. Enables third-party wallet implementations without reading the full spec. -
GroupDescriptor: canonical JSON backup file format for multisig groups. One file contains everything needed to restore a group from seeds (group_id, threshold, pubkeys, relays, fingerprint). Rust type in
shekyl-wallet-core, Tauri export/import commands, and GUI component inshekyl-gui-wallet. -
Failure-mode UX: Multisig page restructured with 6 failure-mode alert banners (unresponsive co-signer, counter divergence, relay disconnect, fingerprint change, stuck intent, CounterProof failure). All Phase 3 components (SigningDashboard, ViolationAlert, ProverView, FingerprintBadge, LossAcknowledgment, AddressProvenance, RelayConfig) wired into the Multisig page.
-
File-based transport: promoted from placeholder to first-class GUI option with Tauri file I/O commands and functional import/sign/export workflow. Equal prominence with relay transport.
-
Fee impact analysis: added to MULTISIG_OPERATIONS.md with tx size comparison, per-input/per-output overhead, Bitcoin comparison, and economic viability analysis for small transactions.
-
Address format discipline: cursor rule
65-address-format-discipline.mdccodifying thatshekyl1mis the sole multisig HRP for V3.x, with version bytes as the extension mechanism.
📚 Documentation
-
docs/MULTISIG_OPERATIONS.md: expanded from 222-line protocol reference to ~500-line comprehensive operations guide with decision framework, 3 operational playbooks, 6 failure recovery guides, threat model worksheet, and honest limitations section. -
docs/FOLLOWUPS.md: added hardware wallet constraints (ML-DSA-65 computation cost on Cortex-M, screen constraints, vendor outreach) and headless co-signer service reference implementation, both targeting V3.2. -
GUI wallet cursor rules: added
81-no-protocol-knowledge.mdc(users never see FCMP++, KEM, HKDF in the UI) and82-failure-mode-ux.mdc(every feature must enumerate failure modes before implementation, failure states get dedicated UI).
🔒 Security
-
Zeroize ephemeral multisig signing seeds.
ed_seedandml_seedstack copies inconstruct_multisig_output_for_senderare now wrapped inZeroizing<[u8; 32]>, ensuring automatic zeroing on drop. Closes a theoretical side-channel surface from FOLLOWUPS.md V3.1 audit response. -
PersistedMultisigOutputDebug redaction. TheDebugderive onPersistedMultisigOutputwas replaced with a manual implementation that redactsmy_shared_secret(64-byte KEM-derived material). Prevents accidental secret exposure throughdbg!or structured logging. -
validate_balancechecked arithmetic.SpendIntent::validate_balancenow useschecked_addfor input sums, output sums, and fee addition. Previously used wrappingsum()— crafted u64 values could wrap both sides to the same value and pass the equality check. -
HKDF derivations return
Result.derive_multisig_kem_seedandderive_participant_kem_randomnessnow returnResult<..., CryptoError>instead of panicking via.expect()on the transaction construction path. -
eprintln!removed fromshekyl_fcmp_verifyFFI. Two diagnosticeprintln!calls in the FCMP verification FFI path have been removed. The C++ caller already logs verification failures; the Rust-side stderr output was redundant and failed the CI lint.
🐛 Fixed
-
FCMP++ FFI: move depth-to-layers conversion to C++ callers.
shekyl_fcmp_proveandshekyl_fcmp_verifypreviously converted LMDB depth to upstreamlayersinternally (layers = depth + 1). This created an ambiguous contract where the sametree_depthparameter meant different things in different FFI functions. Now both functions accept the upstreamlayerscount directly; C++ callers (blockchain.cpp,rctSigs.cpp) performdepth + 1before calling.shekyl_sign_fcmp_transactionstill accepts LMDB depth and converts internally (wallet callers pass LMDB depth). Added diagnostic tracing toproof::verifyforFcmpPlusPlus::readand key image decompression failures. Fixedvalidate.rsc1/c2 alternation comment (the formula was correct but had been transiently swapped during refactoring). Tests simplified to single-layer Selene root (layers=1) to match the Rust unit test convention. -
CI: fix
cargo auditfailure from RUSTSEC-2026-0098/0099. Bumpedrustls-webpki0.103.10 -> 0.103.12 andrand0.9.2 -> 0.9.4 inCargo.lock. Addedrust/audit.tomlto acknowledgerand0.8.5 (RUSTSEC-2026-0097, not applicable: Shekyl usesOsRng, notrand::rng()with a custom logger). -
Remove dead
verify_transaction_pqc_authone-arg overload. The no-argument overload intx_pqc_verify.cpphad zero callers — the sole production caller (blockchain.cpp) uses the two-arg form withexpected_scheme_id. Replaced with a default parameter. Per15-deletion-and-debt.mdc: dead code goes. -
Fix stale
shekyl_ffi.hshekyl_pqc_verify_debugcomment. The error code documentation (0-4) did not match the RustPqcVerifyErrorenum (0-11). Updated to reflect the actualrepr(u8)discriminants. -
Reconcile FOLLOWUPS.md and STRUCTURAL_TODO.md. Marked 5 items in STRUCTURAL_TODO as resolved (code already fixed). Corrected the
expected_scheme_idFOLLOWUPS entry (parameter is actively used byblockchain.cpp, contrary to the prior note). Markedrpasswordaudit as covered by CI.
🔄 Changed
-
FFI: verification functions return typed
u8error codes instead ofbool.shekyl_pqc_verify,shekyl_pqc_verify_with_group_id, andshekyl_fcmp_verifynow return 0 on success and a nonzero error discriminant on failure. PQC verify usesPqcVerifyErrorcodes 1-11; FCMP verify usesVerifyErrorcodes 1-7. Error codes are available in all build modes, eliminating the debug-only double-call pattern. C++ callers (tx_pqc_verify.cpp,blockchain.cpp) updated to log error codes unconditionally. Per30-ffi-discipline.mdc. -
Clippy lint rename:
unchecked_duration_subtraction→unchecked_time_subtraction. Updated in workspaceCargo.tomlto track the upstream rename.
🗑️ Removed
shekyl_pqc_verify_debugdeleted. Now that productionshekyl_pqc_verifyreturns typed error codes, the debug-only variant is redundant. All call sites and the#ifndef NDEBUGC header guard removed.
🐛 Fixed (continued)
-
All Rust clippy warnings resolved in
shekyl-crypto-pq. Fixed 1 error (missing_fields_in_debuginPersistedMultisigOutput) and 13 warnings:op_ref(11 sites inkem.rs,montgomery.rs,output.rs),needless_range_loopandunnecessary_map_or(inmultisig_receiving.rs),uninlined_format_args(inoutput.rstests). Also rancargo fmtacross workspace. -
FCMP++ proof verification: five integration bugs fixed, first green CI. The FCMP++ core tests (
gen_fcmp_tx_valid,gen_fcmp_tx_double_spend,gen_fcmp_tx_reference_block_too_old,gen_fcmp_tx_reference_block_too_recent,gen_fcmp_tx_timestamp_unlock_rejected) have never passed since integration. Root causes identified and fixed:- FFI depth/layers off-by-one. LMDB stores 0-indexed
tree_depth; the upstream library expects 1-indexedlayerscount. Fix:layers = tree_depth + 1at the FFI boundary. - C++ branch extraction loop was
< depthinstead of<= depth. BothgenRctFcmpPlusPlusandassemble_tree_path_for_outputskipped the root layer's branch data. Fix:layer <= tree_depthin both. - Point-to-scalar conversion missing in witness construction.
Raw LMDB point hashes were passed as branch siblings without converting
to cycle scalars. Fix:
selene_to_helios_scalar/helios_to_selene_scalarapplied duringgenRctFcmpPlusPlusbranch assembly. compute_leaf_count_at_heightoff-by-one. Maturity comparison used<= target_height + 1while LMDB'sdrain_pending_tree_leavesuses<= current_height. Fix: removed the+ 1to match LMDB semantics.key_image_y_normalizebroke Ed25519 batch verification. The normalization (clearing byte 31 sign bit) modified the key image away from the truex * Hp(O)used by the Rust prover. Fix: deletedkey_image_y_normalizeentirely — FCMP++ key images are not y-normalized.- PQC signing payload computed before all public keys were derived.
get_transaction_signed_payloadhashes all inputs'hybrid_public_keyvalues, but the single-loop approach signed early inputs before later keys existed. Fix: two-phase PQC signing (derive all keys, then sign all inputs). All 5 FCMP++ core tests, 4 staking tests, 28 FCMP unit tests, and 45 Rustshekyl-fcmptests now pass. This is the first green CI in the repository's history.
- FFI depth/layers off-by-one. LMDB stores 0-indexed
-
Consensus-critical: curve tree leaf ordering bug (DB v6 → v7).
pending_tree_leavesusedMDB_DUPSORTon 128-byte leaf data, causing outputs with the same maturity height to drain into the curve tree in byte-sorted order rather thanglobal_output_indexorder. This broke the implicitglobal_output_index == tree_leaf_indexassumption that every caller ofget_curve_tree_leaf()relied on. Replaced with 16-byte composite keysBE(maturity) || BE(output_index)enforcing canonical drain order. Same restructuring applied topending_tree_drain. Added explicit bidirectional mapping tables (output_to_leaf,leaf_to_output) and ablock_pending_additionsjournal for robustpop_blockreversal. DB schema bumped to v7 (incompatible with v6 — requires resync). -
get_curve_tree_leaf()parameter was silently misnamed. The function acceptedglobal_output_indexin its signature but actually looked up by tree position. Renamed toget_curve_tree_leaf_by_tree_position()and addedget_curve_tree_leaf_by_output_index()(double lookup via mapping table). All callers updated — compile errors catch any missed sites. -
check_stake_claim_inputnow recomputes and verifies the stored leaf. Previously the stake claim gate only checked bounds (staked_output_index < leaf_count). Now the stored leaf is retrieved via the output→leaf mapping and bytewise-compared to a leaf recomputed from the output's(output_key, commitment, h_pqc). This binds the claim to the actual output data in the tree.
✨ Added
-
src/blockchain_db/shekyl_types.h: Strongly-typed identifiers (TreePosition,OutputIndex,MaturityHeight,BlockHeight) and LMDB key/value encoders (PendingLeafKey,DrainKey,DrainValue,BlockPendingKey,BlockPendingValue) for curve-tree state. Designed for 1:1 translation to Rust newtypes and heedBytesEncode/BytesDecode. -
4 new regression tests in
deferred_insertion.cpp: same-maturity drain order by output_index, block_pending_additions journal round-trip, output↔leaf mapping round-trip, pop_block journal-driven reversal simulation.
📋 Protocol
-
X25519 public key derived from Ed25519 view key. The X25519 public key used in the hybrid KEM classical component is the Edwards→Montgomery image of the Ed25519 view public key:
x25519_pub = (1 + y) / (1 - y) mod p. It is not carried in the address or generated independently. The Bech32m address PQC segments carry ML-KEM material exclusively. SeePOST_QUANTUM_CRYPTOGRAPHY.md§X25519 Binding to View Key. -
Unclamped Montgomery DH (not RFC 7748 X25519). The classical KEM component performs
Scalar * MontgomeryPointwith the Ed25519 view scalar as the private input. RFC 7748 scalar clamping is not applied because the view scalar is already reduced modℓ; clamping would mutate it and desynchronize sender/receiver derivation. SeePOST_QUANTUM_CRYPTOGRAPHY.md§DH Semantics. -
Low-order Montgomery point rejection (validation rule). Recipients MUST reject low-order Montgomery points on
kem_ct_x25519before performing DH:if (8 * point).is_identity() → reject. This replaces RFC 7748 clamping's cofactor-clearing role. Sender-side check on the derived recipient X25519 pub is defense-in-depth. SeePOST_QUANTUM_CRYPTOGRAPHY.md§DH Semantics. -
m_pqc_public_keylayout invariant: 1216 bytes.X25519_pub[0..32] || ML-KEM-768_ek[32..1216]whereX25519_pubis derived (never transmitted). Canonical assemblers:get_account_address_from_str,generate_pqc_key_material. Runtime checks enforce exact size at every split site. -
Wallet key consistency invariant.
m_pqc_secret_key[0..32] == m_view_secret_key. Wallet refuses to open on mismatch. -
X25519 derivation test vectors published.
docs/test_vectors/PQC_TEST_VECTOR_005_X25519_DERIVATION.jsonpins the Ed25519→X25519 derivation, unclamped DH shared secrets, low-order rejection inputs, and Edwards rejection inputs for third-party implementers.
✨ Added
montgomery.rs: Edwards→Montgomery conversion, unclamped scalar interpretation, low-order point detection. (shekyl-crypto-pq)shekyl_view_pub_to_x25519_pubFFI export for C++ callers. (shekyl-ffi)- Genesis reproducibility artifacts:
verify_genesis.pyscript andGENESIS_BUILD_INFO.txt. (shekyl-dev/tools/genesis_builder/)
🔄 Changed
genesis_builderprint_usage updated to Bech32m. Usage example now shows<bech32m>addresses instead of<base58>.
🐛 Fixed
-
Fixed
core_testsFCMP++ proof verification failures.gen_fcmp_tx_valid,gen_fcmp_tx_double_spend, andgen_staking_lifecycleall failed with "FCMP++ proof verification failed" because test-chain block headers carried a placeholdercurve_tree_root(selene_hash_init) while witness paths were assembled from the real LMDB tree. Added per-height curve tree root storage (m_curve_tree_rootsLMDB table) so both the prover and verifier read the correct historical root for any reference block height. Also alignedcompute_leaf_count_at_heightinchaingen.cppwith productioncollect_outputslogic (output-type filtering andoutPkbounds checks). -
Reverted
vcpkg.jsonmanifest that broke MSVC CI. Commit397817bintroduced avcpkg.jsonwith"builtin-baseline": null, which caused the MSVC CI job to fail (vcpkg auto-detected the manifest and rejected the null baseline). The CI workflow already manages vcpkg dependencies via explicit CLI invocation. Deleted the manifest to restore the working state. -
Restored and upgraded
JsonSerialization.FcmpPlusPlusTransactiontest. Replaced ring-stylemake_transactionwithmake_fcmp_transaction()that constructs a real v3 FCMP++ transaction via the full Rust FFI signing pipeline: KEM keypair generation, output construction, scan-and-recover, curve tree leaf/root building, FCMP++ proof signing and verification, and PQC auth signing. The test now exercises real cryptographic operations (not stubs) before round-tripping through JSON serialization. Deprecatedwallet_tools::gen_tx_srcwith migration note pointing to the FCMP++ pipeline inchaingen.cpp. -
Fixed
rctSigJSON serializer missingmessageandreferenceBlock. The JSON round-trip forrct::rctSigdid not serialize themessagefield (tx prefix hash) or thereferenceBlockfield (forRCTTypeFcmpPlusPlusPqc). Both are part of the binary wire format inrctTypes.hbut were silently lost during JSON serialization. Addedmessageto all rctSig JSON output andreferenceBlockfor FCMP++ transactions. Discovered by theFcmpPlusPlusTransactionJSON round-trip test. -
on_get_curve_tree_pathRPC consistency fix. The RPC handler readleaf_countfrom tip state but returned areference_blockseveral blocks behind tip. If the tree grew in between, the returned leaf data and layer hashes did not match the reference block'scurve_tree_root. Fixed by computingref_leaf_countatreference_heightvia drain journal, capping all reads to that count, and applying boundary-chunk hash trimming for sibling chunks that changed since the reference block. Mirrors the fix already applied to the test harness inchaingen.cpp. -
MSVC portability batch. Expanded
src/common/compat.hwith centralized platform-conditional includes forunistd.h/io.h,dlfcn.h, andsys/mman.h. AddedAND NOT MSVCguards tomonero_enable_coverage(GCC-only--coverageflags) andenable_stack_trace(GNUld-Wl,--wrap=__cxa_throw). Fixedbootstrap_file.cpplongtypes tostd::streamoff/uint64_tfor LLP64 correctness. Fixed unsigned negation inwallet2.cpp:772(std::advance(left, -N)where N issize_t) withstatic_cast<ptrdiff_t>. Created rootvcpkg.jsonmanifest for deterministic dependency management. -
FCMP++ test harness: tree state mismatch.
assemble_tree_path_for_outputandconstruct_fcmp_txintests/core_tests/chaingen.cppread the current (tip) curve tree state but the verifier checks against the reference block's historical tree root. Fixed by computingref_leaf_countat the reference block height and capping all leaf/layer reads to that count, with boundary chunk hash trimming viashekyl_curve_tree_hash_trim_selenefor siblings that changed since the reference block. Also fixed a layer offset bug where sibling hashes were read fromlayerinstead oflayer - 1. -
FCMP++ test harness: staking tests missing FCMP++ pipeline.
gen_staking_lifecycleandgen_stake_all_tiersusedconstruct_staked_txwhich produced stub RCT signatures without FCMP++ proofs or PQC auth. Rewritten to use callback-based testing (likegen_fcmp_tx_valid) with a newconstruct_fcmp_staked_txthat routes through the full FCMP++ proving and PQC signing pipeline viaapply_fcmp_pipeline.
🔄 Changed
-
Unified constant-time comparison for all 32-byte crypto types.
public_key,key_image, andhashnow usecrypto_verify_32viaCRYPTO_MAKE_HASHABLE_CONSTANT_TIMEinstead ofmemcmp-basedCRYPTO_MAKE_HASHABLE. Eliminates the footgun of a developer choosing the non-constant-time macro for a new secret-bearing 32-byte type. -
Added
ct_signaturestype alias.using ct_signatures = rct::rctSig;added incryptonote_basic.has the starting point for migrating away from the Monero-erarct_signaturesname. Full caller migration andrct::namespace rename deferred to V4. -
Documented alternative tokens decision. Keeping
/FIiso646.hworkaround for MSVC; mechanical replacement ofnot/and/oris high-effort, low-value. Recorded in STRUCTURAL_TODO.md. -
Workspace-wide clippy cleanup. Resolved all
cargo clippy --all-targets --no-deps -- -D warningserrors across the Rust workspace (14 crates, 52 files). Key changes: replacedas u128casts withu128::from(), added#[allow]for intentional truncation in economics/FFI code, marked FFIextern "C"functionsunsafewith# Safetydocs, replaced redundant closures with method references, usedlet...else, switchedfrom_slicetoGenericArray::from()in chacha20poly1305, changed&Vec<T>to&[T]in public APIs. No behavioral changes.
✨ Added
-
Fuzz target for
derive_output_secrets. Newfuzz_derive_output_secretscargo-fuzz harness inrust/shekyl-crypto-pq/fuzz/. Exercises arbitrarycombined_ssinputs (up to 1200 bytes) and output indices; asserts determinism, non-zero ho/y scalars, and absence of panics on truncated/oversized input. Closes FOLLOWUPS.md fuzz-derivation item. -
Witness header round-trip test. New
witness_header_build_then_parse_roundtriptest inrust/shekyl-ffi/with locked vectors indocs/test_vectors/WITNESS_HEADER.json. Provesshekyl_fcmp_build_witness_header(writer) andparse_prove_witness(reader) agree byte-for-byte on all 8 header fields[O:32][I:32][C:32][h_pqc:32][x:32][y:32][z:32][a:32]. Closes FOLLOWUPS.md witness-roundtrip item.
📚 Documentation
-
y=0 consensus check resolved as infeasible. Documented that a consensus-level rejection of outputs with
y=0T-component cannot be implemented: the verifier does not knowy(it is a KEM-derived secret) and testing whetherOlies in the G-only subgroup requires knowing the DL between G and T. Defense is structural viaderive_output_secretshard-assert and fuzz coverage. Closes FOLLOWUPS.md y=0-consensus item. -
scheme_id binding analysis corrected in
PQC_MULTISIG.md. Theexpected_scheme_idparameter inverify_transaction_pqc_authis unused because FCMP++ hides which output is being spent. Scheme downgrade protection is provided by theh_pqccurve tree leaf commitment — the FCMP++ proof bindsH(hybrid_public_key)to the leaf, making a downgrade require a Blake2b-512 collision. Updated Attack 1 mitigation description andPOST_QUANTUM_CRYPTOGRAPHY.mdaccordingly. -
FOLLOWUPS.md and STRUCTURAL_TODO.md audit and cleanup. Marked 5 stale items as resolved (2 in FOLLOWUPS, 3 in STRUCTURAL_TODO):
signing_round_trip.rsnow exercises FFI,AUDIT_SCOPE.mdexists, C++20-isms audit complete, easylogging++ MSVC fully fixed,wallet2.h:2324bool/char pattern removed by wallet refactoring. Updated 2 stale references:simplewallet.cppdeleted (removed fromlongtype sites andmemcmpresolution list),wallet2.cpp:782shifted to line 772. Updated testmemcmpcount from 84 to ~90. Annotatedexpected_scheme_idremoval as deferred to PQC multisig PR.
📚 Documentation
- Cross-repo documentation audit. Comprehensive review across all five
Shekyl repos fixing stale references, Monero-era branding, completed-but-
unchecked items, and broken cross-references. Key changes:
README.md: Removed Monero CI badges (Coverity, OSS Fuzz, Coveralls), stale distribution packages (apt install monero, etc.), Raspberry Pi Jessie instructions, 2022-era pruning sizes,monerod.confreferences. Fixed research section cross-references to shekyl-dev repo.proxies.md: Renamed "Monero ecosystem" to "Shekyl ecosystem".DOCUMENTATION_TODOS_AND_PQC.md: Fixed FCMP++ "Phase 8" references (doc exists), CryptoNight reference (Shekyl uses RandomX from genesis),CURVE_TREE_OPERATIONS.mdreference (covered inFCMP_PLUS_PLUS.md), v2.0 tx references (should be v3).INSTALLATION_GUIDE.md:FCMP_PLUS_PLUS.mdexists, not "planned."V4_DESIGN_NOTES.md: Checked boxes for items done in V3.RELEASE_CHECKLIST.md: Marked wallet/exchange/pool entries as placeholders for Shekyl-specific partners.FOLLOWUPS.md: Added items for fuzz harness onderive_output_secrets, witness header round-trip test, y=0 consensus check, andAUDIT_SCOPE.mdcreation.- KEM plan: Updated 18 todo items from
pendingtocompletedmatching actual codebase state.
🗑️ Removed
-
tests/unit_tests/address_from_url.cppdeleted. The test referencedMONERO_DONATION_ADDR(removed constant) and tested Monero OpenAlias DNS resolution againstdonate.getmonero.org. Both the constant and the DNS endpoint are irrelevant to Shekyl; the test broke the macOS CI build. -
simplewallet(shekyl-wallet-cli) deleted. The 9,126-line C++ interactive wallet REPL has been removed. Its replacement,shekyl-cli(Rust), was already at full parity for all actively-used commands. Removedsrc/simplewallet/directory, CMake target, CI artifact references, and Windows installer entries. Thetranslations/directory retains simplewallet-era.tsstrings as dead entries within shared i18n files. -
wallet/api/C++ wrapper layer deleted. The 3,909-line Monero-era C++ wrapper (wallet2_api.hand 10 implementation files) had no production consumer -- the GUI useswallet2_ffiviashekyl-wallet-rpc(Rust). Removedsrc/wallet/api/directory,tests/libwallet_api_tests/, and theadd_subdirectory(api)entry fromsrc/wallet/CMakeLists.txt. Cleaned up stale#include "wallet/api/*.h"references inobject_sizes.cppandaddress_from_url.cpp.
🐛 Fixed
-
19
core_testsfailures and SEGFAULT from v3 transaction incompatibility. The test framework'sconstruct_miner_tx_manuallywas hardcoded to produce v2 transactions without PQC output construction, causing 16 block validation tests to fail during generation and a SEGFAULT intx_validationtests. Rewrote the function to perform genuine v3 output construction viashekyl_construct_outputFFI. Addedappend_v3_output_to_miner_txhelper for tests that add outputs to coinbase. Fixedfill_tx_sourcesto populateho/v3_ho_validon source entries viatry_v3_scan_output. Removed stale classical key derivation from view tag tests. Fixed serialization consistency in tests that modifyvout/vinwithout updatingrct_signaturesfields. -
Non-exhaustive
TxBuilderErrormatch in FFI error-code mapping. Commitaff9f777addedTreeDepthTooLarge(u8)toTxBuilderErrorbut did not add the corresponding arm totx_builder_error_code()inshekyl-ffi, breaking CI compilation on all platforms. AddedTreeDepthTooLarge(_) => -27.
[core-v3.1.0] - 2026-04-13
🔄 Changed
- Dev merged into main. 128 commits from
devpromoted tomainincluding: FCMP++ curve-tree integration, hybrid PQC KEM scanning, shekyl-cli full parity, shekyl-address Bech32m encoding, native Rust transaction signing, staking enhancements, wallet/api removal, and ZeroMQ cleanup. Tagged ascore-v3.1.0for GUI wallet CI pinning.
✨ Added
-
shekyl-clifull parity with simplewallet (40 of 81 commands). Therust/shekyl-cli/crate now covers all actively-used simplewallet functionality. Key additions since the initial scaffold:- Security-hardened UX:
display.rsfor secret display with TTY checks, multiplexer warnings, best-effort scrollback clear, and honest residual-scrollback warning.errors.rsfor JSON-RPC error sanitization (strips paths/hex;--debugroutes raw errors to stderr or 0600 log file, never stdout). Context-specificconfirm_dangerous()tokens for destructive operations (sweep amount, address prefix, acknowledgment phrase). - Stateless account model:
ReplSessionholds session-default account on REPL stack;ResolvedCommandenum resolves--account Nat parse time. No wallet-level current-account state.--subaddr-index/--subaddr-indicesfor subaddress selection. - Independent daemon client:
daemon.rsusing ureq (rustls backend, pinned) forchain_health. SOCKS stream isolation via distinct auth username.--daemon-ca-certand--proxyCLI flags. Differentiated error reporting (5 failure modes). - Staking:
stake,unstake,claim,staking_info,chain_health. - Keys:
viewkey,spendkeywith terminal safety;export_key_images(0600 permissions,--since-height,--all);import_key_imageswith format validation. - Proofs:
get_tx_key,check_tx_key,get_tx_proof,check_tx_proof,get_reserve_proof,check_reserve_proof. - Wallet ops:
password(old-first with fast-fail validation),rescan(confirm_dangerous),sweep_all(privacy warning),show_transfer. - Offline signing:
describe_transfer,sign_transfer,submit_transfer;--do-not-relayontransfer. - Signing:
sign,verify(domain separation documented),version,wallet_info(no filename). - Input validation:
validate.rswith hex, txid, address, and input-length validators. - Fuzz tests:
proptestdev-dependency with 14 property tests for amount parsing, hex validation, address validation, and argument parsing. - Parity matrix:
docs/CLI_PARITY_MATRIX.mdmaps all 81 simplewallet commands to shekyl-cli equivalents or explicit out-of-scope with reasons. Phase 3 deletion gate defined. - Categorized help with per-command usage docs and domain-separation note on sign/verify.
- Security-hardened UX:
-
CI gate:
dalek-ff-groupversion isolation. Added a workflow step that assertsshekyl-ffi's normal dependency tree never pulls indalek-ff-groupv0.4. The 0.4 version is allowed transitively insideciphersuiteinternals but must never be used directly by Shekyl code. -
CI lint: no debug macros in production Rust. Added a workflow step that rejects
eprintln!,dbg!, andprintln!in production Rust code (excluding test modules, build scripts, binary entry points, and the economics simulator). Prevents accidental debug logging from reaching production builds. -
CI lint: BOOST_FOREACH guard. Added a workflow step that fails if any
BOOST_FOREACHusage is reintroduced via upstream cherry-picks. All 31 prior instances were replaced with range-based for loops.
🔄 Changed
- CI lint: exclude
shekyl-clifrom debug-macro ban. The interactive CLI REPL legitimately usesprintln!/eprintln!for terminal output. The lint now skipsrust/shekyl-cli/to avoid false positives on binary crate I/O.
🐛 Fixed
-
[CONSENSUS] Genesis TX blobs upgraded to v3 wire format. The hardcoded
GENESIS_TXhex incryptonote_config.h(mainnet, testnet, stagenet) was still in the legacy v2 format, missing theenc_amountsandoutPkarrays required by the currentserialize_rctsig_base. Updated all three blobs to v3 (tx.version = 3) with zero-filledenc_amounts/outPkforRCTTypeNullcoinbase. This was the root cause ofcore_testsSEGFAULT,block_weightfailure, and wallet init failures in CI. -
JSON serialization now includes
enc_amounts/commitmentsforRCTTypeNullcoinbase. ThetoJsonValue/fromJsonValueforrct::rctSigpreviously skipped these fields forRCTTypeNull, but the binary wire format serializes them for all RCT types since the v3 format change. This caused JSON round-trip failures for coinbase transactions. -
HTTP_Client_Auth.MD5_authtest used hardcoded empty cnonce. The test computed the expected MD5 digest withcnonce=""while the productionhttp_auth.cppgenerates a random cnonce. Fixed to extract the actual cnonce from the parsed auth response.
🗑️ Deprecated
-
test::make_transactionring-style helper. The helper constructs Monero-era ring-signature source entries incompatible with v3/FCMP++ transaction construction.BulletproofPlusTransactionisGTEST_SKIP'd pending FCMP++ test infrastructure. -
[CONSENSUS-ADJACENT] Branch layer depth validation off-by-one in
shekyl-tx-builder. The rulec1 + c2 == depthwas corrected toc1 + c2 + 1 == depth(layer 0 is the leaf hash and has no branch entry). The previous rule incorrectly rejected valid witnesses at depth=1 and accepted structurally wrong branch counts at all other depths. Discovered by the FFI signing round-trip test introduced in this release. Verifier side verified: uses proof-structure-implicit depth enforcement (no explicit c1/c2 check needed). Additionally, validation now enforces the spec-correct C1/C2 alternation split (c1 == c2orc1 == c2 + 1), the error.rs doc was corrected (previously stated the relationship backwards), andMAX_TREE_DEPTH=24was added as a named constant inshekyl-fcmpwith enforcement in both prover and verifier. See FOLLOWUPS.md for the full audit trail.
✅ Testing
- FFI signing round-trip test rewritten to use
shekyl_sign_fcmp_transaction.rust/shekyl-ffi/tests/signing_round_trip.rsnow exercises the full C-ABI FFI boundary: KEM keypair generation, output construction, output scanning, curve tree leaf/root computation, JSON serialization ofFcmpSignInput+OutputInfo, signing viashekyl_sign_fcmp_transaction, and verification viashekyl_fcmp_verify. Runs 10 iterations with different random seeds. Previously calledproof::provedirectly, bypassing FFI JSON parsing, key derivation, and buffer management.
📚 Documentation
- FFI header upgraded to
///doc comments (Phase 6 completion). Converted all//function and struct documentation comments insrc/shekyl/shekyl_ffi.hto///Doxygen-style. Covers all ~70 FFI exports: output construction/scanning, key image computation, FCMP++ prove/verify, wallet proofs, cache encryption, KEM operations, Bech32m encoding, curve tree hashing, seed derivation, and daemon RPC. Rewrote theSHEKYL_PROVE_WITNESS_HEADER_BYTEScomment fromDEPRECATED/TODOlanguage to document its role as test infrastructure forgenRctFcmpPlusPlusincore_tests.
🔄 Changed
-
simplewalletmarked deprecated. Added a yellow deprecation banner tosimplewallet.cppstartup: "shekyl-wallet-cli is deprecated and will be removed. Use shekyl-cli instead." No new features will be added; the binary will be deleted onceshekyl-clireaches parity. -
Axum RPC binds to standard port. When
--no-rust-rpcis not set, the Axum daemon RPC server now binds to the standard RPC port (11029/12029/13029) and the epee HTTP listener is skipped. Falls back to epee on Axum startup failure. Previously Axum bound toepee_port + 10000. -
Production
eprintln!removed from Rust FFI. Replaced 6eprintln!calls inshekyl-ffi/src/lib.rserror handlers with silent error suppression (the C++ caller checks the bool return). Converted 1eprintln!inshekyl-daemon-rpc/src/ffi_exports.rstotracing::error!. -
Test code migrated to remove all calls to deleted crypto/device functions. Updated 14 test files across
tests/crypto/,tests/unit_tests/,tests/core_tests/,tests/performance_tests/,tests/trezor/, andtests/benchmark.cppto remove references toderive_public_key,derive_secret_key,derivation_to_scalar,derive_subaddress_public_key,derive_view_tag,is_out_to_acc,lookup_acc_outs,ecdhDecode,ecdhHash,genCommitmentMask,generate_key_image_helper, andgenerate_output_ephemeral_keys. Where inline key derivation was needed (block/miner-tx construction tests), local helpers using Ed25519 primitives (hash_to_scalar,ge_scalarmult_base,sc_add) replace the deleted functions. Legacy output scanning inchaingen.cppandchain_switch_1.cppfalls through to the v3 scan path. Alladditional_tx_keysparameters removed fromconstruct_tx_and_get_tx_keycall sites. Benchmark harnesses forderive_subaddress_public_keyand per-tx scanning removed.
🗑️ Removed
-
Complete ZMQ removal. Deleted the entire ZeroMQ subsystem: ZMQ pub/sub (
zmq_pub.cpp), ZMQ RPC server (zmq_server.cpp,daemon_handler.cpp,daemon_messages.cpp), low-level ZMQ helpers (net/zmq.cpp), message schema (message.cpp,daemon_rpc_version.h,rpc/fwd.h), and therpc_pub,daemon_rpc_server,daemon_messagesCMake targets. Removedlibzmqbuild dependency from root CMakeLists,contrib/depends, and all link targets. Deleted 3 test files (zmq_rpc.cpp,txpool.py,python-rpc/framework/zmq.py) and thezeromq.mkdepends recipe with its patches. Removed--zmq-rpc-bind-ip,--zmq-rpc-bind-port,--zmq-pub,--no-zmqCLI arguments. ZMQ was a duplicate, unauthenticated RPC surface inherited from an abandoned Monero "migrate RPC to ZMQ" effort. It had zero first-party consumers, leakeddo_not_relaytransactions, and its tests had been broken for 82+ consecutive CI runs, polluting the test signal during the FCMP++ migration. Ports 11025/12025/13025 are now reserved. Re-audit follow-up: removed stale#include "rpc/daemon_messages.h"and two ZMQ-schema-dependent tests (DaemonInfo,HandlerFromJson) fromjson_serialization.cpp, and fixed daemon link order (rpcafter${SHEKYL_DAEMON_RPC_LINK_LIBS}) to resolve circular FFI back-references previously satisfied transitively throughdaemon_rpc_server. -
wallet/api/C++ wrapper layer deleted (~3,900 lines). Thesrc/wallet/api/directory (22 files) wrappedwallet2for GUI consumption. With the Tauri GUI usingwallet2_ffivia Rust, no production consumer remained. Removed the directory,add_subdirectory(api)from wallet CMakeLists,wallet/apiincludes and sizeof reporters fromobject_sizes.cpp, broken includes insubaddress.cppand trezor tests,wallet_apilink target from trezor CMakeLists, and CI--target wallet_apibuild steps. -
libwallet_api_tests/test suite deleted (~1,300 lines). Removed thetests/libwallet_api_tests/directory and its CMake entry. Cleaned up the Makefile'slibwallet_api_testsctest exclusions (originally disabled for Issue #895, now fully removed). Also removed thewallet_api_testsclass and implementation from trezor tests. -
load_deprecated_formats/is_deprecateddead code excised (Phase 6 completion). Removed theis_deprecated()method,is_old_file_formatmember,m_load_deprecated_formatsmember and its getter/setter fromwallet2.h. Deleted theis_deprecated()definition, JSON save/load ofload_deprecated_formats, the non-JSON wallet keys file fallback (now a hard error), and the boostportable_binary_iarchiveversion\003/\004branches inparse_unsigned_tx_from_strandparse_tx_from_strfromwallet2.cpp. Removed theset_load_deprecated_formatscommand, itsCHECK_SIMPLE_VARIABLEentry, settings display line, and theis_deprecated()upgrade flow fromsimplewallet.cpp/.h. Shekyl is v3-from-genesis; there are no legacy non-JSON wallet files or boost-serialized transaction blobs to load. -
additional_tx_keys/additional_tx_pub_keysinfrastructure fully removed. Deleted member variables, struct fields, serialization entries, and function parameters referencing additional transaction keys fromwallet2.h,wallet2.cpp,cryptonote_tx_utils.h/.cpp,cryptonote_format_utils.h,device.hpp,device_default.hpp/.cpp, anddevice_ledger.hpp/.cpp. Inwallet2.cpp, removed alladditional_tx_pub_keys/additional_tx_keyslocal variables, derivation computation loops,m_additional_tx_keysmap operations,etd.m_additional_tx_keysexport/import paths, and updated function definitions (get_tx_key_cached,get_tx_key,set_tx_key,check_tx_key,get_tx_proof) to match the simplified header signatures. Theconceal_derivationdevice method implementations were updated to match the simplified signatures (no additional keys/derivations parameters). TheABPkeysstruct no longer carriesadditional_key. Cleaned up all remaining call sites acrosswallet2_ffi.cpp,wallet/api/wallet.cpp,simplewallet.cpp,wallet_rpc_server.cpp, andtrezor/protocol.cpp— removing additional-key parsing loops, serialization, and pass-through parameters.get_additional_tx_pub_keys_from_extrais now an inline stub returning an empty vector. In V3, per-output KEM ciphertexts replace additional tx keys; there is only one tx pubkey per transaction. -
derive_public_key,derive_secret_key, andderivation_to_scalarremoved from the device interface chain. Deleted the pure virtual declarations fromdevice.hppand all override implementations fromdevice_defaultanddevice_ledger. Also deletedderive_public_keyandderive_secret_keyfromcrypto.cpp/crypto.h(keptderivation_to_scalarin crypto, still needed byderive_subaddress_public_key). Removed associated performance test files. These Keccak-based one-component key derivation helpers are superseded by the V3 HKDF two-component output key derivation incryptonote_tx_utils. -
out_can_be_to_acc,is_out_to_acc_precomp, andderive_view_tagdead code removed. Deleted the Keccak-basedout_can_be_to_accandis_out_to_acc_precompfunctions fromcryptonote_format_utils, thederive_view_tagfunction fromcrypto, and thederive_view_tagvirtual method from the device interface chain (device.hpp,device_default,device_ledger). Removed associated performance tests. These functions were superseded by the X25519/HKDF view-tag derivation path in the V3 transaction format. -
ecdhHashandgenCommitmentMaskdead code removed. Deleted theecdhHashandgenCommitmentMaskfunction definitions fromrctOps.cpp, their declarations fromrctOps.h, thegenCommitmentMaskvirtual method from the device interface chain (device.hpp,device_default,device_ledger), and theecdhDecodeunit test that depended on them. These Keccak-based helpers were superseded by HKDF-derived amount encryption in V3. -
Ring signature / decoy infrastructure removed from wallet2. Removed
fake_outs_countparameters fromcreate_transactions_2,create_transactions_all,create_transactions_single, andcreate_transactions_from. Removedtransfer_selected_rct'sfake_outputs_countandoutsparameters. Deletedget_output_relatedness,outs_unique,m_print_ring_members, andm_ringsbookkeeping. FCMP++ eliminates ring signatures, making decoy selection and output relatedness scoring dead code.
🔒 Security
-
m_combined_shared_secretchanged toscrubbed_arr<uint8_t, 64>(Phase 6, Gate 3). Replacedstd::vector<uint8_t>withtools::scrubbed_arr<uint8_t, 64>in bothtransfer_detailsandexported_transfer_details. This ensures zero-on-drop semantics consistent withm_yandm_mask. A booleanm_combined_shared_secret_setflag replaces size-based emptiness checks. All serialization (epee and Boost) updated with safe vector round-trip conversion. -
WalletState invariant enforcement (Phase 6, Gate 5b). Added
check_invariants()toWalletStateverifying 8 structural properties (balance consistency, spendable/spent partition, key image correspondence, etc.).debug_assert!fires after every mutation in debug builds. Property test (Gate 5c) exercises random operation sequences against invariant checks.
✨ Added
-
PQC output round-trip property tests (Phase 6, Gate 1).
prop_round_trip.rsexercisesconstruct_output→scan_output_recover→derive_proof_secrets→compute_key_imagewith random keys and amounts viaproptest. Asserts determinism (same inputs → identical outputs) and non-zero secrets (ho,y,z,k_amount,key_image). Includes boundary cases foramount=0andamount=u64::MAX. Runs with--releasein CI. -
Wallet cache AEAD tests (Phase 6, Gate 2).
cache_crypto.rscovers encrypt/decrypt round-trip, version mismatch detection (returns -1 before AEAD decryption attempt), wrong-key auth failure, empty ciphertext, and truncated ciphertext. Sub-case A2 proves version check ordering by corrupting ciphertext and asserting version mismatch fires first. -
100-iteration signing round-trip stress test (Phase 6, Gate 4).
test_gate4_signing_round_trip_100inproof_round_trip.rsruns full outbound prove+verify cycle 100 times with unique randomness per iteration. -
unmark_spentunit tests (Phase 6, Gate 5a). Five tests covering: reversal to spendable pool, unknown key image noop, idempotent on already-unspent, partial set behavior, and invariant preservation after unmark. -
Random-sequence invariant property test (Phase 6, Gate 5c).
proptestdrives random sequences ofAddOutputs,MarkSpent,UnmarkSpent,Freeze,Thaw, andReorgoperations, assertingcheck_invariants()after each step. -
Sync bookkeeping tests (Phase 6, Gate 7). Mock-block-driven tests for
WalletStatemutations: progress monotonicity, spend detection, reorg state restoration, empty block height advancement, and spend/unmark round-trip. Explicitly documented as bookkeeping-only (not integration against a real daemon). -
CI grep gates (Phase 6). Seven blocking grep gates in
build.yml:shekyl_yabsence,derivation_to_y_scalarabsence, legacy RCT type absence, v1/v2 tx version branch absence,HASH_KEY_TXPROOFabsence,combined_shared_secretconfinement to wallet boundary,ecdhEncode/ecdhDecodeconfinement to Ledger gate. All run withoutcontinue-on-error. -
FFI header documentation (Phase 6).
shekyl_ffi.hnow has Doxygen-style file-level documentation covering the memory model, secret handling conventions, and error reporting contract.
🗑️ Removed
-
derivation_to_y_scalardeleted (Phase 6). Removed the function body fromcrypto.cpp, declarations fromcrypto.h, and all call sites inderive_public_keyandderive_subaddress_public_key. The"shekyl_y"salt no longer appears in the binary. -
Test stubs 9-10 deleted (Phase 6). Removed
#[ignore]placeholder teststest_09_watch_only_outbound_proof_errorandtest_10_restored_wallet_outbound_proof_errorfromproof_round_trip.rs. Future implementations tracked inWALLET_STATE_MIGRATION.md. -
Dead v1/v2 transaction branches in consensus (Phase 5).
check_tx_outputsnow rejectstx.version < 3instead of< 2. Removed redundantif (tx.version >= 2)zero-amount guard (now unconditional). Tightened coinbase version check from>= 2to>= 3. Removed deadtx.version < 3early return incheck_commitment_mask_valid. Commitment mask checks are now unconditional (version is always >= 3). -
Dead legacy code excision (Phase 6 completion). Deleted
decodeRctSimpleand its overload fromrctSigs.cpp/.h. Deletedtools::decodeRctwrapper and all callers inwallet2.cpp. Deletedgenerate_output_ephemeral_keysdeclaration fromcryptonote_tx_utils.h. Deletedtx_proof.cppunit test (referenced removedcrypto::generate_tx_proof_v1). Deletedis_out_to_acc.hperformance test and its registrations. -
generate_key_image_helper/generate_key_image_helper_precompfully removed. Migrated remaining production callers inwallet2.cpp(export_key_images, twoimport_outputsoverloads) to the v3 HKDF path viashekyl_derive_proof_secretsFFI. Replaced deadelsebranch incryptonote_tx_utils.cpp::construct_tx_with_tx_keywith a hard error. Replacedscan_output'sgenerate_key_image_helper_precompcall with a v3-only assertion (function is dead for v3 scanning). Deleted both function definitions fromcryptonote_format_utils.cpp/.h, thecompute_key_imagevirtual method fromdevice.hppand its Trezor override indevice_trezor.hpp/.cpp. Updated test callers inchaingen.cppandtx_validation.cppto use v3sc_add(ho, b)derivation.
🔒 Security (Phase 5 Audit Notes)
-
Consensus hardening: commitment mask validation verified (Phase 5). Audited
check_commitment_mask_validinblockchain.cpp: confirms rejection of identity commitment (mask=0, amount=0), generator-point commitment (mask=1, amount=0), and coinbasezeroCommit(amount)form (mask=1, any amount). Called unconditionally for both miner transactions and regular transactions. -
y=0 defense-in-depth verified (Phase 5). Confirmed construction-time
assert!(y != [0u8; 32])andassert!(ho != [0u8; 32])inderive_output_secrets(Rust, release-mode assert). Both sender (construct_output) and receiver (scan_output_recover) hit the same assert. Documented inPOST_QUANTUM_CRYPTOGRAPHY.mdwith full defense stack analysis.
✨ Added
-
GUI wallet native-sign activation (Phase 4a). Added
native-signfeature to the GUI wallet'sshekyl-wallet-rpcdependency. The transfer path is now: C++ prepare → Rust sign → C++ finalize. -
Scanner keys FFI export (Phase 4b). Added
wallet2_ffi_get_scanner_keysto the wallet2 FFI layer, returning all keys needed by the Rust scanner (spend/view secrets, X25519 SK, ML-KEM DK) as JSON. Addedget_scanner_keyswrapper method toWallet2. -
Hybrid PQC KEM scanner (Phase 3a).
shekyl-scannernow scans blocks using the V3 two-component key derivation: X25519 + ML-KEM-768 hybrid KEM. TheInternalScanner::scan_transactionpipeline parsesTX_EXTRA_TAG_PQC_KEM_CIPHERTEXT(0x06), applies X25519 view-tag pre-filtering (~99.6% rejection), and callsscan_output_recoverfor full KEM decapsulation, HKDF secret derivation, amount decryption, and B' recovery. Key images are computed natively in Rust viahash_to_point+compute_output_key_image. Legacy ECDH scan path removed. -
RecoveredWalletOutputstruct. New scan result type carrying all KEM-derived secrets (ho,y,z,k_amount,combined_shared_secret), the computedkey_image, and decryptedamountalongside the baseWalletOutput. ImplementsZeroizeOnDrop— secrets are wiped when the struct leaves scope. -
TransferDetailsPQC fields andeligible_height. Extended withho,y,z,k_amount,combined_shared_secret(allZeroizing) andeligible_height: u64(block_height + SPENDABLE_AGE). Outputs beloweligible_heightare immature (no curve-tree path) and cannot be spent.is_spendable()enforces this gate. -
WalletStateKEM-aware processing.process_scanned_outputsnow populates all PQC fields fromRecoveredWalletOutput, sets key images at scan time, and performs duplicate output key detection (burning bug).spendable_outputsfilters oneligible_height. -
unmark_spentfor rollback.WalletState::unmark_spentreverses spent marks on outputs whose signing round succeeded but whose finalize step failed (daemon rejection, relay timeout). Prevents phantom-spent balance loss. -
Background sync loop (Phase 3b).
shekyl-scanner::sync::run_sync_looppolls the daemon RPC for new blocks, feeds them through the hybrid KEM scanner, detects spent outputs via key-image matching against block inputs, and emitsSyncProgressevents after each block. Cancellation-safe viatokio_util::CancellationToken. Configurable flush interval: every 100 blocks on desktop, every block on mobile (OS can kill without warning). -
BalanceSummaryuseseligible_height. Timelock categorization now readstd.eligible_heightdirectly instead of recomputing fromblock_height + DEFAULT_LOCK_WINDOW. -
ViewPairextended with KEM keys. Addedx25519_skandml_kem_dkfields toViewPairfor hybrid KEM decapsulation. The scanner requires both the X25519 secret and ML-KEM decapsulation key.
🐛 Fixed
-
Stale
fake_outs_countarguments in wallet transaction creation. Removed vestigial0(decoy count) from 9 call sites acrosswallet2_ffi.cpp,wallet_rpc_server.cpp, andwallet/api/wallet.cppthat no longer matchcreate_transactions_2,create_transactions_all, andcreate_transactions_singlesignatures after ring removal. -
Test compilation:
wallet_tools.cppandtransactions_flow_test.cpp. Replaced removedtd.is_rct()calls withtrue(all Shekyl outputs are RCT), changedtools::wallet2::get_outs_entryto the local typedef fromchaingen.h, and removed stalemix_in_factorargument in the functional test. -
PQC doc label error. Fixed incorrect HKDF label reference in
POST_QUANTUM_CRYPTOGRAPHY.md: the output-key check useshowith labelshekyl-output-x, notshekyl-pqc-output(which is the ML-DSA seed label). -
Test compilation:
json_serialization.cppaggregate init. Replaced brace-enclosed initializer list fortx_source_entrywith explicit member assignment. The struct is no longer an aggregate (user-declared destructor forhowiping) and the old initializer also referenced a removedreal_out_additional_tx_keysfield. -
Multi-output scan bug. Removed erroneous
breakinInternalScanner::scan_transactionthat exited the output iteration loop after finding the first matching output. Transactions with multiple wallet outputs (e.g., payment + change) now detect all of them. -
Reorg handling in
handle_reorg. RewroteWalletState::handle_reorgto use(height, hash)pairs instead of treating height as a direct vector index. Correctly handles non-genesis-aligned and sparse sync histories.synced_heightis now derived from the last remaining block entry. -
Reorg detection in sync loop.
run_sync_loopnow compares each incoming block'sheader.previoushash against the wallet's stored hash for the prior height. On mismatch, walks backwards to find the fork point and callshandle_reorgbefore resuming. -
Block fetch retry with backoff. Per-block
get_scannable_block_by_numbercalls now retry up to 5 times with exponential backoff (500ms initial, capped at 30s) instead of immediately aborting the sync loop on transient failures. -
Secure memory wiping.
TransferDetailsnow implements bothZeroize(covering all fields includingkey,commitment, andfcmp_precomputed_path) andDrop(callszeroize()on drop).WalletStateimplementsDropto wipe all transfers, key images, pub keys, and block hashes. Removed unsafe#[derive(Clone, Debug)]fromTransferDetails;Debugis now manual and redacts secret fields. -
Misleading payment ID comment. Corrected comment in
scan.rsthat incorrectly described ECDH-based XOR decryption for payment IDs; V3 transactions do not use encrypted payment IDs. -
Always-true pattern in sync loop. Removed
if let Some(tx_hashes) = Some(&scannable.block.transactions)which was a no-op guard. Block transactions are now iterated directly.
🔄 Changed
-
EncryptedAmountwire format fix. The RustEncryptedAmountstruct (inshekyl-oxide::fcmp) now correctly includes bothamount: [u8; 8]andamount_tag: u8, matching the C++ 9-byte wire format. Previously only the 8-byte amount was read, causing silent data misalignment. -
Scanner::newsignature. Now requires the wallet'sspend_secret(Zeroizing<[u8; 32]>) for native key image computation at scan time. BothScanner::newandGuaranteedScanner::newupdated. -
Deterministic KEM encapsulation from
tx_key_secret.construct_outputnow derives X25519 ephemeral keys and ML-KEM ciphertexts deterministically via HKDF-SHA-512 (derive_kem_seed), eliminating the need to cache per-output shared secrets. The sender can re-derivecombined_ssat proof time fromtx_key_secretand public data. -
Proof pipeline helpers in
shekyl-crypto-pq. Seven new functions:rederive_combined_ss,derive_proof_secrets,derive_output_key,recover_recipient_spend_pubkey,decrypt_amount,compute_output_key_image, andcompute_output_key_image_from_ho. These support the V3 tx_proof / reserve_proof / key-image protocols. The narrowProofSecrets(ho, y, z, k_amount)projection ensurescombined_ssnever crosses the FFI boundary. -
ProofSecretswidened to includez. The Pedersen commitment mask is now part of the proof secrets projection, enabling directC = z*G + amount*Hverification in TX proofs.derive_proof_secretspasseszthrough instead of discarding it. -
shekyl-proofscrate: full Phase 1a implementation. Three modules:dleq.rs: Two-base Schnorr DLEQ proof with domain separatorshekyl-reserve-proof-dleq-v1and full base binding in the challenge hash (G,Hp(O),R1,R2,P,I,msg). 6 unit tests.tx_proof.rs: Outbound (101+128N bytes) and inbound (69+128N bytes) proof generation and verification. Domain-separated Schnorr signatures (shekyl-outbound-tx-proof-v1,shekyl-inbound-tx-proof-v1). Per-outputho,y,z,k_amountwith algebraic output key and commitment checks.reserve_proof.rs: Reserve proof (69+192N bytes) with per-output DLEQ key image binding.enc_amountsourced from blockchain, not from proof.- Version assertion (v1) before any cryptographic work. 4-byte output_count (u32 LE) supporting up to 2³²−1 outputs per proof.
- 10-point round-trip test skeleton (exit criterion for Phase 5,
#[ignore]).
-
FCMP_PLUS_PLUS.md section 21: Wallet Proof Structure. Genesis-native proof design rationale. Documents the Schnorr/KEM decomposition, reserve proof DLEQ requirement, HKDF binding argument for z-omission in reserve proofs, and the
enc_amount-from-chain invariant. -
Phase 1b FFI exports (PR-wallet). New exports in
shekyl_ffi.h:shekyl_scan_and_recover: Merged scan + key image in one call. All secret outputs write directly intotransfer_detailsfields (no intermediate scratch buffers).persist_combined_ssflag controls whethercombined_ssis returned or wiped internally (hot vs cold).shekyl_compute_output_key_image/_from_ho: Key image computation for the 2 remaining sites (stake claim, tx_source_entry).shekyl_sign_fcmp_transaction: Collapsed signing. C++ passes wallet master spend keyb+ per-input{combined_ss, output_index, ...}. Rust derivesx = ho + bandyinternally via HKDF. C++ never touchesx.shekyl_derive_proof_secrets: Helper writingho,y,z,k_amountdirectly to caller-provided destination addresses.shekyl_encrypt_wallet_cache/shekyl_decrypt_wallet_cache: AEAD encryption with AAD binding oncache_format_version. Distinct error codes for version mismatch (-1), auth failure (-2), and format error (-3).- 6 proof FFI exports:
shekyl_generate_tx_proof_outbound,shekyl_verify_tx_proof_outbound,shekyl_generate_tx_proof_inbound,shekyl_verify_tx_proof_inbound,shekyl_generate_reserve_proof,shekyl_verify_reserve_proof. Signatures stabilized; wiring toshekyl-proofsinternals deferred to Phase 2e.
-
shekyl-chachaAEAD extension. Addedchacha20poly1305(v0.10) support:encrypt_with_aadanddecrypt_with_aadwrapping XChaCha20-Poly1305. No hand-rolled AEAD — nonce handling, constant-time tag comparison, and AD framing delegated to audited crate. 6 new tests. -
RecoveredOutputnow includescombined_ss. The scan result carries the 64-byte combined shared secret so the merged scan FFI can optionally persist it without re-doing KEM decapsulation. Wiped byZeroizeOnDrop. -
ML-KEM shared secret
Zeroizingwrap (W5 fix). All 4 production sites whereml_ss.into_bytes()produces a bare stack-local now wrap the result inZeroizing<[u8; 32]>, ensuring the ML-KEM shared secret bytes are zeroed on scope exit. Closes the W5 correlation leak. -
Fixed stale
shekyl_construct_outputC header. Added missingtx_key_secretparameter to match the Rust implementation. -
KEM derivation KAT vectors.
docs/test_vectors/KEM_DERIVE_V1_KAT.jsonwith 8 pinned vectors forderive_kem_seed. Serves as tripwire against silent behavior changes fromfips203orcurve25519-dalekupgrades. -
fips203exact version pin. Pinned to=0.4.3with audit comment explaining theDummyRng::fill_bytes = unimplemented!()risk. -
Fuzz target for
derive_output_key. Exercisesderive_output_keyandrecover_recipient_spend_pubkeyround-trip with fuzzer-supplied inputs. -
Ledger V3 hard gate.
device_ledger.cppnow has a#errorthat fires whenWITH_DEVICE_LEDGERis defined, preventing silently broken builds. The Ledger APDU protocol has not been updated for V3 two-component keys. -
Fuzz target for malformed KEM ciphertexts on scan. New
fuzz_scan_malformed_ctexercises corrupted, truncated, and random ML-KEM ciphertexts throughscan_output_recoverwith a valid wallet KEM secret. Validates ML-KEM implicit rejection + downstream algebraic checks fail closed without panics or timing leaks.
📚 Documentation
-
Security properties of the derivation section in
docs/POST_QUANTUM_CRYPTOGRAPHY.md. Documents the y==0 defense-in-depth stack (construction assert + probabilistic impossibility + fuzz coverage), explains why a wire-level y==0 check is impossible, documents malformed KEM ciphertext handling through ML-KEM implicit rejection, view-tag pre-filter behavior on adversarial match grinding, and the wallet cache version gate requirement for PR-wallet. -
Tightened malformed KEM ciphertext framing. Reframed
amount_tagas a ~99.6% cheap pre-filter (performance optimization), not a security gate. Commitment algebraic checkC == z*G + amount*His the soundness barrier. Documented structural independence of the two algebraic checks (different HKDF labels, different scalar families). -
Wallet cache version gate hardened. Added mandatory AAD binding (include
cache_format_versionin XChaCha20-Poly1305 AAD to prevent version-confusion attacks) and hard no-migration policy (delete and resync from seed, never in-place migration).
🗑️ Removed
-
ecdhTuple/ecdhEncode/ecdhDecoderemoval. Deleted the Monero-era ECDH amount-masking struct and encode/decode functions fromrctTypes.h,rctOps.h/.cpp,device.hpp,device_default.hpp/.cpp,device_ledger.hpp/.cpp, and the Trezor protocol files. Theenc_amount_to_ecdh_compatshim is deleted. -
check_tx_key_helper/is_out_to_accdeletion. Both overloads ofwallet2::check_tx_key_helperandwallet2::is_out_to_accremoved. These usedderive_public_key(Keccak Category 1) and the old ecdhDecode path. Replaced by KEM-based proof FFI round-trip incheck_tx_key. -
crypto::generate_tx_proof/generate_tx_proof_v1/check_tx_proofdeletion. Monero-era DH-based Schnorr proof functions removed fromcrypto.cpp,crypto.h,device_default.cpp,device_ledger.cpp,device.hpp, and derived device headers.HASH_KEY_TXPROOF_V2removed fromcryptonote_config.h. -
ecdh.rsmodule stub cleanup. Removed orphanedmod ecdhdeclaration and associated test functions fromshekyl-tx-builder(module file was previously deleted, declaration left behind). -
V3-from-genesis Boost serialization purge (
wallet2.h). Deleted allif (ver < N)migration branches from Boostserializefunctions fortransfer_details,unconfirmed_transfer_details,confirmed_transfer_details,payment_details,address_book_row,unsigned_tx_set,signed_tx_set,tx_construction_data, andpending_tx. Deleted theinitialize_transfer_detailshelper (both saving and loading overloads). Reset allBOOST_CLASS_VERSIONmacros to 1 (genesis version). Addedassert(ver == 1)guards. Epee cache envelopeif (version < N)branches also removed, replaced withassert(version == 2). Staking fields (m_staked,m_stake_tier,m_stake_lock_until,m_last_claimed_height) and new Phase 2b field (m_k_amount) added to thetransfer_detailsBoost serializer. Legacym_rctfield no longer serialized (previously removed from struct).
🔄 Changed
-
Phase 2e: Proof functions collapsed to Rust FFI (PR-wallet). All six wallet proof functions (
get_tx_proof,check_tx_proof,get_reserve_proof,check_reserve_proof) now delegate to theshekyl-proofsRust crate via the FFI bridge.check_tx_keyalso uses the FFI round-trip (generate outbound proof + verify with on-chain data). The intermediate C++ helperscheck_tx_key_helper(both overloads) andis_out_to_acchave been deleted. Newgather_on_chain_proof_datahelper extracts output keys, commitments, encrypted amounts, and KEM ciphertexts from transactions for proof verification. Reserve proof wire format now includes output locators (txid + index_in_tx) as a header so the verifier can independently fetch on-chain data from the daemon. -
Phase 2f: Category 1 Keccak deletions (PR-wallet). Deleted Monero-era DH-based proof functions from the crypto layer:
crypto::generate_tx_proof,crypto::generate_tx_proof_v1,crypto::check_tx_proof, along with their device implementations (device_default, device_ledger) and virtual interface declarations. RemovedHASH_KEY_TXPROOF_V2fromcryptonote_config.h. Removed orphanedecdh.rsmodule declaration and tests fromshekyl-tx-builder. Remaining Category 1 functions (derive_public_key,derivation_to_scalar,derive_subaddress_public_key,decodeRctSimple) still have live callers in scan/sign paths and are deferred to Phase 3 migration.ecdhHashandgenCommitmentMaskhave been removed. -
Phase 2d: Collapsed signing via
shekyl_sign_fcmp_transaction(PR-wallet). The CLI wallet'stransfer_selected_rctnow calls the Rust collapsed signing FFI instead of C++genRctFcmpPlusPlus. C++ builds JSON arrays ofFcmpSignInput(per-inputcombined_ss,output_index, tree layers) andOutputInfo(per-outputcommitment_mask,enc_amount), then unpacks the returnedSignedProofs(BP+ blob, FCMP++ proof, pseudo-outs, commitments, enc_amounts) intotx.rct_signatures. Rust owns all witness assembly — C++ never touches the ephemeral spend secretx.genRctFcmpPlusPlusis deprecated (retained only forchaingen.cpptest infrastructure). -
Rust
sign_transactionupdated for v3 HKDF semantics (PR-wallet).OutputInfonow carriescommitment_mask: [u8; 32]andenc_amount: [u8; 9](pre-derived byconstruct_output), replacing the oldamount_keyfield.SignedProofs.enc_amountswidened from 8 to 9 bytes. The signing pipeline uses pre-derived HKDF masks for BP+ instead of generating random ones, and uses pre-encrypted amounts instead of Keccak-based ECDH encoding. -
wallet2_ffi.cppenc_amountsfield name fix. The native-sign finalize path now readsenc_amountsfrom RustSignedProofsJSON (was incorrectly readingecdh_amounts). -
enc_amountsfield comment updated inrctTypes.h. Clarifies that byte [8] is the HKDF-derivedamount_tagAAD, documents the Rust scanner validation behavior (reject on mismatch), and removes the staleRESERVED_AMOUNT_TAG_PLACEHOLDERreference. -
Comprehensive CLI User Guide (
docs/USER_GUIDE.md). Covers all shipped executables, daemon operation (flags, config file, console commands), wallet CLI (create, restore, send, receive, proofs), staking (tiers, unstake, claim, accrual rules), mining, PQC multisig (file-based workflow, size table), anonymity networks (Tor/I2P), wallet RPC, blockchain utilities, security/backup, and troubleshooting. Mirrors the GUI wallet guide structure for easy cross-referencing. -
C++/Rust cross-validation test for
total_weighted_stake. New test instaking.cppconstructs the same staker set via both the C++ 128-bit cache accumulation and the Rust FFI, then asserts byte-equality of the results. Prevents spec/impl drift regression. -
u128saturation test. Demonstrates that the u128 weighted stake does NOT saturate where u64 would (100M stakers at 100 SKL, tier 2), and verifies reward computation remains correct with the large denominator. -
LMDB write atomicity audit. Comprehensive audit of all
BlockchainLMDBwrite paths (block connect, block pop, txpool, alt blocks, staking, FCMP++ curve tree). Documented indocs/LMDB_WRITE_ATOMICITY_AUDIT.md. Found and fixed a missinglock.commit()inget_relayable_transactions(Dandelion++ timestamp rollback bug) and added a defensivedb_wtxn_guardaround the staker accrual reversal inpop_block_from_blockchain. -
LMDB schema reference (
docs/LMDB_SCHEMA.md). Complete documentation of all 28 sub-databases: LMDB names, open flags, custom comparators, key/value byte layouts with struct field offsets, read/write access patterns, and hard fork version introduction. Standalone audit value and prerequisite for the eventual heed migration. -
Vendored dependency tracking (
docs/VENDORED_DEPENDENCIES.md). Documents the vendored LMDB version (0.9.70, based on OpenLDAPmdb.masterbranch), applied upstream patches (ITS#9385, ITS#9496, ITS#9500, etc.), CVE review (CVE-2026-22185 does not affect us), and themdb.mastervsmdb.master3branch distinction relevant to future heed migration. -
V4 design notes (
docs/V4_DESIGN_NOTES.md). Records the heed LMDB migration deferral with detailed reasoning (shared-write risk, schema drift, map resize race conditions) and the recommended approach for V4 (single Rust-owned Env, no split write ownership, full BlockchainLMDB unit cutover). -
Additional C++ conservation-invariant tests. Six new tests in
tests/unit_tests/staking.cpp: weighted denominator >= raw sum invariant, tier-0 weight equality, higher-tier strict inequality, zero-staker burn path, single-staker full capture, dust staker conservation, multi-block claim range conservation, and MAX_CLAIM_RANGE boundary validation. -
shekyl-wallet-corecrate. New Rust crate providing transaction builder plans for stake, unstake, and claim operations. IncludesClaimTxBuilderfor constructing claim transaction plans with automatic MAX_CLAIM_RANGE splitting, andClaimAndUnstakePlanfor the two-step drain-then-unstake workflow. -
Coin selection module (
shekyl-scanner/coin_select.rs). Min-relatedness output selection algorithm that prefers combining outputs with fewer shared metadata fingerprints (tx hash, block height, subaddress, tier) for improved on-chain privacy. Supports dust separation and configurable selection criteria. -
Output freezing and coin control.
WalletStatenow supports freeze/thaw of individual outputs by index or key image, with frozen outputs excluded from spendable candidate lists. Newspendable_outputs()method with optional account, subaddress, and minimum amount filters. -
Staker pool tracking in Rust (
shekyl-scanner/staker_pool.rs). Wallet-sideStakerPoolStatemirrors per-block accrual records from the daemon, enabling local reward estimation without RPC round-trips. Supports reorg handling and conservation invariant checking. -
Claim watermark tracking.
TransferDetailsnow carrieslast_claimed_heightfor monotonic claim watermark management.WalletStateexposesupdate_claim_watermark(),claimable_outputs(), andclaimable_rewards_summary()methods. NewClaimableInfostruct provides per-output claim state including accrual frozen status. -
New RPC methods.
get_claimable_stakes,get_unstakeable_outputs,freeze, andthawadded to the Rust scanner-backed RPC handler. All four are routed through the Rust scanner whenrust-scannerfeature is active. -
GUI wallet staking bridge.
wallet_bridge.rsextended withget_scanner_claimable_stakes,get_scanner_unstakeable_outputs,scanner_freeze, andscanner_thawfor Tauri frontend integration. -
Staking transaction types in
shekyl-oxide.Input::StakeClaimvariant (binary tag 0x03) andOutput::staking: Option<StakingMeta>(binary tag 0x04) added with full binary serialization/deserialization.StakingMetacarries thelock_tierfield (lock_untilis computed dynamically). -
Property-based staking tests. 11 new property tests in
shekyl-staking: conservation across uniform/mixed/stress scenarios, proportionality, floor division safety, weight function validation, multi-block accumulation bounds, and adversarial edge cases. -
shekyl-chachacrate. New Rust crate providing XChaCha20 (192-bit nonce) stream cipher for wallet and cache file encryption. Wraps the NCC-audited RustCryptochacha20crate. Exported via FFI asxchacha20(), replacing the C implementation inchacha.c. -
KEM-derived output secrets (
OutputSecrets). New Rust infrastructure inshekyl-crypto-pq/src/derivation.rsderives per-output secrets (ho,y,z,k_amount,view_tag_combined,amount_tag,ml_dsa_seed) from the combined X25519 + ML-KEM shared secret via HKDF-SHA-512 with distinct info labels. Includesderive_view_tag_x25519for fast wallet scan pre-filtering without ML-KEM decapsulation. FFI exports:shekyl_derive_output_secrets,shekyl_derive_view_tag_x25519. -
Cross-language HKDF test vectors. Python reference implementation (
tools/reference/derive_output_secrets.py) generates locked JSON test vectors (docs/test_vectors/PQC_OUTPUT_SECRETS.json). Rust unit tests validate byte-for-byte against these vectors. -
Witness header constant.
SHEKYL_PROVE_WITNESS_HEADER_BYTES = 256defined in bothshekyl_ffi.handshekyl-ffi/src/lib.rs, replacing all magic literal 256 values. -
Consensus
mask=1placeholder.check_commitment_mask_valid()wired intocheck_tx_outputsfor all v3 transactions. Returns accept-all now; PR-construct will flip to rejectzeroCommitform for non-coinbase. -
HKDF label registry.
docs/POST_QUANTUM_CRYPTOGRAPHY.mdnow documents all HKDF salt/info pairs for the per-output derivation stream and the separate X25519-only view tag derivation. -
Unified Rust output construction (
construct_output). Newshekyl-crypto-pq/src/output.rsimplementsconstruct_output(KEM encapsulation + HKDF → two-component keyO = ho*G + B + y*T, Pedersen commitmentC = z*G + amount*H, encrypted amount, view tag, PQC leaf hash) andscan_output_recover(KEM decapsulation + HKDF → recovered spend keyB' = O - ho*G - y*Tfor subaddress lookup, plus all per-output secrets). FFI exports:shekyl_construct_output,shekyl_scan_output_recover. -
PQC signing in Rust (
sign_pqc_auth). ML-DSA-65 keypair is derived, used, and wiped entirely within Rust. The secret key never crosses the FFI boundary. FFI export:shekyl_sign_pqc_auth. -
FCMP++ witness header assembly in Rust. The 256-byte witness header (
[O:32][I:32][C:32][h_pqc:32][x:32][y:32][z:32][a:32]) is now assembled viashekyl_fcmp_build_witness_headerwith a typedProveInputFieldsstruct, replacing 8 rawmemcpycalls in C++. -
construct_miner_txandconstruct_tx_with_tx_keyrewired to Rust. Both v3 output construction paths now callshekyl_construct_outputper output in a unified loop. KEM ciphertexts and PQC leaf hashes are written totx_extra. The legacyderivation_to_y_scalarpath is retired on all construction paths. -
Wallet scanner uses
scan_output_recover.wallet2::process_new_transactionhas a v3-specific scanning path that callsshekyl_scan_output_recoverfor KEM decapsulation, HKDF derivation, amount recovery, and subaddress lookup. Key images are computed as(ho + b_spend) * Hp(O). -
X25519-derived view tag. Per-output view tags are now derived from the X25519 shared secret only (no ML-KEM needed), enabling fast wallet scan pre-filtering. Written during construction, checked first during scanning.
-
additional_tx_keysremoved for v3.need_additional_txkeysis false fortx.version >= 3. Theadditional_tx_public_keysfield is no longer populated or consumed in v3 construction or scanning. -
Real Pedersen commitments for coinbase (
RCTTypeNull).outPkandenc_amountsare now serialized forRCTTypeNulltransactions.blockchain_db.cppuses the on-chainoutPk[i].maskfor v3+ coinbase instead of computingzeroCommit(amount). -
check_commitment_mask_validenforced. Rejects trivial commitment masks (z = 0orz = 1) for all non-coinbase v3 outputs. Called from bothcheck_tx_outputsandprevalidate_miner_transaction. -
PQC salt consolidation. All per-output PQC key derivation now uses the unified
OutputSecrets.ml_dsa_seedfrom salt B (shekyl-output-derive-v1). The legacyHKDF_SALT_PQC_DERIVEsalt A is deleted. Testnet reset required — invalidates all existingh_pqc. -
Chaingen test infrastructure updated for v3.
init_output_indices,fill_tx_sources,init_spent_output_indices, andconstruct_fcmp_txnow useshekyl_scan_output_recoverfor HKDF-based output ownership detection, mask recovery, and key image computation. -
genRctFcmpPlusPlususes HKDF commitment masks. The function now accepts pre-computed HKDFzscalars (commitment_masks) and pre-computed encrypted amounts (enc_amounts_precomputed) instead of re-deriving them internally via Keccak. This fixes a critical mismatch where BP+ proofs used Keccak-derived masks whilescan_outputexpected HKDF-derived values. The oldamount_keysparameter is removed. Testnet reset required — on-chain commitments and encrypted amounts are now HKDF-derived, incompatible with prior Keccak format. -
Stake claim outputs use
shekyl_construct_output. The wallet'screate_stake_claim_txnow constructs outputs via the unified Rust HKDF path, producing correct output keys, view tags, KEM ciphertexts, leaf hashes, andenc_amountswithamount_tag. BP+ blinding factors remain constrained by thezeroCommitpseudo-out balance equation (sum to N). -
Chaingen PQC signing via
shekyl_sign_pqc_auth. Core testconstruct_fcmp_txnow uses the high-level FFI that derives, signs, and wipes the ML-DSA secret key entirely inside Rust. The rawshekyl_pqc_signcall (which accepted the secret key as a C++ byte pointer) is replaced. -
zeroCommitdead code removed from DB layer.blockchain_db.cppanddb_lmdb.cppno longer fall back tozeroCommit(amount)for output commitments. All outputs (including coinbase) use on-chainoutPk[i].mask. Thepre_rct_outkeybranch in LMDB now throws foramount != 0(Shekyl has no pre-RCT outputs). -
RCTTypeNull round-trip serialization test. New test in
tests/unit_tests/serialization.cppverifies thatRCTTypeNulltransactions with populatedoutPkandenc_amounts(8-byte amount + 1-byteamount_tag) survive binary serialize/deserialize round-trip. -
libFuzzer harness for
construct_output. New fuzz targetfuzz_construct_outputinrust/shekyl-crypto-pq/fuzz/exercisesconstruct_output+scan_outputround-trip with arbitrary spend keys, amounts, corruptedenc_amount, and wrongamount_tag. -
libFuzzer harness for malformed KEM keys. New fuzz target
fuzz_construct_output_malformed_kemfeeds arbitrary bytes as X25519 and ML-KEM-768 encapsulation keys toconstruct_output. Exercises wrong-length, oversized, and garbage KEM public key inputs to ensure the function returnsErr, never panics. -
PQC leaf hash known-answer test. New JSON fixture
docs/test_vectors/PQC_LEAF_HASH_KAT.json(8 vectors) pins the output ofderive_pqc_leaf_hash(combined_ss, output_index). Rust KAT test validates byte-for-byte against the fixture. -
Coinbase
check_commitment_mask_validhardened. ForRCTTypeNull(coinbase) outputs, the consensus check now rejects commitments that equalzeroCommit(public_amount)(i.e.C = G + amount*H), preventing miners from constructing trivial-mask coinbases that leak amount to observers. Non-coinbase defense-in-depth checks (identity and G) are retained. -
Dead Keccak y-scalar fallback removed from wallet scanner. The
else if (tx.vout[o].amount == 0)andelse if (miner_tx)branches that fell back toderivation_to_y_scalarare removed. Shekyl is v3 from genesis; all matched outputs must succeed the HKDF scan path. A hardwallet_internal_erroris thrown ifv3_hkdf_scannedis false, preventing silent domain fallback that would produce unspendable outputs. -
Legacy coinbase construction path removed.
construct_miner_txnow asserts PQC key presence with a clear error message (CHECK_AND_ASSERT_MES) before entering the output construction loop, instead of falling back to legacy Keccakderive_public_key/derive_view_tagwhich would produce an invalid (unscannable, missingoutPk/enc_amounts) coinbase. All Shekyl addresses carry PQC keys from genesis. -
Genesis coinbase builder uses
shekyl_construct_output.build_genesis_coinbase_from_destinationsnow constructs outputs via the Rust HKDF path, producing correct HKDF-derived output keys, view tags, commitments, encrypted amounts withamount_tag, KEM ciphertexts, and PQC leaf hashes. The legacy Keccak derivation path is removed. -
Legacy
additional_tx_public_keysdead code removed. Theneed_additional_txkeyslogic,additional_tx_public_keysvector, and pre-v3 output derivation loop inconstruct_tx_with_tx_keyare deleted. V3 replaces per-output additional tx keys with KEM ciphertext (tag 0x06).
🔄 Changed
-
transfer_details::m_masktype changed.rct::key→crypto::secret_keyfor automatic zeroization on drop. All RCT call sites use explicitrct::sk2rct()/rct::rct2sk()conversion. Binary-compatible (same 32-byte layout). -
ecdhInforeplaced byenc_amounts. The per-output encrypted amount format changes fromecdhTuple(64 bytes: 32 mask + 32 amount) tostd::array<uint8_t, 9>(8 bytes XOR-encrypted amount + 1 byte amount tag). AffectsrctSigBase, all serialization paths (binary, boost, JSON), and transaction construction (genRctFcmpPlusPlus,fill_construct_tx_rct_stub, wallet claim construction). -
ecdhEncoderemoved. The ECDH encoding function is deleted fromrctOps,device.hpp, anddevice_default. Transaction construction now writesenc_amountsdirectly via Rust HKDF-based output construction.ecdhDecodeis retained as a scanner shim until the wallet migrates to Rustscan_output.ecdhHashandgenCommitmentMaskhave been fully removed fromrctOps, the device interface chain, and tests. -
FROST SAL deferred to V4. Per-output HKDF-derived
yis incompatible with DKG group-sharedy. FROST SAL section indocs/PQC_MULTISIG.mdmarked as deferred with V4 resolution path (Carrot-style address scheme).
🐛 Fixed
-
sc_check()signed left-shift undefined behavior.signum(...) << konint64_tincrypto-ops.cis UB when the result is negative. Introducedsigned_lshift()helper that uses multiplication on non-GCC compilers. Ported from monero@c5be4dd. -
wallet2::verify_password()logic inversion. Background wallet detection usedHasParseError() && IsObject()instead of!HasParseError() && IsObject(), causing background wallets to fail password verification. Added the missing!. Ported from monero@b19cd82. -
HTTP digest auth missing client nonce (
cnonce). The epee HTTP client sent an emptycnoncewithqop=auth, weakening the digest exchange against replay attacks. Now generates a random 16-byte cnonce viaRAND_bytesand includes it in the response hash and Authorization header. Ported from monero@3d6b9fb. -
Critical: SAL
y/ commitment maskzconflation in FCMP++ prover.wallet2.cpppassedtd.m_mask(Pedersen commitment mask) asspend_key_yto the FCMP++ prover, but SAL requiresysuch thatO = xG + yT. Since legacy outputs hady = 0andz != 0,OpenedInputTuple::openalways failed. Fixed by migrating to two-component output keys (O = xG + yT) wherey = Hs_y(derivation || i), and passingzas a separatecommitment_maskfield. Affects every spend on the chain — this was the root cause of all FCMP++ proof generation failures. -
Coinbase commitment mask in test harness.
fill_tx_sourcesinchaingen.cppsetts.mask = rct::zero()for coinbase, butzeroCommit(amount) = G + amount*Hhas mask = scalar 1. Fixed torct::identity(). -
Critical: u64 saturation in
total_weighted_stake(Bug 7). The in-memory cache and LMDBstaker_accrual_recorduseduint64_tfor the tier-weighted stake denominator. With 12-decimal atomic units and tier multipliers > 1.0, this saturates at ~18.4M SHEKYL of weighted stake — well below moderate adoption. Reward computation collapses to a meaningless ceiling once saturated. Fixed by widening to u128 end-to-end: in-memory cache uses lo/hi u64 pairs with proper carry arithmetic, LMDB record gainstotal_weighted_stake_hifield (32→40 bytes), FFIshekyl_calc_per_block_staker_rewardaccepts lo/hi parameters, and RustAccrualRecord/StakeRegistry::total_weighted_stake()return u128. -
Critical: back-dating exploit on first claim (Bug 3).
check_stake_claim_inputonly enforcedfrom_height == watermarkwhen watermark > 0. For the first claim (no watermark),from_heightwas unconstrained. An attacker could stake at block N, then submit a claim withfrom_height = 0, walking 10,000 historical blocks and collecting rewards against denominators that never included the attacker's output. Fixed by looking up the staked output's creation height and requiringfrom_height >= creation_heightwhen no watermark exists. -
Critical: inter-tx pool sufficiency race within a block (Bug 4). The per-tx pool balance check in
check_tx_inputsreads the pre-block pool balance, so five claim txs each claiming 1000 against a pool of 3000 all individually pass. The silent-skip path inadd_transaction_datathen lets over-claimed txs through without decrementing the pool. Fixed with two changes: a block-level aggregate pool check inhandle_block_to_main_chainthat sums all claim amounts across ALL txs and rejects the block if the total exceeds the pool, plus converting the silent-skip path inadd_transaction_datato a hard throw (dead code if validation is correct, fatal if not). -
Reorg watermark restoration loses data (Bug 5).
remove_transactionusedfrom_height == 0as the signal for "first claim, remove watermark." Butfrom_heightfor a first claim is typically the creation height (non-zero). Fixed by looking up the staked output's creation height to distinguish first claims from subsequent claims. -
Reorg pool reversal direction wrong for no-staker blocks (Bug 6).
pop_block_from_blockchainunconditionally subtracted accrued inflow frompool_balance, but for no-staker blocks the inflow was burned (not added to pool). Popping such a block caused a spurious pool underflow. Fixed by reading the accrual record'stotal_weighted_stake: if zero, subtract fromtotal_burnedinstead ofpool_balance. -
Empty-staker-set accrual audit trail. The
actually_destroyedfield in the persisted accrual record did not reflect the no-staker burn because the record was written before the burn decision. Fixed by movingadd_staker_accrualto after the no-staker burn path, so the record captures the fullactually_destroyedvalue. -
Dandelion++ relay timestamp rollback.
get_relayable_transactionsintx_pool.cppwas missinglock.commit(), causing all stem/forward timestamp updates to be silently rolled back by theLockedTXNdestructor. Transactions in Dandelion++ stem/forward states could be re-relayed with stale timing data, degrading transaction-origin privacy. Fixed by adding the missing commit. -
Staker accrual reversal without write transaction guard. The staker pool balance and burn total reversal in
pop_block_from_blockchainrelied on the caller's batch context for a write transaction but had no defensive guard. While all current production callers maintain a batch, a future caller without one would crash or produce undefined behavior. Fixed by wrapping the reversal block indb_wtxn_guard. -
Critical: weighted denominator bug in staker reward accrual. The per-block
total_weighted_stakewas computed from raw staked amounts instead of tier-weighted amounts, causing proportional over-distribution (up to +100% when all stakers use the Long tier). Fixed by introducing separate caches for raw and tier-weighted stake amounts inblockchain.h/blockchain.cpp. -
Claim timing: lock conflated with claimability.
check_stake_claim_inputincorrectly rejected claims whenlock_until > current_height, making rewards unclaimable during the lock period. Fixed by removing the lock-based rejection and addingto_height <= min(current_height, lock_until)enforcement. Wallet filters updated to include both locked and matured-but-unspent outputs. -
Zero-staker blocks: unclaimed pool accumulation. When no stakers existed, staker emission and fee pool amounts accumulated in
staker_pool_balanceindefinitely. Fixed to burn these amounts whentotal_weighted_stake == 0. -
Staked outputs incorrectly spendable.
is_spendable()allowed spending staked outputs after maturity. Fixed: staked outputs are never directly spendable -- they must go through the unstake path. -
Claim watermark not persisted. Added
m_last_claimed_heighttotransfer_details(C++ wallet) andTransferDetails(Rust scanner) with serialization. FFI layer now callsstage_claim_watermarks()after broadcasting claim transactions. -
Critical: stake tx only mineable in exact creation block (Bug 13).
handle_block_to_main_chainvalidated staked outputs with strict equalitystaked.lock_until == blockchain_height + lock_blocks. Since the wallet signedlock_until = current_height + lock_blocks, any mempool latency made every honest stake tx permanently unminable. Fixed by removinglock_untilfrom the on-chaintxout_to_staked_keystruct entirely. The effective lock expiry is now computed dynamically ascreation_height + tier_lock_blocksat every check site. Removes ~8 bytes per staked output and eliminates the signing-time/mining-time mismatch bug class. -
High: mempool admits unminable stake txs (Bug 12). Pool admission checked tier validity and non-zero
lock_untilbut not the strict equality that block validation enforced. Honest and malicious stake txs passed admission but were rejected at block-add time, causing miners to waste work on blocks that would be rejected. Resolved by the Bug 13 fix: with no on-chainlock_until, the entire validation path is removed. -
Medium: off-by-one at upper lock boundary (Bug 11). The accrual scan excluded an output at block
lock_until(<= eval_height), but claim validation acceptedto_height <= lock_until. A staker could claim a one-block reward atlock_untilagainst a denominator that didn't include their weight. Fixed by changing the accrual scan toeffective_lock_until < eval_height(inclusive upper bound) and scheduling unlock subtraction ateffective_lock_until + 1.lock_blocks = Nnow means exactly N blocks of accrual. -
Medium: unstake forfeits unclaimed rewards (Bug 8).
create_unstake_transactionjumped straight tocreate_transactions_fromwithout checking for unclaimed reward backlog. A user who staked for the long tier and never claimed would silently forfeit all accrued rewards. Fixed: the wallet now refuses to unstake if any target output hasm_last_claimed_height < min(current_height, effective_lock_until)and instructs the user to claim first. -
Minor: local claim watermark advanced on broadcast, not confirmation.
update_claim_watermarks(nowstage_claim_watermarks) committed the watermark immediately after broadcast. If the tx was dropped or never confirmed, the local watermark diverged from consensus. Fixed with an in-flight tracking system: claims are staged inm_pending_claim_watermarksat broadcast, committed byconfirm_claim_watermarkswhen the tx appears in a confirmed block during scan, and expired byexpire_pending_claim_watermarksafter 100 unconfirmed blocks.
🔄 Changed
-
Wallet encryption upgraded from ChaCha20 (64-bit nonce) to XChaCha20 (192-bit nonce). The 24-byte nonce eliminates collision risk for randomly-generated nonces. Implementation moved from C (
chacha.c) to Rust (shekyl-chachacrate) using the NCC-audited RustCryptochacha20crate.CHACHA_IV_SIZEincreased from 8 to 24 bytes. Wallet keys files and cache files now use XChaCha20 exclusively. -
Two-component output keys (
O = xG + yT). All output public keys now include a domain-separatedycomponent along generatorT, satisfying the FCMP++ SAL proof'sOpenedInputTuple::openconstraint. Previously, outputs were single-component (O = xG + 0·T) and the wallet incorrectly passed the Pedersen commitment maskzas the SALy, causing proof generation to fail. The y-scalar uses the"shekyl_y"domain separator incrypto.cpp. The commitment maskzis now passed separately in the 256-byte witness header at offset 192.transfer_detailsstoresm_y(boost serial v14). Two regression tests inproof.rsverify that the old bug (y=mask) fails and the correct path (y=real) succeeds. -
MAX_TX_EXTRA_SIZE(24576 bytes). The previous Monero-era cap (1060) was too small for FCMP++tx_extrapayloads (hybrid KEM ciphertexts ~1120 B per output, PQC leaf hashes, pubkey/nonce). Construction of v3 spends failed once PQC fields were appended; the pool andconstruct_txchecks now allow the larger bound. -
construct_txRCT/PQC stubs. v3 spends require|pqc_auths| == |vin|for binary serialization, andRCTTypeFcmpPlusPlusPqcneeds BP+, ECDH, and pseudo-out vectors sized to inputs/outputs.construct_txnow assigns stubpqc_authenticationentries and callsrct::fill_construct_tx_rct_stub()(dummy Bulletproofs+, ECDH encoding, Pedersen pseudo-outs) soget_transaction_hashand JSON/blob round-trips succeed before the wallet replaces the RCT payload withgenRctFcmpPlusPlus().
🗑️ Removed
-
shekyl_fcmp_derive_pqc_keypairFFI function. Deleted the Rust FFI function and its C declaration. This function returned the ML-DSA secret key to C++, violating the security invariant that PQC secrets stay in Rust. Replaced byshekyl_derive_pqc_leaf_hash(returns only h_pqc) andshekyl_derive_pqc_public_key(returns only the public key). -
derive_pqc_keypair,derive_hybrid_pqc_keypair,DerivedPqcKeypair,DOMAIN_PQC_OUTPUTfromshekyl-crypto-pq. These legacy derivation functions used the old salt A (shekyl-pqc-derive-v1) and returned secret key material. All callers now usederive_output_secrets(salt B) +keygen_from_seedor the higher-levelsign_pqc_auth_for_output. -
derived_pqc_secret_keys,derived_pqc_public_keys,claim_signing_sksvectors inwallet2.cpp. These C++ vectors held PQC secret keys in wallet memory. All 4 call sites migrated toshekyl_derive_pqc_leaf_hash+shekyl_sign_pqc_auth, which derive and zeroize internally in Rust. -
pqc_secret_keysfromnative_sign_state(wallet2.h). The deferred native-signing path no longer stores PQC secret keys. The Rust tx-builder receivescombined_ss+output_indexand derives keys internally. -
SpendInput::pqc_secret_keyfromshekyl-tx-builder. Replaced withcombined_ss: Vec<u8>(64 bytes) andoutput_index: u64. The Rustsign_pqc_authsfunction now callssign_pqc_auth_for_outputinternally. -
4 legacy Monero fixture tests in
serialization.cpp. Removedportability_wallet,portability_outputs,portability_unsigned_tx,portability_signed_tx. These tested Monero-era wallet/tx formats that Shekyl does not support (no backward compatibility). -
10 Monero-specific long-term block weight tests. Removed all tests from
long_term_block_weight.cpp(empty_shortthroughcache_matches_true_value). Monero-specific weight baselines do not apply to Shekyl economics. -
chacha.c(C ChaCha implementation). Replaced by the Rustshekyl-chachacrate via FFI. The C implementation had a strict aliasing violation in itsU8TO32_LITTLE/U32TO8_LITTLEmacros (pointer cast touint32_t*). -
ChaCha8 dead code. All
crypto::chacha8()call sites inwallet2.cppwere Monero backward-compatibility fallbacks for reading pre-2018 wallet files. Shekyl has no legacy wallets; these paths were unreachable.
🔒 Security
-
ML-DSA secret keys never cross the FFI boundary. All wallet PQC signing paths now use
shekyl_sign_pqc_auth(Rust FFI) orsign_pqc_auth_for_output(Rust tx-builder), which derive the keypair fromcombined_ss+output_index, sign, and zeroize the secret key — all within Rust. No ML-DSA secret key bytes exist in C++ memory at any point. This eliminates the largest PQC secret key exposure surface (~4064 bytes per input) from the wallet process. -
XChaCha20 192-bit nonces for wallet encryption. Upgraded from the DJB ChaCha20 64-bit nonce to XChaCha20 192-bit nonce, eliminating nonce collision risk for randomly-generated nonces. The previous 64-bit nonce was safe for Shekyl's usage pattern but the larger nonce provides a wider safety margin.
-
Secure memory hardening (project-wide). Systematic implementation of the
secure-memory.mdcrule across Rust and C++ codebases:shekyl_buffer_freenow useszeroizecrate instead ofstd::ptr::write_bytes, preventing the compiler from optimizing away the secret-wiping write.native_sign_state::clear()inwallet2.hnowmemwipes all secret fields (spend_key_x,spend_key_y,h_pqc,amount_key,pqc_secret_keys) before clearing vectors.- Added
prctl(PR_SET_DUMPABLE, 0)to daemon (main.cpp), simplewallet, andwallet2_ffi_create()to prevent core dumps containing key material on Linux. - Passwords, seeds, spend keys, and view keys in
wallet2_ffi.cppJSON-RPC dispatch now usememwipescope guards to wipe temporarystd::stringbuffers after use. - New
shekyl_madvise_dontdumpFFI function (MADV_DONTDUMPon Linux, no-op elsewhere) declared inshekyl_secure_mem.h. - PQC long-lived secret keys (
m_pqc_secret_key) are nowmlocked andmadvise(MADV_DONTDUMP)ed after generation and decryption, andmemwiped +munlocked onforget_spend_key().
-
Dev branch audit: Tier 1-6 security and code hardening. Comprehensive re-audit of the dev branch with 22 findings addressed:
- PQC secret key lifecycle (Tier 1). Added
~account_keys()destructor that wipes all secret keys (classical + PQC) and munlocks PQC material. Fixedcreate_from_keysandset_nullto wipe+unlock PQC secrets before clearing. Prevents secrets from lingering in freed heap memory. - Debug trait on secret key types (Tier 1). Removed
#[derive(Debug)]fromHybridSecretKey,HybridKemSecretKey, andSharedSecret. All now implement manualDebugprinting[REDACTED]to prevent log leakage. - Proof generation panic removal (Tier 1). Replaced 12
ScalarDecomposition::new(...).unwrap()calls inproof.rswith?-propagatedProveError::ScalarDecompositionFailed. Zero-scalar blinding factors now return a clean error instead of panicking the wallet. - RELEASE-BLOCKER resolution (Tier 1). Evaluated and downgraded all 6 RELEASE-BLOCKER comments in shekyl-oxide to TODO with documented justifications. None were correctness or security blockers.
- FROST multisig feature-gated (Tier 1). All FROST SAL and DKG FFI
functions gated behind
#[cfg(feature = "multisig")]. Production builds exclude multisig code unless the feature is enabled. C++#ifdef SHEKYL_MULTISIGblocks have been removed fromshekyl_ffi.h,wallet2.h/cpp, andwallet2_ffi.cpp— FROST multisig is now consumed exclusively through the Rust wallet crates. - CString unwrap removal (Tier 2). Replaced all
CString::new().unwrap()inshekyl-wallet-rpcwithto_cstring()helper returningWalletError. FixedMutex::lock().unwrap()in server.rs to return JSON-RPC error on lock poisoning. - Sign function zeroization (Tier 2).
HybridEd25519MlDsa::sign()now wraps temporary secret arrays inZeroizing<[u8; N]>for automatic cleanup. - hex_to_key temp buffer wiped (Tier 2). Added
memwipescope guard tohex_to_keyinwallet2_ffi.cpp. - PQC verify debug gated (Tier 2).
shekyl_pqc_verify_debugnow only compiled withdebug_assertionsordebug-verifyfeature to prevent use as a signature oracle in production. - Free-string wipe (Tier 2).
wallet2_ffi_free_stringnow wipes the buffer before freeing, protecting against secret-bearing JSON residue. - Buffer free contract documented (Tier 2).
shekyl_buffer_freelen safety contract documented in both Rust doc-comment and C header. - Claim builder silent wrong index (Tier 2).
position(...).unwrap_or(0)replaced with explicitTransferNotFounderror inclaim_builder.rs. - deny(unsafe_code) added (Tier 3). Added to 5 pure-Rust crates:
shekyl-consensus,shekyl-economics,shekyl-staking,shekyl-crypto-hash,shekyl-crypto-pq. - Workspace lints inherited (Tier 3).
[lints] workspace = trueadded to 11 Shekyl-first crates for consistent Clippy enforcement. - Legacy naming cleanup (Tier 4). Renamed
MONERO_DEFAULT_LOG_CATEGORYtoSHEKYL_DEFAULT_LOG_CATEGORYacross 128 files. - FCMP++ edge-case tests (Tier 5). Added 9 parametrized tests covering boundary input counts, missing tree paths, empty proof data, count mismatches, zero tree depth, and wrong signable_tx_hash.
- CI improvements (Tier 6). Added
.envto.gitignore, created explicit CodeQL workflow targeting bothdevandmainbranches, addedpermissions: contents: readtobuild.yml.
- PQC secret key lifecycle (Tier 1). Added
-
Base58 overflow and non-canonical encoding fix (monero-oxide fork).
shekyl-base58::decode()now useschecked_addto prevent integer overflow during character accumulation, and rejects non-canonical encodings where unused high bytes of the decoded sum are non-zero. Defense-in-depth measure; Shekyl production addresses use Bech32m. -
Cargo profile hardening (both Rust workspaces). All profiles (dev, release, test, bench) now enforce
overflow-checks = truein both the monero-oxide forkCargo.tomland the Shekylrust/Cargo.toml. Dev and release profiles additionally setpanic = "abort". -
HKDF domain-separated salts for PQC key derivation. All HKDF-SHA-512 calls in
shekyl-crypto-pqnow use explicit fixed salts (shekyl-pqc-derive-v1,shekyl-master-derive-v1) instead ofNone. Strengthens domain separation and prevents cross-protocol seed reuse if the same combined shared secret appears in other contexts. -
FrostSalSessionsecret deduplication. Removed the redundantx(spend secret scalar) fromFrostSalSessionstruct fields. Previously the secret was stored both in the struct and insideSalAlgorithm, with only the struct copy explicitly zeroized on drop. Now the secret lives solely inside the algorithm, eliminating the unprotected duplicate. -
Levin double-compression guard.
try_compress_messagenow checksLEVIN_PACKET_COMPRESSEDin the input header before compressing. Prevents double-compression of already-compressed messages in future refactors. -
Divisor degree underflow assertions.
Divisor::divnow asserts thatself.a.degree >= rhs.degreeandself.b.degree >= rhs.degreebeforeusizesubtraction, converting silent wraparound into a clear panic with diagnostic context. -
Interpolator allocation bounds fix.
Interpolator::interpolatenow allocates the output coefficient vector using the domain size (self.lagrange_polys.len()) instead ofevals.len(), preventing trailing zeros from inflating the vector when callers provide excess evaluations. -
member_of_listwitness construction hardened. Replacednext_eval.unwrap()withcarry_eval.zip(next_eval)in the FCMP++ circuit gadget, eliminating a potential panic if evaluation invariants change.
✨ Added
-
shekyl-tx-buildercrate. New Rust crate (rust/shekyl-tx-builder/) consolidating Bulletproofs+ range proofs, FCMP++ full-chain membership proof construction, ECDH amount encoding, and PQC (ML-DSA-65) signing into a single native Rust call path. Replaces the prior C++ → Rust → C++ → Rust FFI round-trip for proof generation. Includes 19 unit tests covering validation edge cases (0 inputs, overflow amounts, empty trees, wrong-length PQC keys) and ECDH encoding round-trips. All secret key material is wrapped inzeroize::Zeroizingand wiped on drop. -
shekyl_sign_transactionFFI export. New C ABI function inshekyl-ffiwrappingshekyl-tx-builder::sign_transaction(). Accepts JSON-serialized inputs/outputs, returns aShekylSignResultwith either JSON proofs or a structured error code and message. Declared inshekyl_ffi.h. -
Wallet RPC
native-signfeature.shekyl-wallet-rpcgains an optionalnative-signCargo feature that enablestransfer_native()— a pure-Rust transfer path usingshekyl-tx-builderdirectly, eliminating C++ proof FFI round-trips. The split pipeline useswallet2_ffi_prepare_transfer(C++ → JSON) →shekyl-tx-builder::sign_transaction(pure Rust) →wallet2_ffi_finalize_transfer(JSON → C++). -
wallet2_ffi_prepare_transfer/wallet2_ffi_finalize_transferimplemented. Full C++ implementation of the split transfer pipeline.prepare_transferactivates native-sign mode intransfer_selected_rct(skipping C++ proof generation), gathers per-input signing data (secret keys, tree paths parsed into c1/c2 branch layers, leaf chunks, PQC key material), per-output data (dest keys, amount keys), tree context (reference block, curve tree root, depth), and serializes everything as hex-encoded JSON matching the RustSpendInput/OutputInfo/TreeContexttypes.finalize_transferreceives the Rust-generatedSignedProofsJSON, manually reconstructs the BP+ struct from the Rust blob (handling the V-field format difference), inserts all proofs intotx.rct_signatures, performs PQC signing using stored secret keys, and commits/broadcasts the transaction. Fee estimation usesshekyl_fcmp_proof_len()to pad the stub FCMP++ proof to the correct estimated size. -
Native-sign mode in
wallet2::transfer_selected_rct. Newm_native_sign_modeflag andnative_sign_statestruct onwallet2. When enabled,transfer_selected_rctskipsgenRctFcmpPlusPlusand PQC signing, instead storing all signing data for the Rust path. Tree path blobs are parsed into structured c1/c2 branch layers. Padded stub proofs provide accurate fee estimation. -
Hex serde for
shekyl-tx-buildertypes. All[u8; 32],Vec<u8>, andVec<[u8; 32]>fields onSpendInput,OutputInfo,TreeContext,SignedProofs,LeafEntry, andPqcAuthnow serialize/deserialize as hex strings via custom serde modules. This enables clean JSON interop with the C++ FFI layer which produces hex-encoded cryptographic keys and blobs. -
Secure memory Cursor rule. Added
.cursor/rules/secure-memory.mdccodifying project-wide conventions for cryptographic secret zeroization in both Rust (Zeroizing<T>,ZeroizeOnDrop) and C++ (memwipe, scope guards,wipeable_string), FFI boundary ownership, and OS-level protections (mlock,prctl(PR_SET_DUMPABLE, 0),MADV_DONTDUMP). -
Vendored monero-oxide protocol crates. Completed the vendored crate set in
rust/shekyl-oxide/: addedshekyl-primitives(Keccak-256, Pedersen commitments),shekyl-bulletproofs(BP+ range proofs), the rootshekyl-oxidecrate (transaction/block types, FCMP module),shekyl-rpc(daemon RPC trait,ScannableBlock), andshekyl-simple-request-rpc(HTTP transport). Resolved theshekyl-addressnaming collision by removing the oxide base58 address dependency from the vendored RPC crate (Shekyl uses Bech32m exclusively). Added crypto-heavy crate optimizations to[profile.dev.package]and workspace-level clippy lints for the oxide crates. -
shekyl-scannercrate. New Rust crate (rust/shekyl-scanner/) providing a native transaction scanner with Shekyl-specific extensions. Ported the core scanning pipeline from monero-oxide (SharedKeyDerivations, Extra parsing, ViewPair, per-block/per-tx/per-output ECDH scan loop) and extended it with:- PQC KEM ciphertext parsing (tx_extra tag 0x06) and leaf hash parsing (0x07)
- Staking output detection and balance categorization (matured/locked tiers)
TransferDetailsstruct with FCMP++ path precompute, combined PQC shared secret, and spend tracking fieldsWalletStatefor in-memory transfer management with key image dedup, spend detection, and reorg handlingBalanceSummarywith staking-aware breakdown (total, unlocked, timelocked, staked matured/locked, frozen)
-
Split RPC routing (
rust-scannerfeature).shekyl-wallet-rpcnow supports arust-scannerfeature flag that routes scanner-backed read-only methods (get_balance, get_transfers, incoming_transfers, get_height, get_staked_outputs, get_staked_balance) to native Rust handlers viashekyl-scanner, while all mutation methods continue through the C++ FFI. AddedScannerState,dispatch_with_scanner(), and typed scanner handlers. -
GUI wallet scanner integration. Updated
wallet_bridge.rsinshekyl-gui-walletto include aScannerStatealongside the FFIWallet2handle. Addedget_scanner_balance(),get_scanner_staked_outputs(), andget_scanner_height()bridge methods for future scanner-backed queries. -
shekyl-encodingcrate. New standalone Rust crate (rust/shekyl-encoding/) for general-purpose Bech32m blob encoding and decoding with arbitrary HRPs. Defines HRP constants for wallet proofs (shekylspendproof,shekyltxproof,shekylreserveproof,shekylsig,shekylmultisig,shekylsigner). -
shekyl-addresscrate. New standalone Rust crate (rust/shekyl-address/) for network-aware segmented Bech32m address encoding. DefinesNetworkenum (Mainnet, Testnet, Stagenet) with HRP lookup tables for classical (shekyl,tshekyl,sshekyl) and PQC (skpq/skpq2,tskpq/tskpq2,sskpq/sskpq2) segments.ShekylAddresssupportsencode(),decode(), anddecode_for_network(). -
Generic Bech32m blob FFI.
shekyl_encode_blob()andshekyl_decode_blob()FFI functions allow C++ to encode/decode arbitrary binary data with purpose-specific HRPs, replacing all direct Base58 calls in wallet proofs. -
Network-aware address FFI.
shekyl_address_encode()andshekyl_address_decode()now accept/return anetworkparameter (0=mainnet, 1=testnet, 2=stagenet) for HRP-based network discrimination. -
Shekyl-first development rule. Added
.cursor/rules/shekyl-first-development.mdccodifying that Shekyl core is the authoritative codebase and the monero-oxide fork is a disposable downstream consumer. -
FROST SAL threshold signing for FCMP++ multisig. New
frost_salmodule inshekyl-fcmpwraps upstreamSalAlgorithm<Ed25519T>for threshold Spend-Auth-and-Linkability proofs.FrostSalSessionmanages per-input FROST state;prove_with_sal()constructs FCMP++ proofs from pre-aggregated SAL pairs. FFI functions (shekyl_frost_sal_session_new,_get_rerand,_aggregate_and_prove,_session_free) expose the session lifecycle to C++. Themultisigfeature flag enables FROST dependencies (modular-frost,transcript,rand_chacha). -
FROST DKG key management. New
frost_dkgmodule inshekyl-fcmpprovidesSerializedThresholdKeysforThresholdKeys<Ed25519T>serialization/deserialization, group key extraction, and parameter validation. FFI functions (shekyl_frost_keys_import,_export,_group_key,_validate,_free) manage threshold keys from C++. -
Variable-length FCMP++ witness wire format.
shekyl_fcmp_proveFFI now accepts a singlewitness_ptr/witness_lenblob containing per-input fixed headers, leaf chunk Ed25519 output data, and Helios/Selene branch layers.genRctFcmpPlusPlusinrctSigs.cppserializes the full witness. -
Daemon RPC
chunk_outputs_blob.get_curve_tree_pathresponse now includes per-chunk compressed Ed25519 output data (O, I=Hp(O), C, H(pqc_pk)) enabling the wallet to pass full output points to the prover. -
C++ wallet FROST multisig integration (removed). Previously added C++ FROST integration in
wallet2.cpp(prepare_multisig_fcmp_proof,export_multisig_signing_request,import_multisig_signatures, threshold key import/export). This C++ code has been replaced by the Rust-native wallet crates and all#ifdef SHEKYL_MULTISIGblocks have been removed fromwallet2.h/cpp,wallet2_ffi.cpp, andshekyl_ffi.h. -
FrostSigningCoordinatorfor multi-input nonce aggregation. New coordinator inshekyl-fcmp/src/frost_sal.rsmanages per-input preprocess collection, nonce sum computation, share collection, and final aggregation intoSpendAuthAndLinkabilitypairs forprove_with_sal(). -
Full FROST DKG ceremony via
MultisigDkgSession. New wallet-level wrapper inshekyl-wallet-core/src/multisig/dkg.rsdrives thedkg-pedpopKeyGenMachinestate machine through all three rounds with type-safe transitions:generate_coefficients→generate_secret_shares→calculate_share→complete. DKG messages are exchanged as byte buffers (file-based, air-gap compatible). -
MultisigSigningSessionfor wallet-level FROST orchestration. New session inshekyl-wallet-core/src/multisig/signing.rswraps per-inputFrostSalSessioninstances and aFrostSigningCoordinator, providing hex-encoded preprocess/share exchange for transport-agnostic signing. -
MultisigGroupwith PQC keypair management. New type inshekyl-wallet-core/src/multisig/group.rsstores threshold keys, group metadata, and PQC hybrid keypairs with automatic zeroization on drop. Supports serialization/deserialization for wallet storage. -
FROST multisig RPC endpoints. 9 new JSON-RPC methods in
shekyl-wallet-rpc/src/multisig_handlers.rsfor FROST signing coordination:multisig_register_group,multisig_list_groups,multisig_create_signing,multisig_sign_preprocess,multisig_sign_add_preprocess,multisig_sign_nonce_sums,multisig_sign_own,multisig_sign_add_shares,multisig_sign_aggregate. All byte fields hex-encoded. DKG is intentionally excluded from RPC (file-based only). -
SalLegacyAlgorithmandlegacy_multisigremoved from shekyl-oxide. Deleted the legacy Monero multisig SAL algorithm and test module from the vendoredshekyl-oxide/fcmp/fcmp++crate. Only the modernSalAlgorithm(used byFrostSalSession) is retained. -
16+ new Rust tests for FROST. 4
frost_salunit tests (session creation, pseudo-out distinctness, identity rejection, field roundtrip), 6FrostSigningCoordinatortests (wrong preprocess count, shares before nonces, duplicate shares, nonce sums timing, point addition, bytes roundtrip), 2FrostSalSessionnegative tests, 4frost_dkgunit tests (serialization roundtrip, group key extraction, parameter validation, byte-level roundtrip), 8 FFI lifecycle tests (null safety, invalid data rejection, session handle management), 5shekyl-wallet-coremultisig tests (DKG 2-of-3 and 3-of-5 roundtrips, DKG state machine errors, group serialization, threshold keys roundtrip). -
FCMP++ prove/verify round-trip test.
prove_verify_roundtrip()inrust/shekyl-fcmp/src/proof.rsexercises the full stack: random key generation, single-leaf tree root computation,prove(),verify(), and negative tests (tampered key image, wrong tree root).
🐛 Fixed
-
Suppressed vendored crate warnings. Fixed
dead_codewarning forInconsistentWitnessvariant ingeneralized-bulletproofs(only constructed underdebug_assertions) with#[cfg_attr(not(debug_assertions), allow(dead_code))]. Fixed deprecatedGenericArray::as_slice()inhelioseleneciphersuite by replacing withas_ref(). -
Stake-claim vs
verRctSemanticsSimpleconflict. Stake-claim transactions useRCTTypeFcmpPlusPlusPqcbut have no FCMP++ membership proof (they prove ownership via PQC auth on public amounts).ver_non_input_consensusnow excludes stake-claim-only transactions from the RCT semantics batch that rejects emptyfcmp_pp_proof. -
genRctFcmpPlusPlushard-fail on proof failure. Previously logged and returned anrctSigwith an empty proof whenshekyl_fcmp_provefailed; now throwsCHECK_AND_ASSERT_THROW_MESso the wallet catches the error immediately rather than producing an invalid transaction. -
PQC leaf scalar now uses proper Selene field reduction.
PqcLeafScalar::from_pqc_public_keyandhash_pqc_public_keypreviously truncated Blake2b-512 to 32 bytes and cleared bit 255, which could produce non-canonical values exceeding the Selene base field modulus. Now usesHelioseleneField::wide_reduceon the full 64-byte hash for unbiased, canonical field elements. -
Deterministic PQC keygen stability. Replaced
rand::rngs::StdRngwithrand_chacha::ChaCha20Rngfor ML-DSA-65 keypair derivation.StdRng's underlying algorithm is not a stability guarantee acrossrandversions, which could break wallet-restore-from-seed. -
Bech32m variant enforcement.
decode_blobnow strictly enforces the Bech32m checksum variant instead of accepting both Bech32 and Bech32m. Removed unusedEncodingError::EmptyDatavariant.
🔒 Security
-
FrostSalSession spend secret zeroized on drop. The FROST SAL session's spend secret scalar is zeroized when the session is dropped, per the project-wide secure memory rule. After the
FrostSalSessionsecret deduplication (see Changed), the secret lives solely inside theSalAlgorithmand is zeroized through itsDropimpl. -
RELEASE-BLOCKER resolved in circuit gadgets. The
incomplete_add_pubfunction in the FCMP++ circuit already receives parameters typed asOnCurve, which guarantees the on-curve constraint. Replaced theRELEASE-BLOCKER(shekyl)comment with documentation explaining why no additional constraint is needed. -
Pruning watermark hardening.
BlockchainLMDB::prune_tx_data()now fails the current batch on missing transaction rows (TX_DNE) instead of logging and continuing, sotx_prune_next_blockcannot advance on partial pruning. -
FCMP++ compile-path compatibility fixes. Updated wallet/core-test FCMP++ construction callsites for the current
genRctFcmpPlusPlusleaf-chunk API, and added explicit cached-chunk torct::fcmp_chunk_entryconversion in wallet construction to keep GCC 14 builds green. -
CI portability and fuzz gate hardening. Replaced GNU-only
xargs -rusage in Cargo absolute-path guard with a portable shell loop, and added a required fuzz-harness inventory smoke gate in Rust CI. -
Stale fuzz targets updated.
fuzz_fcmp_proof_deserializeandfuzz_tx_deserialize_fcmp_type7now pass the requiredsignable_tx_hash7th argument toverify().fuzz_block_header_tree_rootrewritten for the currentProveInputstruct and 4-argprove()signature. -
prune_tx_dataminer output lookup. When storing output-pruning metadata, RCT coinbase outputs are keyed under amount0in LMDB (same asadd_transaction); pruning now uses that amount forget_output_keyinstead of the plaintextvout.amount, avoidingOUTPUT_DNEduring prune for miner transactions.
🗑️ Removed
-
RingCT-era dead code excision (C++ wallet). Comprehensive removal of ring-signature infrastructure that is structurally unreachable on an FCMP++ chain. Deleted:
gamma_pickerclass andGAMMA_SHAPE/GAMMA_SCALEconstants,transfer_selected(non-RCT overload),wallet2::get_outsdecoy-fetching overloads (~700 lines),tx_add_fake_output,select_available_mixable_outputs,select_available_outputs_from_histogram,get_spend_proof/check_spend_proof(ring-sig-dependent proofs),get_min_ring_size/get_max_ring_size,m_confirm_non_default_ring_sizepreference, the entireringdb.h/ringdb.cppsubsystem (LMDB ring database), ring commands in simplewallet, spend proof RPC endpoints and FFI dispatch,boroSigstruct fromrctTypes.h, unreachablehf_version < HF_VERSION_FCMP_PLUS_PLUS_PQCbranch incryptonote_tx_utils.cpp,blockchain_blackballutility, andoutput_selection.cppunit test. Removed LMDB link dependency from wallet CMake target. -
Decoy and ring_size removal from Rust RPC. Removed
ring_size: u32parameter fromshekyl-wallet-rpctransfer API (types.rs,wallet.rs,ffi.rs), from the C++ FFI boundary (wallet2_ffi.h/.cpp), and from the C++ wallet RPCestimate_tx_size_and_weightcommand definition. DeletedDecoysstruct,MAX_RING_SIZEconstant,DecoyRpctrait and blanket implementation,OutputInformationstruct,rpc_pointhelper, andtest_decoy_rpctest fromshekyl-oxide. Removed/get_output_distribution.binroute fromshekyl-daemon-rpc. -
Bulletproof v1 ("Original") deletion. Deleted the entire
original/module tree and its tests fromshekyl-bulletproofs. RemovedBulletproof::Originalenum variant, v1prove()/read()functions, v1 match arms inverify/batch_verify/write_core, and the standaloneBulletproofsBatchVerifierstruct. Cleaned up deadinner_productandmul_vecmethods that were only used by v1 code. -
Light wallet support removed. Deleted all
m_light_walletstate,set_light_wallet,light_wallet_login,light_wallet_get_outs,import_outputs,get_unspent_outs,submit_raw_tx, and allif (m_light_wallet)branches fromwallet2.cpp/.h. Deletedwallet_light_rpc.hentirely. Removed light wallet API fromwallet2_api.h/wallet.h/wallet.cpp. Fundamentally incompatible with FCMP++ privacy model (sends view keys to remote server).
🔄 Changed
-
MLSAG naming debt resolved. Renamed
get_pre_mlsag_hashtoget_tx_prehash,mlsag_prehash/mlsag_prepare/mlsag_hash/mlsag_signtotx_prehash/tx_prepare/tx_hash/tx_signacross the device interface hierarchy (device.hpp,device_default.hpp/.cpp,device_ledger.hpp/.cpp),rctSigs.cpp/.h, andprotocol.cpp. Renamed LedgerINS_MLSAGconstant toINS_TX_SIGN. These functions are live code repurposed for FCMP++ transaction hashing; the names now reflect their actual role. -
Base58 encoding removed entirely. Deleted
src/common/base58.{h,cpp},tests/unit_tests/base58.cpp,tests/fuzz/base58.cpp, and all CMake references. RemovedCRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX,CRYPTONOTE_PUBLIC_INTEGRATED_ADDRESS_BASE58_PREFIX, andCRYPTONOTE_PUBLIC_SUBADDRESS_BASE58_PREFIXconstants from all network namespaces andconfig_t. No code path accepts or produces Base58 strings. -
Legacy address structs removed.
integrated_address,legacy_account_public_address, andlegacy_integrated_addressstructs removed fromcryptonote_basic_impl.cpp. Subaddress and integrated address logic removed from address encoding/decoding chokepoints.
🔄 Changed
-
Rust naming convention cleanup. Fixed phantom FFI function reference in
shekyl_pqc_verifydoc comment (referenced non-existentshekyl_pqc_verify_multisig_with_group_id, now points toshekyl_pqc_multisig_group_id). Renamed WindowsSystemInfo.dw_page_sizetopage_size(drop Hungarian notation). Renamedshekyl-wallet-rpc-rsbinary toshekyl-wallet-rpc(drop-rssuffix per Rust API Guidelines). -
Address encoding migrated to Bech32m.
get_account_address_as_str()andget_account_address_from_str()now call Rust FFI (shekyl_address_encode,shekyl_address_decode) for network-aware Bech32m encoding. Thesubaddressparameter is retained for API compatibility but ignored.address_parse_infofieldsis_subaddressandhas_payment_idare always false. -
Wallet proofs use Bech32m blob encoding. Spend proofs, tx proofs (in/out), reserve proofs, message signatures, multisig signatures, and signer keys are now encoded with purpose-specific HRPs via
shekyl_encode_blob/shekyl_decode_blobFFI. Version headers (SpendProofV1,InProofV2, etc.) removed; the HRP now serves as the type discriminator. -
shekyl-crypto-pqre-exportsshekyl-address. Theaddressmodule inshekyl-crypto-pqis now a re-export of the standaloneshekyl-addresscrate. The oldshekyl-crypto-pq/src/address.rshas been deleted. -
Tx-data prune watermark.
prune_tx_datanow storestx_prune_next_block(exclusive next height) instead of ambiguouslast_pruned_tx_data_heightvalues; legacy keys migrate on read/write. LMDB unit tests live intests/unit_tests/tx_data_pruning_lmdb.cpp(minimal block builder only; does not linktests/core_tests/chaingen.cppintounit_tests, avoiding duplicate object code and macOS linker unwind/diagnostic issues in CI). -
FCMP++ Rust dependency source moved in-repo.
shekyl-fcmpnow consumes vendoredshekyl-oxidecrates via path dependencies underrust/shekyl-oxide/instead of git dependencies plus local absolute-path[patch]overrides. This removes host-specific Cargo path failures in CI and keeps builds fully repo-local. -
Upstream sync and portability guardrails. Added vendored snapshot metadata at
rust/shekyl-oxide/UPSTREAM_MONERO_OXIDE_COMMIT, a divergence workflow (.github/workflows/shekyl-oxide-divergence.yml), and build workflow checks that fail on absolute local paths in Cargo manifests/config.
✨ Added
-
--prune-blockchaintransaction-data pruning. LMDB v6 addstxs_pqc_auths(split fromtxs_prunedatpqc_auths_offset), implementsprune_tx_data(batch 256 blocks, output metadata, watermark, TOCTOU height check), default depthCRYPTONOTE_TX_PRUNE_DEPTH(5000),pop_blockguard when verification data is gone, continuous pruning viaupdate_blockchain_pruning, RPCget_transactions.prunedandget_info.tx_prune_height. -
Staking FFI and config-driven tier parameters.
shekyl-stakingnow generates tier lock durations, yield multipliers, and max stake-claim range fromconfig/economics_params.jsonat build time (aligned withshekyl-economics). New FFI:shekyl_calc_per_block_staker_reward(128-bit division with optional overflow flag),shekyl_stake_tier_count,shekyl_stake_tier_name,shekyl_stake_max_claim_range. C++ uses these inblockchain.cpp,core_rpc_server.cpp, andsimplewalletinstead of duplicating tier strings or inlinemul128/div128_64reward math. -
FCMP++ transaction construction helper (
construct_fcmp_tx). New chaingen helper intests/core_tests/chaingen.cppthat builds fully valid FCMP++ transactions during core test replay: tree path assembly from the live LMDB curve tree,genRctFcmpPlusPlusproof generation, KEM decapsulation for per-input PQC keypair derivation, and PQC auth signing. This unblocks 30+ disabled core tests that relied on the oldconstruct_tx_rctstub. -
FCMP++ core test generators (Phase 7). Five new tests in
tests/core_tests/fcmp_tests.cpp:gen_fcmp_tx_valid: end-to-end FCMP++ transaction construction and pool acceptance during replaygen_fcmp_tx_double_spend: second FCMP++ spend of the same output rejectedgen_fcmp_tx_reference_block_too_old: stale referenceBlock rejectedgen_fcmp_tx_reference_block_too_recent: too-recent referenceBlock rejectedgen_fcmp_tx_timestamp_unlock_rejected: timestamp-basedunlock_timerejected
-
Verification caching unit tests. Six new GTest cases in
tests/unit_tests/fcmp.cppvalidatingcompute_fcmp_verification_hashdeterminism, sensitivity to proof/referenceBlock/key-image changes, null return for non-FCMP++ types, and multi-input handling. -
Deferred insertion boundary tests. New
tests/unit_tests/deferred_insertion.cppwith tests for: outputs not drainable before maturity, coinbase maturity window (60 blocks), regular tx maturity window (10 blocks), drain journal atomicity round-trip, and insertion ordering determinism across two DB instances. -
Pending tree add/pop stress test. New
tests/unit_tests/pending_tree_fuzz.cppwith randomized stress test (100 random leaves, multi-height draining), add/remove round-trip, drain journal CRUD, and leaf removal correctness. -
fuzz_tx_deserialize_fcmp_type7Rust fuzz target. New cargo-fuzz target inrust/shekyl-fcmp/fuzz/that exercises FCMP++ proof verification with transaction-structured random inputs: pseudoOuts, proof blobs, PQC hashes, corrupted type bytes, empty proofs, and mismatched input counts. -
Comprehensive staking test suite. New test coverage across C++ and Rust:
tests/unit_tests/staking.cpp: 20+ GTest unit tests coveringtxin_stake_claimandtxout_to_staked_keyserialization round-trips, reward integer math (includingmul128/div128_64vsdoubledivergence at large values), helper function coverage (get_inputs_money_amount,check_inputs_overflow,check_inputs_types_supported,get_output_staking_info,set_staked_tx_out), stake weight/tier FFI validation, and variant type handling.tests/core_tests/staking.cpp+staking.h: 18 chaingen core tests covering staking lifecycle (stake output creation), invalid claim rejection (inverted range, oversized range, future height, wrong watermark, wrong amount, non-staked output, output not in tree), lock period enforcement (invalid tier), rollback correctness (pool balance, watermark), txpool handling, sorted-input enforcement, and multi-tier staking.rust/shekyl-staking/src/tiers.rs: 10 edge-case tests including exhaustive invalid tier ID rejection, ordering invariants for yield multiplier and lock blocks, contiguous ID verification, and positive parameter assertions.rust/shekyl-staking/fuzz/fuzz_targets/fuzz_claim_reward.rs: cargo-fuzz target that generates random accrual records and verifies reward computation invariants (no overflow, reward <= pool, weight monotonicity, cumulative bounds).
🔄 Changed
-
Universal deferred curve-tree insertion (Decision 15). All outputs (coinbase, regular, staked) now enter the
pending_tree_leavestable at creation and drain into the curve tree only after their type-specific maturity height (coinbase: +60, regular: +10, staked: max(effective_lock_until, +10)). Thepending_staked_*identifiers were renamed topending_tree_*across all database interfaces. The drain journal (pending_tree_drain) now stores full 136-byte entries (maturity_height + leaf_data) for exactpop_blockreversal instead of just a drain count.pop_blockrestores drained leaves to pending and removes the popped block's own pending entries. -
FCMP_REFERENCE_BLOCK_MIN_AGE reduced to 5 (Decision 14). With maturity enforced by deferred tree insertion, MIN_AGE now serves only as a reorg safety margin (5 blocks ≈ 10 minutes). The old static_asserts tying MIN_AGE to unlock windows have been removed.
-
Timestamp-based
unlock_timerejected (Decision 13). Transactions withunlock_time >= CRYPTONOTE_MAX_BLOCK_HEIGHT_SENTINEL(500M) are now rejected incheck_tx_outputs. Only height-based lock times are accepted. -
prune_tx_datastatus clarification. The output-metadata pruning loop indb_lmdb.cppis a plumbing-only stub (TODO(phase6f)). Thestore_output_metadata,get_output_metadata, andis_output_prunedinterfaces are live, but the block-iteration pruning loop does not execute.
🗑️ Removed
-
Vestigial hard fork constants. Removed
HF_VERSION_CLSAGandHF_VERSION_MIN_V2_COINBASE_TXfromcryptonote_config.h. All test references replaced with literal1. -
Legacy tests incompatible with FCMP++ consensus. Disabled 30+ core and unit tests that relied on Monero-era transaction construction (
RCTTypeBulletproofPlus, CLSAG ring signatures, v1/v2 transactions):tests/core_tests/chaingen_main.cpp: Disabledgen_simple_chain_001,gen_simple_chain_split_1,gen_chain_switch_1,gen_ring_signature_1,gen_ring_signature_2, alltxpool_*tests, allgen_double_spend_*tests,gen_block_reward, allgen_bpp_*Bulletproofs+ tests, and severalgen_tx_*tests whose setup required valid user transactions. These tests construct transactions viaMAKE_TX/construct_tx_rctwhich produceRCTTypeFcmpPlusPlusPqcstubs with emptypqc_auths, rejected bycheck_tx_inputseven in FAKECHAIN mode.tests/unit_tests/bulletproofs.cpp: All three weight tests (weight_equal,weight_more,weight_pruned) prefixed withDISABLED_and hex blobs removed. Shekyl'srctSigBaseserialization rejects any type other thanRCTTypeFcmpPlusPlusPqc(type 7), so oldRCTTypeBulletproofPlus(type 6) blobs fail to deserialize.- Re-enabling requires a chaingen FCMP++ transaction generator that produces valid PQC auth signatures and curve-tree membership proofs.
🔄 Changed
-
Upstream monero-oxide dependencies renamed to shekyl-oxide. Updated
shekyl-fcmp/Cargo.tomland all Rust source files to use the renamed packages from the monero-oxide fork (monero-fcmp-plus-plus→shekyl-fcmp-plus-plus,monero-generators→shekyl-generators).Cargo.lockadvanced from pin92af05eto416d8d1which includes the completemonero-oxide/→shekyl-oxide/directory and package rename. -
shekyl-fcmpcrate cleanup. Removed unusedsha2andshekyl-crypto-pqdependencies fromrust/shekyl-fcmp/Cargo.toml. Renamed the misleadingProveError::InputCountMismatchvariant toProveError::PqcHashMismatchwith a clearinput_indexfield indicating which input has a mismatched leafh_pqcvspqc_authcommitment.
🐛 Fixed
-
Private member access in pending tree unit tests. Fixed 18 compile errors in
pending_tree_fuzz.cppand 4 indeferred_insertion.cppon macOS CI where calls toadd_pending_tree_leaf,drain_pending_tree_leaves,add_pending_tree_drain_entry,get_pending_tree_drain_entries,remove_pending_tree_drain_entries, andremove_pending_tree_leafwere calling private overrides onBlockchainLMDB. Changed all test methods to useBlockchainDB&references, accessing the public base class interface. -
CI compile errors across all platforms. Fixed compilation failures in the new staking and FCMP++ test suites:
tests/core_tests/staking.cpp: Added missingfill_tx_sourcesdeclaration tochaingen.hand movedBlockchain::check_stake_claim_inputfrom the private section to the public API so core tests can call it withoutIN_UNIT_TESTS.tests/unit_tests/fcmp.cpp: Fixed serialization calls to usedo_serialize(ar, v)instead of non-existentv.serialize(ar)member; replacedbinary_archive<false>(istringstream&)with the correctbinary_archive<false>(span<const uint8_t>)constructor; fixedshekyl_pqc_verifycall to include the requiredscheme_idfirst argument and corrected parameter order.tests/unit_tests/staking.cpp: Samebinary_archive<false>constructor fix — replacedistringstreamwithepee::span<const uint8_t>in all four serialization round-trip tests.- macOS CI: Added
zstdto Homebrew dependencies and fixed CMake to usePkgConfig::ZSTDimported target instead of bare library name, resolvingld: library 'zstd' not foundon macOS Homebrew where the library lives in a non-standard path (/opt/homebrew/lib).
-
RPC estimate_claim_reward floating-point precision bug. The
on_estimate_claim_rewardRPC handler useddouble-precision arithmetic for reward estimation, which diverges from the consensusmul128/div128_64path whentotal_weighted_stake > 2^53. Fixed to use identical 128-bit integer math, ensuring wallet reward estimates always match consensus.
🐛 Fixed
-
FCMP++ wallet precompute metadata and input consistency checks.
transfer_selected_rctand multisig proof prep now read tree depth from RPC metadata (tree_depth) instead ofpath_blob[0], enforce that all selected inputs share the same reference block/depth snapshot, and reject empty precomputed paths. This fixes silent spend-construction failures. -
Stake-claim input routing in consensus verification.
Blockchain::check_tx_inputsnow routes puretxin_stake_claimtransactions through the claim-specific input checks before generic FCMP++txin_to_keyvalidation, preventing incorrect rejection of valid stake-claim transactions that useRCTTypeFcmpPlusPlusPqc. -
Stake-claim reward math overflow defense. Added a defensive
q_hi != 0check afterdiv128_64in claim reward computation, rejecting impossible overflow states instead of silently truncating. -
Claim transaction PQC signing correctness/performance. Removed wallet master-key fallback for claim input signing and now require per-output shared-secret rederivation for all claim inputs. Claim signing keypairs are derived once per input and reused for both
pqc_authspublic key and signature generation. -
Curve-tree path RPC returns spendable reference block.
get_curve_tree_pathnow returns areference_blockat leastFCMP_REFERENCE_BLOCK_MIN_AGE + 1behind tip, avoiding immediate mempool rejection of freshly built transactions that used a too-recent tip anchor. -
PQC derivation index correctness and duplicate derivation overhead. Spend-path and multisig PQC key derivation now use
m_internal_output_index(matching KEM encapsulation/decapsulation) and derive each per-input keypair once per transaction, reusing it for bothH(pqc_pk)and signing. -
Staked-output FCMP++ path precompute filtering. Wallet precompute/incremental updates now skip still-locked staked outputs (
m_stake_lock_until > current_height) to avoid daemon path lookup errors. -
Stake-claim rollback completeness.
BlockchainDB::remove_transactionnow fully reversestxin_stake_claimstate on reorg: watermark is restored to its pre-claim value (or removed for first-time claims) and the claimed amount is credited back into the staker reward pool. Previously only the spent key was removed, leaving claim-progress accounting permanently advanced after a reorg. -
Txpool key-image handling for stake claims. All six txpool functions that walk transaction inputs (
insert_key_images,remove_transaction_keyimages,have_tx_keyimges_as_spent,have_key_images,append_key_images,mark_double_spend) now handletxin_stake_claiminputs alongsidetxin_to_key. Previously they usedCHECKED_GET_SPECIFIC_VARIANT(..., txin_to_key, ...)which caused immediate false-return on any stake-claim input, breaking mempool bookkeeping for claim transactions. -
remove_transaction_keyimagesno longer returns early on error. The function now continues removing remaining key images instead of aborting at the first mismatch, eliminating the partial-cleanup semantics noted by the long-standing FIXME. -
Core helper support for
txin_stake_claim.get_inputs_money_amountandcheck_inputs_overflownow handle bothtxin_to_keyandtxin_stake_claiminput variants instead of failing on the latter. These are called unconditionally for all transactions (viacheck_money_overflow), so the old hard-cast totxin_to_keywould reject any transaction containing a stake claim.
🔒 Security
-
FFI buffer zeroization before free.
shekyl_buffer_freenow wipes buffer contents prior to deallocation, reducing secret-key residue risk in allocator-managed memory. -
Wallet KEM key management fix.
generate_pqc_key_material()now generatesHybridX25519MlKemKEM keypairs viashekyl_kem_keypair_generate()instead ofHybridEd25519MlDsasigning keypairs. The wallet-level PQC keys (m_pqc_public_key/m_pqc_secret_key) are encapsulation/decapsulation keys; per-output ML-DSA-65 signing keys are always derived from the KEM shared secret at spend time. -
Full hybrid ciphertext storage in tx_extra tag 0x06. All KEM encapsulation sites (coinbase, claim, regular transfers) now store the complete 1120-byte hybrid ciphertext (
x25519_ephemeral_pk[32] || ml_kem_ct[1088]) instead of only the ML-KEM portion. This enables correct hybrid decapsulation during wallet scanning and seed restore.
✨ Added
-
FCMP++ wallet transaction construction (Phase 5).
transfer_selected_rctnow builds transactions using full-chain membership proofs instead of ring signatures:- Inputs contain only the real output (no decoy selection).
genRctFcmpPlusPlusgenerates the combined Bulletproofs+ and FCMP++ membership proof.- Per-input PQC auth signatures use ML-DSA-65 keypairs derived from the KEM shared secret and output index.
construct_tx_with_tx_keyadds KEM encapsulation (tag 0x06) andH(pqc_pk)leaf hashes (tag 0x07) for each output, and skips wallet-level PQC signing.
-
KEM decapsulation during wallet scanning.
process_new_transactionnow extracts hybrid KEM ciphertexts fromtx_extratag 0x06, callsshekyl_kem_decapsulatewith the wallet's KEM secret keys, and stores the resulting 64-byte combined shared secret intransfer_details::m_combined_shared_secret. This enables per-output PQC key derivation at spend time. -
FCMP++ fee estimation.
estimate_rct_tx_sizenow accounts for the FCMP++ membership proof size (shekyl_fcmp_proof_len), per-input PQC auth envelopes (~5400 bytes each), and per-output KEM ciphertexts and leaf hashes. -
GUI wallet QR code. Receive page now renders a real QR code encoding the full FCMP++ Bech32m address via
qrcode.react. -
GUI wallet fee preview. Send page shows an estimated transaction fee before submission, debounced as the user types.
🗑️ Removed
-
CLSAG device interface methods. Removed
clsag_prepare,clsag_hash, andclsag_signvirtual methods fromdevice.hppand all implementations (device_default.cpp,device_ledger.cpp). Shekyl never supported CLSAG; the device interface now only exposes FCMP++ methods. -
get_outs/get_outs.binRPC endpoints. Removed the ring member fetching endpoints from the C++ daemon (core_rpc_server), the FFI dispatch tables (core_rpc_ffi.cpp), and the Rust daemon RPC (shekyl-daemon-rpc). FCMP++ uses full-chain membership proofs; there is no decoy selection. -
Dead hard fork constants. Removed
HF_VERSION_MIN_MIXIN_4/6/10/15,HF_VERSION_SAME_MIXIN,HF_VERSION_ENFORCE_MIN_AGE,HF_VERSION_EFFECTIVE_SHORT_TERM_MEDIAN_IN_PENALTY,HF_VERSION_REJECT_SIGS_IN_COINBASE,HF_VERSION_ENFORCE_RCT,HF_VERSION_DETERMINISTIC_UNLOCK_TIMEfromcryptonote_config.h. These were defined but never referenced in production code.HF_VERSION_CLSAGandHF_VERSION_MIN_V2_COINBASE_TXare retained for test compilation until Phase 7 rewrites the legacy tests.
✨ Added
- Zstd compression for Levin P2P relay (Phase 6e). P2P payloads above
256 bytes are transparently compressed with zstd (level 1) before relay.
A new
LEVIN_PACKET_COMPRESSEDflag (0x10) in the Levin header marks compressed frames. Peers negotiate compression viaP2P_SUPPORT_FLAG_ZSTD_COMPRESSION(0x02) in the handshake support flags. Reduces relay bandwidth by ~10-20% for FCMP++ transactions, especially important for Tor/I2P connections. Compression is optional at compile time (requires libzstd); decompression always succeeds if the flag is set.
📚 Documentation
- Updated
DAEMON_RPC_RUST.md. Fixed stale references toget_outs.binandget_curve_tree_root; corrected endpoint counts and cutover test steps.
🐛 Fixed
-
rct::keymissingoperator!=. Addedoperator!=to thekeystruct inrctTypes.h. The operator was present for cross-type comparisons (rct::keyvscrypto::public_key) but not forrct::keyvsrct::key, causing compilation failures on all platforms when comparing pseudo-outs to expected zero-commitments in the stake claim verification path. -
MSVC
binary_archiveconstructor mismatch. Fixedwallet2.cppto useepee::strspan<std::uint8_t>instead ofstd::istringstreamfor constructingbinary_archive<false>, which MSVC could not resolve. -
Memory leak on exception in PQC auth signing. Added RAII scope guard for
ShekylPqcKeypairbuffers intransfer_selected_rctPhase C, ensuring Rust-allocated key material is freed even ifTHROW_WALLET_EXCEPTION_IFthrows mid-loop. -
Secret key material not wiped on KEM decapsulation failure. The stack buffer in
process_new_transactionKEM decapsulation is now wiped unconditionally (success or failure), preventing partial key material from lingering on the stack. -
Shadowed
tx_extra_fieldsvariable in KEM decapsulation. Removed redundant innertx_extra_fieldsreference that shadowed the outer one inprocess_new_transaction, using the already-resolved outer reference instead.
🔄 Changed
-
Decoy selection functions are dead code.
get_outs,tx_add_fake_output, andlight_wallet_get_outsinwallet2.cppare no longer called from the active transfer path. They remain in the codebase for reference and will be removed in a follow-up cleanup. -
Claim transaction indistinguishability (Phase 4 — CRITICAL). Rewrote
wallet2::create_claim_transaction()to produce privacy-preserving claim transactions that blend into the anonymity set:- Uses
RCTTypeFcmpPlusPlusPqcwith Bulletproofs+ range proofs instead ofRCTTypeNullwith plaintext amounts. - Adds a dummy change output (amount = 0) to match the standard 2-output transaction structure, preventing structural fingerprinting.
- Performs hybrid KEM derivation (X25519 + ML-KEM-768) via
shekyl_fcmp_derive_pqc_keypair()for per-output PQC keys instead of reusing the wallet master PQC key. - Embeds ML-KEM ciphertexts in
tx_extraunder tag0x06andH(pqc_pk)leaf hashes under new tag0x07. - Signs with per-output KEM-derived PQC keys, not the wallet-level key.
- Sets deterministic pseudo-outs (
zeroCommit(claim_amount)) for each stake claim input to satisfy the Bulletproofs+ balance check.
- Uses
-
Consensus rejects
RCTTypeNullfor non-coinbase v3 transactions.check_tx_inputsnow enforces that only coinbase (txin_gen) may useRCTTypeNull. All other v3 transactions (including stake claims) must useRCTTypeFcmpPlusPlusPqcwith confidential amounts. Claim transactions are validated within the FCMP++ handler with their own sub-path that verifies pseudo-out determinism, PQC ownership, and pool balance while skipping the membership proof (which is not applicable totxin_stake_claiminputs).
✨ Added
-
TX_EXTRA_TAG_PQC_LEAF_HASHES(0x07). Newtx_extrafield (tx_extra_pqc_leaf_hashes) stores per-outputH(pqc_pk)values — the 32-byte Blake2b-512 hashes of each output's derived ML-DSA-65 public key. Used by curve tree insertion to commit the correct PQC ownership hash to each leaf instead of a zero placeholder. -
Curve tree leaves use actual
H(pqc_pk)fromtx_extra. Thecollect_outputs/make_leafpath inblockchain_db.cppnow extractsH(pqc_pk)values from the0x07tag, replacing the zero placeholder that was previously committed to the 4th leaf scalar. This enables the PQC ownership cross-check for stake claim verification. -
Coinbase transactions emit
H(pqc_pk)leaf hashes.construct_miner_txnow derives per-output PQC keypairs via KEM shared secrets and includes theirH(pqc_pk)values in the0x07tx_extrafield alongside the existing KEM ciphertexts in0x06.
🔒 Security
-
Integer-only stake reward computation. Replaced floating-point arithmetic (
(double)total_reward * weight / total_weighted_stake) with 128-bit integer math (mul128/div128_64) incheck_stake_claim_inputto eliminate rounding errors that could cause determinism mismatches across platforms. -
Batch pool balance validation for stake claims. Moved the staker pool balance check from per-claim (
check_stake_claim_input) to a batch check incheck_tx_inputsthat sums all claim amounts first. Prevents multiple claims in the same block from independently passing the balance check and overdrawing the pool. -
PQC ownership cross-check on stake claims. Each
txin_stake_claimnow verifies that theH(pqc_pk)stored in the curve tree leaf (bytes 96–128) matchesshekyl_fcmp_pqc_leaf_hash(pqc_auths[i].hybrid_public_key), preventing reward claims for outputs the claimer does not own the PQC key for.
🐛 Fixed
- Stake claim key image cleanup on reorg.
remove_transactioninblockchain_db.cppnow handlestxin_stake_claimkey images in addition totxin_to_key, preventing stale key images from persisting after block pops.
🔄 Changed
-
Sorted input enforcement extended to stake claims. The sorted-inputs check in
check_tx_inputsnow covers bothtxin_to_keyandtxin_stake_claimkey images, ensuring consistent ordering rules across all input types. -
Third-party headers treated as SYSTEM includes.
external/,external/rapidjson,external/easylogging++, andexternal/supercopare now-isystemin CMake, suppressing-Wsuggest-overrideand other warnings from third-party code while keeping strict warnings for first-party code.
🗑️ Removed
-
Dead
check_ring_signaturefunction. Removed unused ring signature verification fromblockchain.cppand its declaration fromblockchain.h. Shekyl uses FCMP++ from genesis; ring signatures are never validated. -
Dead
expand_transaction_2function. Removed the no-op transaction expansion function fromblockchain.cppand its declaration fromblockchain.h. FCMP++ does not use mixRing expansion. -
Dropped
serde_jsondev-dependency fromshekyl-fcmp. Replaced the JSON round-trip test with a byte-level serialization check, reducing the dev-dep surface.
📚 Documentation
- Synced
docs/FCMP_PLUS_PLUS.mdcurve-tree text with consensus: outputs are indexed at creation; maturity is enforced viareferenceBlockand other rules, not by delaying leaf insertion. - Clarified
docs/POST_QUANTUM_CRYPTOGRAPHY.mdto usepqc_auths(per-input) terminology consistently. - Documented mempool FCMP verification-cache id:
compute_fcmp_verification_hashbinds proof +referenceBlock+ key images (comment inblockchain.cpp). - Noted the monero-oxide commit pin in
rust/shekyl-fcmp/Cargo.tomlcomments (lockfile remains authoritative). - Updated
docs/STAKER_REWARD_DISBURSEMENT.mdwith integer arithmetic, batch pool check, PQC cross-check, and sorted input consensus rules.
✨ Added
-
Block-inclusion FCMP++ cache fast path. When a transaction was previously verified in the mempool and arrives in a block,
check_tx_inputsskips the expensiveshekyl_fcmp_verifyFFI call (~35ms/input) while still running all structural checks (referenceBlock, depth, key images, PQC auth). -
construct_leafnow accepts PQC key hash parameter. The Rust FFI functionshekyl_construct_curve_tree_leaftakes a 4thh_pqc_ptrargument (32 bytes) to set the 4th leaf scalar. Callers pass zero bytes until per-output PQC commitments are wired in Phase 3. -
Deferred staked leaf insertion infrastructure. Added
pending_staked_leaves(LMDB DUPSORT/DUPFIXED table keyed bylock_until_heightwith 128-byte leaf values) andpending_staked_drain(block_height → drain count) tables to the blockchain database layer. Five new methods onBlockchainDB:add_pending_staked_leaf,drain_pending_staked_leaves,set_pending_staked_drain_count,get_pending_staked_drain_count, andremove_pending_staked_drain_count. This enables staked outputs whoseeffective_lock_until > block_heightto be parked in a pending table and batch-inserted into the curve tree when they mature. -
Comprehensive FCMP++ test suite and fuzz targets (Phase 7). Added 6
cargo-fuzztargets acrossrust/shekyl-fcmp/fuzz/(proof deserialization, curve tree leaf hashing, block header tree root mismatch) andrust/shekyl-crypto-pq/fuzz/(Bech32m address decoding, KEM decapsulation with corrupted ciphertexts). Extended Rust unit tests inproof.rs,tree.rs,leaf.rs,kem.rs,address.rs, andderivation.rscovering prove/verify round-trips, hash grow/trim inverse properties, boundary values, and cross-crate consistency. Extended C++ unit tests intests/unit_tests/fcmp.cppwith RCTTypeFcmpPlusPlusPqc serialization round-trip, key image y-normalization, referenceBlock staleness constants, and empty proof rejection. Added PQC rederivation criterion benchmark (rust/shekyl-crypto-pq/benches/pqc_rederivation.rs) targeting < 100ms per output for the full ML-KEM-768 decapsulation + HKDF-SHA-512 + ML-DSA-65 keygen pipeline. -
Stressnet tooling for FCMP++ pre-audit gate (Phase 7.7). Added
tests/stressnet/with configuration, load generator, and monitoring scripts for a 4-week sustained-load testnet. The stressnet exercises curve tree growth, verification caching, wallet restore correctness, pruned vs. full node storage, staking lifecycle, and block validation latency under near-block-weight-limit load. Includesconfig.yamlwith load profiles,load_generator.pyfor synthetic transaction submission, andmonitor.pyfor real-time metric collection, consensus checking, and daily report generation. -
Security audit scope document (Phase 9). Added
docs/AUDIT_SCOPE.mddefining the scope for a third-party security review of the 4-scalar leaf circuit modification. Covers soundness, zero-knowledge, and completeness verification for theH(pqc_pk)extension, Shekyl fork modifications to monero-fcmp-plus-plus, PQC commitment binding, and the FFI verification boundary. Includes materials list, auditor guidance questions, success criteria, and timeline. -
Mainnet gate: stressnet and audit prerequisites in release checklist. Updated
docs/RELEASE_CHECKLIST.mdwith "Stressnet stable for 4 consecutive weeks" and "4-scalar leaf circuit audit completed" as hard prerequisites for mainnet launch.
🔄 Changed
-
Renamed
src/ringct/tosrc/fcmp/for naming consistency. Shekyl does not use ring signatures; the directory now reflects the actual FCMP++ confidential transaction system. CMake targets renamed fromringct/ringct_basictofcmp/fcmp_basic. All#include "ringct/..."paths updated across 44 source and test files. Log categories, user-facing strings ("RingCT" → "FCMP"), JSON keys, and documentation updated. Therct::namespace is preserved for now as a separate future rename. -
Unified coinbase transaction version to v3.
construct_miner_txandbuild_genesis_coinbase_from_destinationsnow emittx.version = 3, matching regular FCMP++ transactions. Allminer_tx && tx.version == 2checks have been widened to>= 2acrossblockchain_db,blockchain,wallet2, and test infrastructure. Thepqc_authsserialization gate (!txin_gen) already excluded coinbase, so v3 coinbase serializes identically to v2 minus the version byte.
🐛 Fixed
-
Fixed wallet API compilation errors after ring-signature removal.
wallet/api/wallet.cppstill referenced the undefinedfake_outs_countvariable and calledestimate_feewith the old 12-argument signature. Replacedfake_outs_countwith0(FCMP++ has no decoys) and updatedestimateTransactionFeeto use the simplified 8-argumentestimate_feesignature with hardcodeduse_per_byte_fee=true,use_rct=true,use_view_tags=true. -
Fixed CI build failure from removed legacy RCT types in test files. Stripped all references to removed
rct::Bulletproof,rct::RCTConfig,rct::RangeProofType,rct::RCTTypeBulletproofPlus,rct::clsag,rct::proveRctCLSAGSimple/verRctCLSAGSimple, andrct::genRctSimplefrom:chaingen.h/.cpp,bulletproof_plus.cpp/.h,chain_switch_1.cpp,wallet_tools.h/.cpp,bulletproofs.cpp(unit),ringct.cpp(unit),serialization.cpp(unit),ver_rct_non_semantics_simple_cached.cpp,json_serialization.cpp,fuzz/bulletproof.cpp, and all performance test headers. Removed legacy-only test cases; updated shared test helpers to dropRangeProofType/bp_versionparameters.
🗑️ Removed
- Dead verification cache code (
verRctNonSemanticsSimple,ver_rct_non_semantics_simple_cached). Removed the stubverRctNonSemanticsSimplefromrctSigs.cpp/.h(returnedtrueunconditionally), thever_rct_non_semantics_simple_cachedwrapper and itsver_rct_non_semhelper fromtx_verification_utils.cpp/.h, the unusedrct_ver_cache_ttype alias andm_rct_ver_cachemember fromBlockchain, and the deadRCT_CACHE_TYPEconstant fromcheck_tx_inputs. Real FCMP++ verification lives incheck_tx_inputs(blockchain.cpp) and the mempool usescompute_fcmp_verification_hashfor caching.
🔒 Security
-
CRITICAL: PQC signed payload now binds to prunable FCMP++ data (Phase 4c).
get_transaction_signed_payloadnow includesH(serialize(RctSigPrunable))in the signed payload, binding PQC signatures to the FCMP++ proof, pseudoOuts, curve_trees_tree_depth, and Bulletproofs+. Without this, an attacker could substitute different prunable data without invalidating PQC signatures, breaking the dual-layer security model. -
CRITICAL: Wired stake claim validation in
check_tx_inputs(Phase 4e audit fix). The non-FAKECHAIN gate incheck_tx_inputsrejected allRCTTypeNulltransactions, which includes pure stake-claim txs. The gate now allowsRCTTypeNulltransactions through when all inputs aretxin_stake_claim. Additionally, theRCTTypeNullswitch case now callscheck_stake_claim_inputfor each claim input and checks key image double-spend — previously itbreaked without any validation. -
HIGH: Bound all inputs' H(pqc_pk) hashes into PQC signed payload.
get_transaction_signed_payloadnow appendsH(pqc_pk_0) || ... || H(pqc_pk_{N-1})after the per-input header blob, preventing key-substitution attacks where an attacker replaces one input's PQC key without invalidating other signatures. -
MEDIUM: Stake claim curve tree leaf verification (Phase 4e).
check_stake_claim_inputnow verifies the staked output's leaf is present in the curve tree by checkingstaked_output_index < get_curve_tree_leaf_count()and reading the leaf withget_curve_tree_leaf(). Previously, only the lock period check was performed, which didn't guarantee the leaf had been inserted into the tree. -
MEDIUM: PQC
auth_versionandflagsconsensus enforcement.verify_transaction_pqc_authnow rejectsauth_version != 1andflags != 0, enforcing spec steps 6a/6c. Previously these fields were serialized and signed over but never validated. -
LOW: Single-signer
hybrid_public_keysize enforcement.verify_transaction_pqc_authnow verifies single-signer key blobs are exactlyHYBRID_SINGLE_KEY_LEN(1996 bytes). Previously only multisig keys had size bounds checks; single-signer keys relied solely on the FFI call to reject malformed keys. -
LOW: Added deserialization size bounds for
pqc_authenticationblobs.hybrid_public_keyandhybrid_signaturevectors are now rejected during deserialization if they exceedPQC_MAX_PUBLIC_KEY_BLOBorPQC_MAX_SIGNATURE_BLOB, preventing memory-exhaustion attacks via oversized PQC fields.
🐛 Fixed
-
HIGH: Fixed
pop_block()off-by-one for staked-output curve tree removal. The height used for staked-output eligibility checking was captured afterremove_block(), using the post-pop height instead of the removed block's height. This caused a mismatch withadd_block()'s logic: outputs added at the exact lock boundary were inserted during add but not removed during pop, leaving orphaned leaves in the curve tree. -
HIGH: Fixed
pseudoOutsserialization mismatch in genericrctSigBase. The genericBEGIN_SERIALIZE_OBJECT()path inrctSigBaseunconditionally includedpseudoOuts, even forRCTTypeFcmpPlusPlusPqcwhere pseudo-outs live in the prunable section. Now gated withif (type != RCTTypeFcmpPlusPlusPqc)to match the custom serializer. -
MEDIUM:
get_curve_tree_pathRPC now fails on missing layer hashes. Previously, a failedget_curve_tree_layer_hash()silently inserted zeros into the proof path, potentially generating invalid proofs from inconsistent DB state. Now returnsCORE_RPC_ERROR_CODE_INTERNAL_ERROR. -
CRITICAL: Fixed incorrect existing_child in internal layer hash propagation (
grow_curve_tree). When updating an existing child chunk's hash, the parent's Pedersen commitment was computed withexisting_child = 0instead of the previous cycle-scalar. This produced wrong chunk hashes for any block that updated (rather than created) a child chunk. The fix tracks both old and new hashes throughupdated_chunk_tand passes the previous cycle-scalar tohash_grow. -
CRITICAL: Replaced O(N)
trim_curve_treewith incrementalhash_trim. Reorgs previously read all remaining leaves, cleared the tree, and rebuilt from scratch — a liveness risk at scale. The new implementation useshash_trim_selene/hash_trim_heliosFFI to surgically update only the affected chunks, then propagates the old→new deltas up through internal layers. Complexity is now O(removed × log N). -
CRITICAL: Enforced output maturity via
FCMP_REFERENCE_BLOCK_MIN_AGE. Outputs enter the curve tree at creation time (maximising the anonymity set). Maturity is enforced at spending time by requiring the reference block to be at leastCRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW(60) blocks behind the tip. Addedstatic_asserts incryptonote_config.hto prevent regression. -
HIGH: Validated meta reads in
save_curve_tree_checkpoint. The function now checks that root, depth, and leaf_count were all successfully read from meta before storing a checkpoint. If any value is missing or leaf_count is 0, the checkpoint is skipped with a log warning instead of storing a corrupt zero-valued checkpoint.
🔄 Changed
-
Consensus:
curve_trees_tree_depthvalidation now accepts<= current. The referenceBlock's tree may have fewer layers than the current tip (depth is monotonically non-decreasing). The strict!=check was replaced with a range check(0, current_depth], and the FCMP++ proof verifier provides the authoritative depth validation. -
Consensus: Removed ring-based validation path from
check_tx_inputs. Shekyl starts at genesis with FCMP++; the legacy ring-signature per-input validation is unreachable dead code. Theelsebranch now immediately rejects non-FCMP++ transactions with a clear error message. -
Coinbase KEM: Added warning when miner address lacks PQC public key. If a miner's address has no PQC key at the FCMP++ hard fork, a warning is logged noting that the output will have
H(pqc_pk) = 0in the curve tree — a distinguishable pattern. -
RPC: Replaced hardcoded chunk widths with FFI calls.
get_curve_tree_pathnow callsshekyl_curve_tree_selene_chunk_width()andshekyl_curve_tree_helios_chunk_width()instead of using static constants. -
RPC: Added
reference_heightandleaf_counttoget_curve_tree_pathresponse. Wallets can now verify response freshness and detect stale paths without parsing the reference block hash. -
RPC: Added
MAX_OUTPUTS_PER_RPC_REQUEST(64) rate limit toget_curve_tree_pathto prevent abuse from unbounded requests.
✨ Added
-
RPC:
get_curve_tree_infoendpoint returns root hash, depth, leaf count, and chain height for the current curve tree state. -
RPC:
get_curve_tree_checkpointendpoint retrieves a stored checkpoint (root, depth, leaf_count) at a given block height, needed for fast-sync.
📚 Documentation
- Documented
verRctNonSemanticsSimplestub status: the FCMP++ membership proof is verified in the main consensus path (check_tx_inputs), not in the verification-caching path. Added TODO for Phase 5 unification. Documented coinbase— superseded: coinbase is now version 3, unified with regular transactions.tx.version = 2rationale- Documented LMDB post-delete cursor contract (
MDB_GET_CURRENTaftermdb_cursor_delreturns the next item) in pruning and GC loops. - Added
ct_layer_chunk_keybit-layout comment explaining the 8-bit layer / 56-bit chunk index encoding for LMDB integer keys. - Documented
construct_leafzero 4th scalar (H(pqc_pk)) and the tree rebuild requirement when PQC per-output keys are activated. - Documented depth tracking semantics (root layer index, not layer count) and
root detection invariant in
grow_curve_tree. - Added TODO for async/batched checkpoint+pruning in
add_block. - Documented
get_curve_tree_rootempty-tree return semantics (returnshash_init, callers should checkleaf_count).
🗑️ Removed
- Legacy RCT and mixin references stripped from wallet layer. Completed
the wallet-side refactor removing all references to legacy ring sizes,
adjust_mixin,default_mixin,m_default_mixin,RCTConfig, and mixin-count parameters:wallet2.h: Removedestimate_feemixin/bulletproof/clsag params,adjust_mixin(),default_mixin()getter/setter,m_default_mixinmember,rct_configfrompending_txandtransfer_selected_rct.wallet2.cpp: Removed mixin fromestimate_rct_tx_size,estimate_tx_size,estimate_tx_weight,estimate_feesignatures and all call sites. Removedadjust_mixin()definition, JSON serialization ofdefault_mixin, constructor initialization. Removedconst bool clsag/bulletproof/bulletproof_plus = truepatterns.wallet_errors.h: Removedmixin_countfield fromnot_enough_outs_to_mixerror struct.wallet2_ffi.cpp: Replacedadjust_mixincalls with constant0.wallet_rpc_server.cpp: Replacedadjust_mixincalls with constant0.wallet2_api.h,wallet.h,wallet.cpp: Removedmixin_countparameter fromcreateTransactionandcreateTransactionMultDest.unsigned_transaction.cpp: Simplifiedmixin()andminMixinCount()to always return 0 (FCMP++ has no explicit mixin).simplewallet.cpp: Removed ring-size parsing,adjust_mixincalls, anddefault_mixindisplay. All fake_outs_count set to 0.
- Legacy RCT references stripped from all src/ files. Removed all
remaining references to CLSAG, legacy RCT types,
RCTConfig,mixRing, andlow_mixinfrom device drivers, Trezor protocol, RPC handlers, blockchain verification, transaction utilities, wallet, and serialization:device_ledger.cpp: RemovedINS_CLSAGdefine, legacy type branches inmlsag_prehash, replacedclsag_prepare/clsag_hash/clsag_signwith FCMP++ TODO stubs.protocol.cpp/protocol.hpp(Trezor): Removedrct::Bulletproofvariant,is_simple()/is_req_bulletproof()/is_bulletproof()/is_clsag()helpers,mixRingresize, CLSAG deserialization instep_final_ack. Addedis_fcmp_pp()helper.core_rpc_server.cpp/core_rpc_server_commands_defs.h: Removedlow_mixinfield and its assignment from send_raw_tx response.daemon_handler.cpp: Removedm_low_mixinerror branch.verification_context.h: Removedm_low_mixinfromtx_verification_context.blockchain.cpp: Replaced legacy mixin-checking branch with a reject gate for non-FCMP++ transactions (Shekyl only supports FCMP++).cryptonote_tx_utils.h/.cpp: Removedrct::RCTConfigparameter fromconstruct_tx_with_tx_keyandconstruct_tx_and_get_tx_key. ReplacedgenRctSimplecall with FCMP++ proof generation stub. RemovedmixRingconstruction.cryptonote_format_utils.cpp: Removedis_rct_bulletproof/is_rct_clsagcalls, simplified BP+ weight calculations.cryptonote_boost_serialization.h: Removed serialization functions forrct::rangeSig,rct::Bulletproof,rct::mgSig,rct::clsag,rct::RCTConfig,rct::boroSig. SimplifiedrctSigBaseandrctSigPrunableserialization to only handle FCMP++.tx_verification_utils.h/.cpp: Removedmix_ringparameter fromver_rct_non_semantics_simple_cached. Removedexpand_tx_and_ver_rct_non_sem,calc_tx_mixring_hash, andis_canonical_bulletproof_layout.json_object.h/.cpp: Removed JSON serialization forrct::rangeSig,rct::Bulletproof,rct::boroSig,rct::mgSig,rct::clsag. Removed legacy prunable fields fromrctSigJSON output.wallet2.h: Removedrct_configfield fromtx_construction_dataserialization and the version-gatedRangeProofPaddedBulletproofdefaults in Boost serialization.wallet2.cpp: Fixedconstruct_tx_and_get_tx_keycall site that still passed{}where the removedrct_configparameter was.bulletproofs.h/.cc: Gutted non-plus Bulletproof PROVE/VERIFY functions — therct::Bulletproofstruct was already removed fromrctTypes.h, making these 1000+ lines of dead code.
- Legacy RCT types stripped from core. Removed
RCTTypeFull(1),RCTTypeSimple(2),RCTTypeBulletproof(3),RCTTypeBulletproof2(4),RCTTypeCLSAG(5), andRCTTypeBulletproofPlus(6) from the enum. OnlyRCTTypeNull(0) andRCTTypeFcmpPlusPlusPqc(7) remain. - Deleted structs:
mgSig,clsag,rangeSig,Bulletproof(non-plus),RangeProofTypeenum, andRCTConfig. - Removed
mixRingmember fromrctSigBaseandmixinparameter fromserialize_rctsig_prunable. - Removed from
rctSigPrunable:rangeSigs,bulletproofs(non-plus),MGs,CLSAGsvectors and their serialization blocks. - Removed functions:
CLSAG_Gen,proveRctCLSAGSimple,verRctCLSAGSimple,genRctSimple(both overloads),populateFromBlockchainSimple,getKeyFromBlockchain,is_rct_simple,is_rct_bulletproof,is_rct_borromean,is_rct_clsag,proveRangeBulletproof,verBulletproof,make_dummy_bulletproof,make_dummy_clsag. - Removed
HASH_KEY_CLSAG_ROUND,HASH_KEY_CLSAG_AGG_0,HASH_KEY_CLSAG_AGG_1, andHASH_KEY_TXHASH_AND_MIXRINGfromcryptonote_config.h. - Removed VARIANT_TAG entries for
mgSig,rangeSig,Bulletproof, andclsag. - Simplified
get_pre_mlsag_hashto only handleRCTTypeFcmpPlusPlusPqc. - Simplified
verRctSemanticsSimpleandverRctNonSemanticsSimpleto only accept FCMP++ transactions (no CLSAG/ring verification path).
🔄 Changed
- FCMP++ Phase 3: Per-input PQC authorization vector. Replaced
std::optional<pqc_authentication> pqc_authwithstd::vector<pqc_authentication> pqc_authsoncryptonote::transaction(onepqc_authenticationper input). Updated binary, Boost, and JSON serialization, transaction hash (cn_fast_hashof serializedpqc_auths), per-input PQC verification, and wallet/RPC signing paths.
✨ Added
-
FCMP++ (Full-Chain Membership Proofs): complete implementation across Phases 1–6. Shekyl replaces ring signatures (CLSAG) with FCMP++ from genesis. Every spend proves membership in the entire UTXO set via a Helios/Selene curve tree, giving every transaction full-chain anonymity instead of 16-decoy ring ambiguity. Combined with hybrid post-quantum spend authorization (Ed25519 + ML-DSA-65), this makes Shekyl the first cryptocurrency to offer full-UTXO-set anonymity with quantum-resistant ownership.
Key components delivered:
- Rust foundation (Phase 1):
shekyl-fcmpcrate wrapping upstreammonero-fcmp-plus-pluswith 4-scalar leaf type{O.x, I.x, C.x, H(pqc_pk)}. Hybrid X25519 + ML-KEM-768 KEM with HKDF-SHA-512. Bech32m segmented address encoding. Per-output PQC key derivation. 15 FFI exports. Security audit (zero vulnerabilities, zero unsafe in first-party code). Reproducible builds with pinned Cargo.lock. - Transaction format (Phase 3):
RCTTypeFcmpPlusPlusPqc = 7withreferenceBlock,curve_trees_tree_depth, andfcmp_pp_prooffields.curve_tree_rootcommitment in every block header. - Consensus verification (Phase 4): 7-step verification order in
check_tx_inputs— referenceBlock age, tree depth, key image y-normalization, FCMP++ proof via Rust FFI, PQC signature verification, BP+ range proofs. Mempool verification caching (fcmp_verification_hashintxpool_tx_meta_t). Staked output curve-tree leaves. - Curve tree database (Phase 2): Full
get_curve_tree_pathRPC implementation assembling real Merkle paths (leaf scalars + per-layer sibling hashes with position encoding). Selective pruning of intermediate tree layers between checkpoints, wired intoadd_blockaftersave_curve_tree_checkpoint. Old checkpoint garbage collection. - Wallet integration (Phase 5):
genRctFcmpPlusPlus()proof construction.get_curve_tree_pathRPC. Tree-path precomputation and incremental update in wallet refresh loop. PQC key rederivation from stored shared secret. Restore-from-seed PQC rederivation. - Infrastructure (Phase 6): Hardware device FCMP++ stubs. CI pipeline
for Rust workspace build, FCMP crate, determinism check, Bech32m tests.
output_pruning_metadata_tandm_output_metadataLMDB table for transaction pruning. LMDB curve tree schema (leaves, layers, meta, checkpoints). Checkpoint every 10,000 blocks for fast-sync resumption.
See
docs/FCMP_PLUS_PLUS.mdfor the full specification. - Rust foundation (Phase 1):
-
FCMP++ Phase 3: KEM ciphertext
tx_extraand coinbase self-encapsulation.tx_extra_pqc_kem_ciphertextwith tagTX_EXTRA_TAG_PQC_KEM_CIPHERTEXT(0x06): payloadblobis the concatenation of N ML-KEM-768 ciphertexts (1088 bytes each), one per output in order.- Coinbase: When the miner address has a PQC key and the hard-fork
version is at least
HF_VERSION_FCMP_PLUS_PLUS_PQC,construct_miner_txperforms KEM self-encapsulation to the miner’s own address per coinbase output (same tag and derivation semantics as normal transfers), then wipes the shared secret after use.
-
FCMP++ Phase 5e: Wallet precomputation of curve tree paths.
- Added
fcmp_precomputed_pathstruct towallet2.hcaching per-output tree path, root hash at precompute time, and precompute height. - Added
m_fcmp_precomputed_pathsruntime cache (not serialized) andm_fcmp_last_precompute_heightwatermark towallet2. precompute_fcmp_paths()fetches tree paths for all unspent outputs via theget_curve_tree_pathdaemon RPC endpoint.update_fcmp_paths_incremental(new_height)extends existing paths and adds newly discovered outputs, pruning paths for spent outputs.- Incremental path update is hooked into the wallet refresh loop, triggering after sync catches up if blocks were fetched.
- Progress callbacks (
on_fcmp_path_precompute_progress) fire during both initial and incremental precomputation.
- Added
-
FCMP++ Phase 5.5: Wallet sync and restore-from-seed PQC support.
transfer_details::m_combined_shared_secret(64 bytes) stores the hybrid KEM shared secret needed to rederive per-output PQC keys.rederive_pqc_keys_for_output(td)callsshekyl_fcmp_derive_pqc_keypairvia FFI to validate keypair derivation from stored shared secret.rederive_all_pqc_keys()iterates all transfers with stored shared secrets and rederives PQC keys, with progress callbackon_pqc_rederivation_progress.- Restore-from-seed triggers full PQC key rederivation on first refresh after sync completes.
🐛 Fixed
-
Curve tree pop_block over-trim:
pop_blockpreviously counted alltx.voutentries when computing how many leaves to trim, butadd_blockskips outputs that fail type checks (unknown target types), locked staked outputs, and outputs whose FFI leaf construction fails. The trim count now mirrors the same filtering logic used in the grow path, preventing tree desynchronization during reorgs. -
Curve tree pruning correctness:
prune_curve_tree_intermediate_layerswas deleting all intermediate layer entries instead of selectively pruning only chunks fully below the previous checkpoint boundary. Fixed to compute the chunk boundary from the previous checkpoint'sleaf_countand only remove sealed entries. Also added garbage collection of stale checkpoint records (only the two most recent are kept). -
LMDB output metadata: removed undefined behavior in cursor macros.
store_output_metadatanow usesmdb_putdirectly withm_write_txninstead of theCURSOR()macro which requiredm_cursorsto be in scope.get_output_metadataandprune_tx_datanow usem_txn(fromTXN_PREFIX_RDONLY) instead oftxn_ptr(fromTXN_PREFIX).- Removed unused
m_txc_output_metadatacursor field andm_cur_output_metadatamacro fromdb_lmdb.h.
-
Wallet FCMP++ path precomputation: fixed undefined behavior.
- Replaced
reinterpret_cast<std::string&>onstd::vector<uint8_t>with a proper intermediatestd::stringcopy in bothprecompute_fcmp_pathsandupdate_fcmp_paths_incremental.
- Replaced
-
FCMP++ Phase 6c: CI pipeline updates.
- Added x86_64 architecture verification step to the
rust-audit-and-testCI job in.github/workflows/build.yml. - Added explicit
cargo build --locked -p shekyl-fcmpstep to verify the FCMP++ crate builds as part of the Rust workspace. - Added dedicated Bech32m address encoding test step that runs
shekyl-crypto-pqaddress tests with visible CI output. - The monero-oxide git dependency is cached via
~/.cargo/gitin the existing Cargo cache key (rust-${{ hashFiles('rust/Cargo.lock') }}). - Determinism check (build twice, diff
libshekyl_ffi.ahashes) andcargo auditremain in place.
- Added x86_64 architecture verification step to the
-
FCMP++ Phase 6f: Transaction pruning mode (skeleton).
- Added
output_pruning_metadata_tpacked struct toblockchain_db.hstoring per-output scan data (pubkey, commitment, unlock_time, height, pruned flag) for wallet scanning after transaction pruning. - Added abstract interface in
BlockchainDB:store_output_metadata(),get_output_metadata(),is_output_pruned(),prune_tx_data(). - Added
m_output_metadataLMDB table (keyed byglobal_output_index) indb_lmdb.handdb_lmdb.cppwith cursor, rflag, and DBI member. - LMDB implementation:
store_output_metadataandget_output_metadataare fully wired;is_output_pruneddelegates toget_output_metadata;prune_tx_datavalidates depth againstCRYPTONOTE_DEFAULT_TX_SPENDABLE_AGEand reads/writes alast_pruned_tx_data_heightwatermark in the properties table to skip already-processed blocks on subsequent runs. The block-iteration pruning loop is documented as a TODO skeleton. --prune-blockchainCLI flag now also triggersprune_tx_data()incryptonote_core.cpp, running output-metadata pruning alongside Monero's existing stripe-based pruning.- Test DB (
testdb.h) updated with no-op stubs for all four new methods.
- Added
-
FCMP++ Phase 4b: Mempool verification caching.
- Added
fcmp_verification_hash(32-bytecrypto::hash) andfcmp_verified(1-bit flag) totxpool_tx_meta_tinsrc/blockchain_db/blockchain_db.h, carved from the existing 76-byte padding (now 44 bytes). Struct stays 192 bytes. - New
Blockchain::compute_fcmp_verification_hash()computes a deterministic cache key fromhash(proof || referenceBlock || key_images). tx_memory_pool::add_txstores the cache hash on successful FCMP++ verification.tx_memory_pool::is_transaction_ready_to_gochecks the cached hash viais_fcmp_verification_cached()and seedsm_input_cacheto skip re-runningshekyl_fcmp_verify()for previously-verified mempool transactions.- Added
static_assertguards at thememcmpsite ontxpool_tx_meta_t(tx_pool.cpp line 1656) enforcing trivially-copyable layout and 192-byte struct size. - All padding and new fields are zero-initialized at every meta construction site.
- Added
-
FCMP++ Phase 4e: Staking consensus rules for FCMP++.
collect_outputsinblockchain_db.cpp::add_blocknow handlestxout_to_staked_keyoutputs using the same 4-scalar leaf format{O.x, I.x, C.x, H(pqc_pk)}.- Deferred insertion: staked outputs only enter the curve tree when
block_height >= effective_lock_until. Outputs still within their lock period are stored in thepending_staked_leavesDB table and inserted into the curve tree when they mature (see deferred staked leaf insertion entry below). check_stake_claim_inputvalidates claims against the staked output'seffective_lock_until(creation_height + tier_lock_blocks) and enforcesto_height <= min(current_height, effective_lock_until).
-
FCMP++ Phase 5: Wallet transaction construction skeleton.
- Added
rct::genRctFcmpPlusPlus()insrc/fcmp/rctSigs.cpp— builds an FCMP++rctSigwithRCTTypeFcmpPlusPlusPqc, Bulletproofs+ range proofs, balanced pseudo-outputs, and invokesshekyl_fcmp_prove()via FFI to generate the membership proof. - Declared the new function in
src/fcmp/rctSigs.h. - Added
COMMAND_RPC_GET_CURVE_TREE_PATHRPC command insrc/rpc/core_rpc_server_commands_defs.h— accepts output indices and returns Merkle paths from the curve tree (stub handler for now). - Wired
get_curve_tree_pathJSON-RPC endpoint insrc/rpc/core_rpc_server.handsrc/rpc/core_rpc_server.cpp. - Added TODO scaffolding in
src/wallet/wallet2.cppat the decoy selection (get_outs), transaction construction (construct_tx_and_get_tx_key), and fee estimation (estimate_tx_weight) sites, documenting how FCMP++ replaces ring signatures in the wallet transfer flow.
- Added
-
FCMP++ Phase 6a: Hardware device stubs.
- Added
fcmp_prepare,fcmp_proof_start, andfcmp_proof_add_inputvirtual methods tohw::device(base class) with defaultreturn falseimplementations for unsupported devices. - Software device (
device_default) returnstrue(scaffolding for Rust FFI delegation). - Ledger device (
device_ledger) logs an informative error and returnsfalse, guiding users to software wallets until Ledger firmware gains FCMP++ support. - Trezor inherits the base-class defaults (unsupported) without code changes.
- Updated
RELEASE_CHECKLIST.mdto document hardware wallet readiness status.
- Added
-
FCMP++ Phase 4a: Verification in
check_tx_inputs.- Added
RCTTypeFcmpPlusPlusPqcverification path inBlockchain::check_tx_inputs(src/cryptonote_core/blockchain.cpp). referenceBlockage validation: confirmed within[tip - MAX_AGE, tip - MIN_AGE]using DB block lookup.curve_trees_tree_depthvalidated against the current tree state.- Key offsets verified empty for all FCMP++ inputs.
- Key image y-normalization enforced (sign bit of byte 31 cleared).
- Input count bounded by
FCMP_MAX_INPUTS_PER_TX. shekyl_fcmp_verify()FFI call wired up with key images, pseudo outputs, and proof blob.- Per-input
pqc_authsverification left as documented TODO pending the per-input auth field migration.
- Added
-
FCMP++ Phase 4a-pre: PQC auth binding specification.
- New
docs/FCMP_PLUS_PLUS.mdformally documents the dual-layer binding model, per-input signed payload layout, and 7-step consensus verification order forRCTTypeFcmpPlusPlusPqctransactions.
- New
-
FCMP++ Phase 3.5: Curve tree root in block header (consensus-critical).
- Added
curve_tree_root(crypto::hash) field toblock_headerinsrc/cryptonote_basic/cryptonote_basic.h, initialized tonull_hash. - Field is always serialized (genesis-native, no version gating) in both
the binary archive (
BEGIN_SERIALIZE) and Boost serialization. - Block template creation (
Blockchain::create_block_template) snapshots the current DB curve tree root into the header. - Block validation (
Blockchain::handle_block_to_main_chain) verifiescurve_tree_rootmatches the locally-computed tree root afteradd_blockgrows the tree; rejects the block on mismatch. - RPC
block_header_responsenow includescurve_tree_roothex string. - Test generator (
chaingen.cpp) setscurve_tree_roottonull_hashinconstruct_blockandconstruct_block_manually.
- Added
-
FCMP++ Phase 3: Transaction format for FCMP++ PQC.
- Added
RCTTypeFcmpPlusPlusPqc = 7to the RCT type enum insrc/fcmp/rctTypes.h— Shekyl's only non-coinbase transaction type. - Added
referenceBlock(block hash anchoring the curve tree snapshot) torctSigBase, serialized only for the new type. - Added
curve_trees_tree_depthandfcmp_pp_proof(opaque FCMP++ proof blob) torctSigPrunable, replacing CLSAG ring signatures for the new type. - Added
TX_EXTRA_TAG_PQC_KEM_CIPHERTEXT(0x06) totx_extra.hfor per-output ML-KEM-768 ciphertexts. - Added
key_image_y_normalize()tocrypto.h/crypto.cpp— clears the sign bit of a key image's y-coordinate as required by FCMP++. - Added
is_rct_fcmp_pp_pqc()helper torctTypes.h/rctTypes.cpp. - Updated serialization helpers (
serialize_rctsig_base,serialize_rctsig_prunable) and type classifier functions (is_rct_simple,is_rct_bulletproof_plus) to handle the new type.
- Added
-
FCMP++ Phase 2e: Curve tree checkpoint strategy.
- New
BlockchainDBvirtual methods:save_curve_tree_checkpoint,get_curve_tree_checkpoint,get_latest_curve_tree_checkpoint_height,prune_curve_tree_intermediate_layers. - LMDB implementation with
curve_tree_checkpointstable (MDB_INTEGERKEY), storing root[32] + depth[1] + leaf_count[8] per checkpoint. - Automatic checkpoint every
FCMP_CURVE_TREE_CHECKPOINT_INTERVAL(10 000) blocks duringadd_block, enabling fast-sync resumption. - Configurable interval via
cryptonote_config.hconstant.
- New
-
FCMP++ Phase 2f: Curve tree pruning strategy.
prune_curve_tree_intermediate_layersremoves recomputable internal hash layers between checkpoints, preserving leaves and the root layer to reduce storage overhead.
-
FCMP++ Phase 1: Rust foundation crates.
- New
rust/shekyl-fcmp/crate wrapping upstreammonero-fcmp-plus-plus(fromShekyl-Foundation/monero-oxidefork,fcmp++branch) with 4-scalar curve tree leaf type{O.x, I.x, C.x, H(pqc_pk)}. - Implemented
HybridX25519MlKem(X25519 + ML-KEM-768 FIPS 203) inshekyl-crypto-pq/src/kem.rswith HKDF-SHA-512 shared-secret combination and master-seed key derivation. - Implemented Bech32m segmented address encoding
(
shekyl1<classical>/skpq1<pqc_a>/skpq21<pqc_b>) inshekyl-crypto-pq/src/address.rs, keeping each segment within Bech32m's proven checksum range. - Implemented per-output PQC keypair derivation (HKDF-Expand → ML-DSA-65
deterministic keygen) in
shekyl-crypto-pq/src/derivation.rs. - Added 15 new FFI exports to
shekyl-ffifor FCMP++ proofs, KEM operations, address encoding, and seed derivation. - Added FCMP++ consensus constants to
cryptonote_config.h:HF_VERSION_FCMP_PLUS_PLUS_PQC,FCMP_REFERENCE_BLOCK_MAX_AGE(100),FCMP_REFERENCE_BLOCK_MIN_AGE(2),FCMP_MAX_INPUTS_PER_TX(8). - Updated
BuildRust.cmakewith--lockedflag for reproducible builds.
- New
-
FCMP++ Phase 1a.1: Security review of forked monero-oxide crates.
cargo audit: 226 crate dependencies scanned, zero vulnerabilities found.unsafeblock audit: zerounsafein first-party monero-oxide workspace code (helioselene, ec-divisors, generalized-bulletproofs, fcmps, monero-oxide). Only 4unsafeblocks exist in helioselene benchmarks (_rdtsc()for cycle counting, not in library code).dalek-ff-group(crates.io dependency) also has zerounsafeblocks.- Veridise audit status: FCMPs circuit audited by Veridise (June 2025);
Generalized Bulletproofs security proofs by Cypher Stack; Divisor proofs
reviewed by both Veridise and Cypher Stack. Pinned commit
92af05e0is post-audit. Helioselene and ec-divisors are not yet independently audited. Multi-phase integration audit (seraphis-migration/monero#294) is in planning.
-
FCMP++ Phase 1a.2: Rust reproducible builds.
Cargo.lockpins all git dependencies to exact commit hash92af05e0.- Double-build determinism verified:
libshekyl_ffi.ahash identical across consecutive builds on x86_64. - Added CI job
rust-audit-and-testto.github/workflows/build.ymlwith cargo audit, workspace tests, and determinism check (build twice, diff). - Documented x86_64-only build requirement and Guix integration status in
docs/COMPILING_DEBUGGING_TESTING.md.
🔄 Changed
- P2P reorg functional test uses deadline-based polling. Replaced three
fixed-sleep polling sites in
test_p2p_reorg()(time.sleep(10)x2,loops = 100counter) with 240 s deadline + 0.25 s interval polling, matching the pattern already used intest_p2p_tx_propagation(). Adapted from upstream Monero #9795.
✨ Added
- Extra compiler warnings and hardening flags. Added
-Wredundant-decls,-Wdate-time,-Wimplicit-fallthrough,-Wunreachable-code(common);-Woverloaded-virtual,-Wsuggest-override(C++ only);-Wgnu,-Wshadow-field,-Wthread-safety,-Wloop-analysis,-Wconditional-uninitialized,-Wdocumentation,-Wself-assign(Clang);-Wduplicated-branches(GCC). Added security protections:-fno-extended-identifiers,-fstack-reuse=none, and ARM64 branch protection (-mbranch-protection=btion macOS,standardelsewhere). Adapted from upstream Monero #9858. - Linker dead-code stripping. Added
-ffunction-sections -fdata-sectionsto compile flags and-Wl,--gc-sections(Linux) /-Wl,-dead_strip(macOS) to linker flags, enabling the linker to strip unreferenced functions and data. Inspired by upstream Monero #9898 author's findings (~14 MiB reduction in Docker images).
📚 Documentation
- Upstream Monero PR triage. Replaced the stale "To be done (and merged)"
section in
COMPILING_DEBUGGING_TESTING.mdwith a structured triage table covering applied PRs (#6937, #9762, #9795, #9858, #9898) and tracked-for- future-work PRs (#10157, #10084, #9801) with STRUCTURAL_TODO.md cross-refs. - FCMP++ documentation rework (Phase 0.5a). Reworked all core documentation to reflect FCMP++ as the membership proof system from genesis. Replaced CLSAG and ring signature references with FCMP++ full-chain membership proof language. Updated PQC spec for per-input pqc_auths, per-output KEM derivation, Bech32m addresses, and curve tower architecture. Retired V4 lattice ring signature roadmap. Updated V3_ROLLOUT.md size estimates for ~23 KB typical transactions. Added FCMP++ items to RELEASE_CHECKLIST.md.
🐛 Fixed
-
Re-enabled
gen_block_rewardcore test with Shekyl economics. Rewrotecheck_block_rewards()inblock_reward.cppto verify miner outputs against Shekyl's four-component economics formula (release multiplier + emission split + fee burn) instead of legacy Monero fixed expectations. Updatedconstruct_miner_tx_by_weightto pass explicit economics parameters. Fixedconstruct_blockandconstruct_block_manuallyinchaingen.cppto passcirculating_supply=already_generated_coinstoconstruct_miner_tx, preventing parameter mismatch between test generator and validator. 80 core_tests now pass (was 79). -
MSVC C4334: 23
1 << nsites widened to1ULL << nin consensus code. Fixed potential undefined behavior (signed 32-bit overflow if shift amount ever reaches 32) incryptonote_format_utils.cpp(3),bulletproofs.cc(6),bulletproofs_plus.cc(6),rctTypes.cpp(5),rctSigs.cpp(2), andmultiexp.cc(2). -
MSVC C4333 right-shift warning in UTF-8 helpers. Changed
wint_t cptouint32_t cpinsrc/common/util.cppget_string_prefix_by_width(), and added an explicitstatic_cast<uint32_t>on the transform result insrc/common/utf8.hutf8canonical(). On MSVC,wint_tis 16-bitunsigned short, socp >> 18shifted by more than the type's width. -
Remaining HF17 references corrected to HF1. Fixed stale Monero-era
HF17/HF_VERSION_SHEKYL_NG = 17references inPOST_QUANTUM_CRYPTOGRAPHY.md(scheme registry, rollout notes, V4 roadmap),PQC_MULTISIG.md(V3 heading, V4 scheme table, activation target),V3_ROLLOUT.md(title, consensus gate, node checklist), andSTAKER_REWARD_DISBURSEMENT.md. Also correctedHF18references toHF2in multisig V4 rollout tables. The source code constantHF_VERSION_SHEKYL_NGwas already correctly defined as1incryptonote_config.h; only documentation was affected. -
CMake Boost detection on CMake 3.30+: The built-in
FindBoost.cmakemodule was removed in CMake 3.30. Restructured Boost detection to try CONFIG mode first (findingBoostConfig.cmakeinstalled by b2), falling back to MODULE on older CMake. Fixescontrib/dependsbuilds on Ubuntu 24.04 runners with CMake ≥ 3.30.
🗑️ Removed
- Classical multisig wallet RPC commands. Removed all 9 Monero-inherited
multisig RPC endpoints (
is_multisig,prepare_multisig,make_multisig,export_multisig_info,import_multisig_info,finalize_multisig,exchange_multisig_keys,sign_multisig,submit_multisig) from the wallet RPC server. Removedmultisig_txsetfields from transfer and sweep response structs. Removed theCHECK_MULTISIG_ENABLEDmacro andmultisig/multisig.hdependency. Classical secret-splitting multisig is replaced by PQC-only authorization (scheme_id = 2); seedocs/PQC_MULTISIG.md. - Classical multisig simplewallet CLI commands. Removed all multisig and
MMS (Multisig Messaging System) commands from
simplewallet:prepare_multisig,make_multisig,exchange_multisig_keys,export_multisig_info,import_multisig_info,sign_multisig,submit_multisig,export_raw_multisig_tx, and allmmssubcommands. Removed--generate-from-multisig-keysand--restore-multisig-walletCLI flags. Removedenable-multisig-experimentalwallet setting. Removedwallet/message_store.hdependency. Thetransfer_main/called_by_mmsindirection was collapsed into a singletransfermethod. - Classical multisig test and device_trezor remnants. Removed stale
multisig references from test infrastructure:
m_multisig*wallet resets inwallet_tools.cpp,multisig_sigs.clear()in Trezor tests,multisig_txsetassertion incold_signing.py, and deletedtests/functional_tests/multisig.py. Removedmultisigfrom the functional test default list. Cleaned up device_trezor protocol: removedtranslate_klrki,MoneroMultisigKLRkialias,m_multisigmember, and multisig cout decryption inSigner::step_final_ack. Removedmms_error,no_connection_to_bitmessage, andbitmessage_api_errorerror classes fromwallet_errors.h. - Classical multisig wallet API layer. Removed all classical multisig
code from the public wallet API:
MultisigStatestruct, virtual multisig declarations (multisig,getMultisigInfo,makeMultisig,exchangeMultisigKeys,exportMultisigImages,importMultisigImages,hasMultisigPartialKeyImages,restoreMultisigTransaction,publicMultisigSignerKey,signMultisigParticipant,multisigSignData,signMultisigTx). Removed multisig helper functions and multisig threshold check from PendingTransaction commit path. Removed multisig guard from the background-sync validation macro. - Classical multisig wallet core (
wallet2.cpp). Removed all classical multisig code from the wallet core:#include "multisig/..."headers,MULTISIG_UNSIGNED_TX_PREFIX/MULTISIG_EXPORT_FILE_MAGIC/MULTISIG_SIGNATURE_MAGICconstants,m_multisig/m_multisig_threshold/m_multisig_rounds_passed/m_enable_multisig/m_message_store/m_mms_filemember initializations,num_priv_multisig_keys_post_setup,get_multisig_seed, multisig restore path ingenerate(),make_multisig,exchange_multisig_keys,get_multisig_first_kex_msg,multisig(),has_multisig_partial_key_images,frozen(multisig_tx_set), allsave/parse/load/sign_multisig_txoverloads, the multisig transaction builder path intransfer_selected_rct,export_multisig,import_multisig,update_multisig_rescan_info,get_multisig_signer_public_key,get_multisig_signing_public_key,get_multisig_k,get_multisig_kLRki,get_multisig_composite_kLRki,get_multisig_composite_key_image,get_multisig_wallet_state,sign_multisig_participant, JSON serialization/deserialization of multisig fields, MMS file handling, and all scatteredm_multisigguard branches. - Classical multisig
m_key_image_partialremnants. Removed them_key_image_partialbitfield fromexported_transfer_detailsand all code references inwallet2.cppandsimplewallet.cpp. Since classical multisig was removed, partial key images can never exist; all guard conditions (!known || partial,known && !partial, standalone partial checks) were simplified to reference onlym_key_image_known. Removed the deadold_mms_filecleanup block fromwallet2::store_to.
✨ Added
- Daemon RPC migrated to Rust/Axum (Phase 1). The daemon HTTP RPC transport
is now served by the
shekyl-daemon-rpcRust crate using Axum, replacingepee::http_server_impl_base. All 90 endpoints (33 JSON REST, 9 binary, 48 JSON-RPC 2.0) are routed through Axum with PQC-ready 10 MiB body limits, CORS, and restricted-mode enforcement. The C++core_rpc_serverhandler logic is unchanged and accessed via acore_rpc_ffiC ABI facade. Enabled by default;--no-rust-rpcfalls back to the legacy epee HTTP server. JSON REST endpoints accept both GET and POST (matching epee). Binary endpoints return 400 on parse failure (matching epee's MAP_URI_AUTO_BIN2). Validated on live testnet: 23/25 pass, 2 expected diffs (rpc_connections_count), 2 binary skips (empty-POST → 400 on both). Validation harness attests/rpc_comparison/compare_rpc.sh; test data inshekyl-dev/data/rpc_comparison/. - PQC multisig core (scheme_id=2). Implemented M-of-N hybrid Ed25519 +
ML-DSA-65 multisig in Rust. Includes
MultisigKeyContainer,MultisigSigContainer,multisig_group_id, and a 10-check adversarial verification pipeline. Maximum 7 participants (consensus constant). Domain separator:shekyl-multisig-group-v1. - PQC multisig FFI bridge. Extended
shekyl_pqc_verifyto acceptscheme_idand dispatch between single-signer (1) and multisig (2) paths. Addedshekyl_pqc_verify_debugfor diagnostic error codes andshekyl_pqc_multisig_group_idfor group identity computation. - Scheme downgrade protection. New
tx_extra_pqc_ownershiptag (0x05) records the expected PQC scheme and group ID for each output, preventing attackers from spending multisig-protected outputs with single-signer transactions. - Wallet multisig coordination. New wallet2 methods for PQC multisig:
create_pqc_multisig_group,export_multisig_signing_request,sign_multisig_partial,import_multisig_signatures. File-based JSON signing protocol. Wallet serialization version bumped to 32. - Cargo-fuzz harnesses. 4 fuzz targets for multisig deserialization and
verification (
fuzz_multisig_key_blob,fuzz_multisig_sig_blob,fuzz_multisig_verify,fuzz_group_id), each validated at 10M iterations with zero panics. - PQC multisig subset-signing test. Added
valid_subset_signing_3_of_5test toshekyl-crypto-pqverifying that any valid 3-of-5 signer subset produces a valid multisig through the full 10-check verification pipeline. - PQC multisig test vectors. Published
docs/PQC_TEST_VECTOR_002_MULTISIG.jsonwith canonical encoding sizes, wire-format sizes, verification pipeline checks, the 10-check pipeline, size regression data, and adversarial test cases forscheme_id = 2. - MSVC wallet-core build path:
BuildRust.cmakenow selects thex86_64-pc-windows-msvcRust target when CMake is driven by MSVC, enabling the Tauri GUI wallet to link against shekyl-core on Windows. The existing MinGW cross-compilation path for headless binaries is unchanged. - CI: Windows MSVC wallet-core job (
build-windows-msvc): New CI lane builds the wallet-core static libraries with Visual Studio / MSVC via vcpkg, validating the MSVC portability patches on every push. - Unified Gitian release pipeline. The
gitianworkflow is now the sole release pipeline, replacing the separaterelease-taggedworkflow. Gitian builds produce reproducible binaries; a newpackage-and-publishjob creates.deb/.rpmpackages, a Windows NSIS installer, source archive, andSHA256SUMS, then publishes the GitHub Release. Eliminates duplicate cross-compilation and host-toolchain issues. - Source archive in GitHub Releases. The packaging job produces
shekyl-vX.Y.Z-source.tar.gzcontaining the full source tree with all submodules, attached to each release alongside the binaries.
🔄 Changed
shekyl_pqc_verifyFFI signature change. Now requiresscheme_idas first parameter for scheme dispatch.depends.ymldemoted to PR-only. The cross-compilation CI workflow now runs only on pull requests (and manual dispatch), not on every push. Saves significant CI minutes; Gitian catches cross-platform issues at release time.release-tagged.ymldisabled. The Gitian pipeline now handles all release artifacts. The old workflow is preserved as.disabledfor one release cycle.- Gitian reproducible builds: migrated from Ubuntu 18.04 (Bionic) to 22.04
(Jammy). All five build descriptors (
gitian-linux.yml,gitian-win.yml,gitian-osx.yml,gitian-android.yml,gitian-freebsd.yml),gitian-build.py, anddockrun.shnow target Jammy. Drops GCC 7 and Python 2 dependencies in favour of the distro-default GCC 11 and Python 3. Upgrades FreeBSD cross-compiler from Clang 8 to Clang 14. Removes Bionic-specific workarounds (i686 asm symlink hack, glibcmath-finite.hhack). Addslinux-libc-dev:i386for native i686 headers. C++17 is now fully supported by the Gitian toolchain.
🐛 Fixed
- Comprehensive compiler warning cleanup across all CI platforms. Eliminated
~30 unique warnings inherited from Monero across Linux, macOS, Windows, and
Arch Linux CI builds:
- Removed dead code:
add_public_key(format_utils),keys_intersect(wallet2), unusedaddressoftemplate specialization (crypto test), unusedmax_block_heightvariable (protocol_handler). - Fixed
oaes_lib.c: replaced deprecatedftime()withgettimeofday(), corrected transposedcallocargument order (5 call sites). - Fixed
rx-slow-hash.c: added(void)to K&R-style function definitions. - Suppressed GCC false positive
-Wstringop-overflowintree-hash.c. - Replaced deprecated
strand::wrap()withboost::asio::bind_executor()inlevin_notify.cpp. - Suppressed GCC
-Wuninitializedfor safe circular-reference constructors incryptonote_core.cppandlong_term_block_weight.cpp. - Added default member initializers to
BulletproofPlus(rctTypes.h),transfer_detailsandpayment_details(wallet2.h) to silence-Wmaybe-uninitialized. - Fixed Windows: removed unused variables in
windows_service.cpp, eliminated-Wcast-function-typeinutil.cppviavoid*intermediate cast, fixed-Wtype-limitsinutf8.hby usinguint32_tinstead ofwint_tfor code points. - Suppressed intentional uninitialized read in
memwipe.cpptest. - Set
MACOSX_DEPLOYMENT_TARGETfor native Darwin Cargo builds inBuildRust.cmaketo eliminate 672 linker warnings fromringcrate.
- Removed dead code:
- CI link errors: separated
shekyl-daemon-rpcfromshekyl-ffi. The daemon RPC Axum crate was bundled intolibshekyl_ffi.a, causingundefined reference to core_rpc_ffi_*on non-daemon targets (gen-ssl-cert, wallet-crypto-bench, etc.) across all 5 CI platforms. Moved FFI exports (shekyl_daemon_rpc_start,shekyl_daemon_rpc_stop) into a newffi_exports.rswithin the daemon-rpc crate, which now produces its ownlibshekyl_daemon_rpc.astaticlib. Only the daemon target links both libraries.BuildRust.cmakeupdated with a second cargo build step andSHEKYL_DAEMON_RPC_LINK_LIBS. - Wallet:
--daemon-porthelp text referenced Monero port 18081. Updated to Shekyl's default RPC port 11029. - Wallet:
account_public_addressequality after PQC. Destination and change-address checks usedmemcmpon the whole struct;m_pqc_public_keyis astd::vector, so equality was wrong when keys matched but allocations differed. All such sites now useoperator==/!=. Added astatic_assertthat the type is not trivially copyable to discourage rawmemcmpregressions. - Wallet / Ledger: constant-time comparison for 32-byte secrets.
wallet2::is_deterministicand Ledger HMAC secret lookup now usecrypto_verify_32instead ofmemcmp. - MSVC: add
<io.h>and POSIX guards inutil.cpp. Added<io.h>for_open_osfhandle/_close, expanded MinGW conditionals to cover MSVC forsetenv→putenv,mode_t/umask, andclosefrom→no-op. - MSVC: replace
__threadwiththread_localinperf_timer.cppandthreadpool.cpp. GCC's__threadis not supported by MSVC. - MSVC: rename
xorparameter inslow-hash.ctoxor_pad. MSVC treatsxoras a reserved keyword in C mode. Both the x86/SSE and ARM/NEON variants ofaes_pseudo_round_xor()were affected. - MSVC: fix iterator-to-pointer cast in
http_auth.cpp. MSVCboost::as_literal()iterator is a class, not a raw pointer. Used&*data.begin()to obtain the address. - MSVC: guard
unbound.hinclude and usage inutil.cpp. The include andunbound_built_with_threads()function/call were not wrapped inHAVE_DNS_UNBOUND, causing a missing-header error. - MSVC: guard
unistd.hin easylogging++. The third-party logging library unconditionally included<unistd.h>which does not exist on MSVC. - MSVC: add
<io.h>include for_isattyinmlog.cpp. The WIN32 code path uses_isatty/_filenowhich require<io.h>on MSVC. - MSVC: fix
boost::iterator_rangeconversion inhttp_auth.cpp. Boost 1.90as_literal()returns an iterator type that does not implicitly convert toiterator_range<const char*>on MSVC. Changed toautodeduction. - MSVC: add
<cwctype>include forstd::towlowerinlanguage_base.h. MSVC does not transitively include wide-character utilities through other Boost headers. - MSVC: fix rvalue binding in portable_storage serialization. Changed
array_entry_t::insert_first_valandinsert_next_valuefrom strict rvalue-reference parameters (t_entry_type&&) to pass-by-value, allowing lvalue forwarding fromportable_storage::insert_first_value/insert_next_valueto work correctly under MSVC template deduction. - MSVC: force-include
<iso646.h>for C++ alternative tokens. The codebase usesnot,and,orextensively (hundreds of sites). MSVC does not recognise these as keywords by default. Added/FIiso646.hto the MSVC compile definitions so they are defined in every translation unit. - MSVC: enable conformant preprocessor (
/Zc:preprocessor). MSVC's traditional preprocessor breaks nested__VA_ARGS__forwarding in theTHROW_ON_RPC_RESPONSE_ERRORmacro chain, causingthrow_wallet_extemplate deduction failures. Added/Zc:preprocessorto MSVC compile flags and removed the obsolete Boost.Preprocessor-basedthrow_wallet_exfallback in favour of the standard variadic template version. - Gitian: enable
universerepository and remove apt proxy in Docker base image. Theubuntu:jammyDocker image only enablesmain restrictedby default;gitian-build.pynow patches the base image aftermake-base-vmto adduniverseand remove theapt-cacher-ngproxy configuration (/etc/apt/apt.conf.d/50cacher). The proxy routes all apt traffic through172.17.0.1:3142which is unreliable on ephemeral CI runners, causing persistent 503 failures during package installation. Usesdocker build(not run+commit) to preserve the image's CMD/USER metadata. - Gitian Linux: fix i386-dependent package installation. The i386
architecture is now enabled in the Docker base image (via
gitian-build.py'sdocker buildstep) along with passwordlesssudofor theubuntuuser, allowinglinux-libc-dev:i386,gcc-multilib, andg++-multilibto be installed normally via the descriptor'spackages:section. - Gitian macOS: add
libtinfo5andpython-is-python3, removepythonfromFAKETIME_PROGS. The pre-built Clang 9 cross-compiler requireslibtinfo.so.5. Thepythonfaketime wrapper broke CMake'sFindPythonInterpversion detection in thenative_libtapibuild (emptyPYTHON_VERSION_STRING); removingpythonfrom the faketime wrappers fixes this while preserving timestamp reproducibility forar,ranlib,date,dmg, andgenisoimage. - Gitian Android: add
python-is-python3. Android NDK r17b scripts use#!/usr/bin/env pythonwhich does not exist on Jammy without this package. - Gitian macOS: fix Rust
ringcrate cross-compilation.BuildRust.cmakeincorrectly overrode the macOS cross-compiler with the Linux systemclangwhen cross-compiling for Darwin, causing theringcrate to include Linux-onlycet.h. Now only uses system clang on native macOS builds. - Gitian Windows: drop i686 (32-bit) target. The i686-pc-windows-gnu Rust
target has an unresolved
GetHostNameW@8symbol against MinGW'sws2_32. Since the release workflow only targets x86_64, the 32-bit Gitian build is removed. - macOS cross-build: exclude
-fcf-protection=full. Intel CET is x86 Linux only; the flag defines__CET__which triggers#include <cet.h>in theringcrate's assembly, butcet.hdoes not exist in the macOS SDK. Now excluded for all Apple targets. - macOS aarch64 cross-build: set
MACOSX_DEPLOYMENT_TARGET=10.16. Clang 9 (depends cross-compiler) does not recognise macOS version 11.0+. Apple aliases 10.16 == 11.0; thecc-rscrate respects this env var, fixing theringbuild foraarch64-apple-darwin. - Gitian Docker base image: install
sudobefore creating sudoers entry. The/etc/sudoers.d/directory does not exist in the minimal Ubuntu image until thesudopackage is installed.
🔄 Changed
- Replace all
BOOST_FOREACH/BOOST_REVERSE_FOREACHwith range-for loops. 31+ call sites across test and utility code replaced with standard C++11 range-based for. Adds/DNOMINMAXto MSVC definitions to prevent Windowsmin/maxmacro collisions. - Replace hardcoded
-fPICwithPOSITION_INDEPENDENT_CODE. The CMake property works across all compilers (GCC, Clang, MSVC). Applied toliblmdbandeasylogging++CMakeLists. - Guard/remove unguarded
#include <unistd.h>. POSIX header guarded behind#ifndef _WIN32inblockchain_import.cpp; unused include removed fromcrypto.cpp. - Replace C++20 designated initializers with C++17-compatible member
assignment. Rewrote 10 call sites in
cryptonote_core.cpp,blockchain.cpp,levin_notify.cpp,multisig_tx_builder_ringct.cpp, andwallet2.cpp. GCC/Clang accepted these as extensions; MSVC rejects them. - Replace all
__threadwiththread_local. Coverseasylogging++.cc,perf_timer.cpp, andthreadpool.cpp. The__threadqualifier is GCC/Clang-specific;thread_local(C++11) is portable across GCC, Clang, and MSVC. - Centralize
ssize_ttypedef insrc/common/compat.h. Replaces duplicate#if defined(_MSC_VER)guards inutil.handdownload.hwith a single include.
🗑️ Removed
- Classical multisig code removed from wallet2.h. Removed all classical
Monero-style multisig types (
multisig_info,multisig_sig,multisig_kLR_bundle,multisig_tx_set), public/private multisig API methods, multisig private members, MMS (message store) integration, and associated Boost serialization functions. Thesrc/multisig/directory andsrc/wallet/message_store.hare deleted;wallet2.hno longer depends on those headers. All multisig uses PQC-only authorization (scheme_id = 2) via thepqc_authlayer. - Gitian Android build. Removed from the Gitian matrix since there is no Android wallet. The Android NDK r17b is also incompatible with Ubuntu Jammy.
- Gitian Linux: drop i686-linux-gnu (32-bit x86) target. Eliminates the
need for
linux-libc-dev:i386,gcc-multilib,g++-multilib,sudo, and thedpkg --add-architecture i386workaround. Simplifies the Docker base image patching to only enable theuniverserepository.
📚 Documentation
docs/RELEASING.md: document all release artifacts. Updated the artifact table to list all 13 files produced per release (was 6), including cross-platform tarballs, aarch64.deb/.rpm, and source archive. Updated "Future Platforms" to reflect that macOS tarballs are now shipping and.dmg/AppImage remain planned.
[3.0.3-RC1] - 2026-03-31
Known Limitations
- Multisig not yet implemented. Multisig wallets are restricted to v2
transactions (no PQC authentication). PQC-enabled multisig is planned for
a future release. See
docs/PQC_MULTISIG.mdfor the design.
✨ Added
-
Rust wallet RPC server (
shekyl-wallet-rpc): New Rust crate that replaces the C++wallet_rpc_serverwith an axum-based JSON-RPC server. Calls the existing C++wallet2library through a new C FFI facade (wallet2_ffi.cpp/.h). Supports all 98 RPC methods with full parity. Can run as a standalone binary (shekyl-wallet-rpc) or be embedded as a library in the Tauri GUI wallet. Seedocs/WALLET_RPC_RUST.md. -
C++ wallet2 FFI facade (
wallet2_ffi.cpp/.h): Opaque-handle C API overwallet2with JSON serialization at the boundary. Includes a genericwallet2_ffi_json_rpc()dispatcher that routes all RPC methods to the underlying wallet2 implementation. Covers lifecycle, queries, transfers, sweeps, proofs, accounts, address book, import/export, multisig, staking, mining, background sync, and daemon management. -
GUI wallet direct FFI integration: The Tauri GUI wallet now calls wallet2 directly through the Rust FFI bridge (
wallet_bridge.rs) instead of spawning a childshekyl-wallet-rpcprocess and communicating via HTTP. Eliminates process management, port allocation, and HTTP overhead. Removedwallet_process.rsandwallet_rpc.rs.
v3-First Core Test Adaptation
- Enforced min_tx_version=3 for non-coinbase transactions: All user transactions in the test suite now construct v3 with PQC authentication (hybrid Ed25519 + ML-DSA-65). Coinbase transactions remain v2.
- Adapted chaingen framework for RCT-from-genesis: Transaction
construction helpers (
construct_tx_to_key,construct_tx_rct) threadhf_version=1anduse_view_tags=true. Coinbase outputs are indexed underamount=0for correct RCT spending. Fixed difficulty is injected for FAKECHAIN replay. Mixin checks are relaxed for FAKECHAIN. - Added RCT-aware balance verification: Pool transaction balance checks
in
gen_chain_switch_1now decrypt ecdhInfo amounts using the recipient's view key instead of relying on the plaintexto.amountfield (always 0 for RCT outputs). - Recalibrated economic constants for Shekyl: Test constants
(
TESTS_DEFAULT_FEE,FIRST_BLOCK_REWARD,MK_COINS) match Shekyl'sCOIN = 10^9,EMISSION_SPEED_FACTOR = 21, and staker/burn splits.construct_miner_tx_manuallyin block validation tests uses Shekyl's reward distribution. - Fixed Bulletproofs+ test suite: Dynamically discover miner output amounts, set HF to 1 for all block construction, correctly flag coinbase outputs as RCT. All 15 BP+ tests pass.
- Fixed txpool tests: Adjusted key image count assertions for multi-input RCT transactions and corrected unlock_time handling.
- Fixed double-spend tests: Modified output selection to pick the largest decomposed output, avoiding underflow on fee subtraction.
- Disabled legacy-incompatible tests:
gen_block_invalid_binary_format(hours-long),gen_block_invalid_nonce,gen_block_late_v1_coinbase_tx,gen_uint_overflow_1,gen_block_reward,gen_bpp_tx_invalid_before_fork,gen_bpp_tx_invalid_clsag_type,gen_ring_signature_big. These rely on pre-RCT economics, legacy fork transitions, or are prohibitively slow. - All 79 core_tests pass with 0 failures.
Test suite cleanup for Shekyl HF1
- Removed 96 dead Borromean ringct tests: All tests in
tests/unit_tests/ringct.cppthat exercised legacy Borromean range proofs were removed. Shekyl HF1 rejects Borromean proofs at thegenRctSimplelevel. Retained 9 non-Borromean tests (CLSAG, HPow2, d2h, d2b, key_ostream, zeroCommit, H, mul8). - Updated transaction construction helpers to Bulletproofs+: The
test::make_transactionhelper (used by JSON serialization and ZMQ tests) now constructs transactions with{ RangeProofPaddedBulletproof, 4 }(BP+/CLSAG) instead of the removed Borromean or unsupported BP v2 configs. Removed the obsoletebulletproofparameter. Consolidated three JSON serialization tests (RegularTransaction, RingctTransaction, BulletproofTransaction) into oneBulletproofPlusTransactiontest. Fixes all 8 zmq_pub/zmq_server test failures. - Updated serialization round-trip test to BP+: Changed
Serialization.serializes_ringct_typesfrombp_version 2(throws "Unsupported BP version") tobp_version 4(Bulletproofs+). Updated assertions from MGs to CLSAGs and frombulletproofstobulletproofs_plus. - Removed legacy Monero-era core/perf test executions: Stopped running
deprecated Borromean/pre-RCT/fork-transition test generators in
core_testsand removed Borromean/MLSAG/range-proof performance test invocations and defaults, so CI validates HF1-era behavior only. - Hardened block-weight test contract for HF1 semantics:
block_weightcomparison now enforces deterministicH/BW/LTBWparity and EMBW floor invariants instead of byte-identical legacy model output, preventing false failures from non-consensus median implementation details. - Fixed block_reward test expected values: Updated emission curve
expectations to match Shekyl's
EMISSION_SPEED_FACTOR = 21(120s blocks) and per-block tail floor ofFINAL_SUBSIDY_PER_MINUTE * target_minutes. - Rewrote mining_parity release multiplier test: Replaced legacy pre-Shekyl-NG equality assertion (which tested a non-existent version 0) with a test that verifies the release multiplier correctly scales rewards above and below the tx volume baseline.
- Fixed Ubuntu 24.04 CI test runner: Replaced
pip installwithapt install python3-*packages to comply with PEP 668 (externally-managed-environment).
🐛 Fixed
-
macOS cross-compilation (depends CI): Fixed multiple build failures for Cross-Mac x86_64 and Cross-Mac aarch64 targets:
- Raised macOS minimum deployment target from 10.8 (Mountain Lion, 2012)
to 10.15 (Catalina, 2019) to enable
std::filesystemsupport in the cross-compiled libc++. - Fixed Boost discovery in depends builds by setting
Boost_NO_BOOST_CMAKEand forcing MODULE mode, preventingBoostConfig.cmakevariant-check failures on cross-compiled Darwin libraries. - Made
boost_localea conditional dependency (Windows only), since it is only used within#ifdef WIN32blocks and was unavailable for Darwin cross-builds. - Added per-target
CC_<triple>/AR_<triple>/CFLAGS_<triple>environment variables inBuildRust.cmakeso theringcrate can locate the cross-compiler for C/assembly code. - Used system clang (instead of the depends-bundled Clang 9) for Rust
crate C compilation on Darwin, since
ring0.17 requires clang features unavailable in Clang 9 (macOS 11 version strings,-fno-semantic-interposition). - Guarded
-fno-semantic-interpositionbehindcheck_c_compiler_flag()so it is only added when the compiler supports it (Clang 9 does not). - Fixed OSX SDK cache key in
depends.ymlto include the SDK version and skip the cache step for non-macOS builds.
- Raised macOS minimum deployment target from 10.8 (Mountain Lion, 2012)
to 10.15 (Catalina, 2019) to enable
-
FreeBSD cross-compilation (depends CI): Fixed multiple build failures for the x86_64 FreeBSD target:
- Switched Boost's b2 toolset from
gcctoclangfor FreeBSD, fixing C++ standard library header resolution (<cstddef>not found). - Embedded
-stdlib=libc++in the FreeBSD clang++ wrapper script so all depends packages automatically use the correct C++ standard library, regardless of whether their own$(package)_cxxflagsoverrides the host flags (previously broke zeromq, sodium, and other packages). - Fixed compiler wrapper argument quoting: replaced the broken
echo "...$$$$""@"pattern withprintf '..."$$$$@"'so"$@"passes through correctly to the generated wrapper, preventing argument mangling for flags containing quotes (e.g.-DPACKAGE_VERSION="1.0.20"). - Added
-D_LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTIONto both Boost's FreeBSD cxxflags and the CMake toolchain, restoringstd::unary_functioncompatibility needed by Boost 1.74'scontainer_hash/hash.hppunder FreeBSD's strict C++17 libc++. - Removed the unsupported
no-devcryptooption from OpenSSL's FreeBSD configure flags (the devcrypto engine was removed in OpenSSL 3.0). - Added
threadapi=pthread runtime-link=sharedto Boost's FreeBSD config options for correct threading and linking behavior.
- Switched Boost's b2 toolset from
-
Linux static release build (libudev linking): Added
libudev-devto therelease-tagged.ymlCI package list. Staticlibusb-1.0.aandlibhidapi-libusb.adepend onlibudevfor USB hotplug support; without the dev package installed,find_library(udev)failed and the final link produced undefinedudev_*references, preventing the "Publish GitHub Release" step from running. -
Win64 build failure (ICU generator expression): Replaced broken CMake generator expressions
$<$<BOOL:${WIN32}>:${ICU_LIBRARIES}>withif(WIN32)blocks insimplewallet,wallet_api, andlibwallet_api_testsCMakeLists. Generator expressions cannot contain semicolon-separated lists; the old pattern passed literal fragments like$<1:icuioto the linker on MinGW cross-compilation. -
Linux static build (libunbound linking): Fixed
FindUnbound.cmakescoping bug wherelist(APPEND UNBOUND_LIBRARIES ...)created a local variable shadowing thefind_librarycache entry. The transitive static deps (libevent, libnettle, libhogweed, libgmp) were silently dropped, causing undefined reference errors inrelease-static-linux-x86_64builds. -
JSON serialization of v3 (PQC) transactions: Added missing
pqc_authfield to the RapidJSONtoJsonValue/fromJsonValueroundtrip forcryptonote::transaction. V3 transactions created underHF_VERSION_SHEKYL_NGinclude apqc_authenticationenvelope; without JSON support the field was silently dropped, causingget_transaction_hashto fail with "Inconsistent transaction prefix, unprunable and blob sizes" after a JSON roundtrip. Fixes theJsonSerialization.BulletproofPlusTransactionunit test failure.
GUI Wallet
- New project: Shekyl GUI Wallet (
shekyl-gui-wallet) at Shekyl-Foundation/shekyl-gui-wallet. Built with Tauri 2 (Rust backend) + Vite + React 19 + TypeScript + Tailwind CSS 4. Initial scaffold includes 6 pages (Dashboard, Send, Receive, Staking, Transactions, Settings), stub Tauri commands, Shekyl gold/purple design system, and verified production builds for Linux (.deb, .rpm, .AppImage). Phase 2 will add the C++ FFI bridge towallet2_api.hfor real wallet operations. - Added testing infrastructure: Vitest + React Testing Library for frontend (20 tests across 6 suites), cargo test for Rust backend (10 tests), with Tauri IPC mocking for isolated component testing.
- Added CI/CD via GitHub Actions:
ci.ymlruns ESLint, TypeScript type-check, Vitest, Rustfmt, Clippy, and cargo test on every PR;release.ymlbuilds multi-platform binaries (Linux x64, Windows x64, macOS ARM64 + Intel) viatauri-actionand creates draft GitHub releases.
Consensus timing alignment (HF1)
- Fixed remaining runtime paths that still derived timing from legacy
DIFFICULTY_TARGET_V1(60s) so active Shekyl HF1 behavior consistently usesDIFFICULTY_TARGET_V2(120s) for difficulty target selection, block reward minute-scaling, unlock-time leeway checks, sync ETA reporting, and wallet lock-time display. - Updated
docs/ECONOMY_TESTNET_READINESS_MATRIX.mdto mark the 120s block-time drift item as resolved (code_fix_requiredcompleted).
📚 Documentation
- Updated
docs/V3_ROLLOUT.mdto reflect HF1 (genesis) activation instead of the stale HF17 references. Added v3-first test strategy section. - Updated
docs/POST_QUANTUM_CRYPTOGRAPHY.mdscheme_id status table and deferred-items section from HF17 to HF1. - Updated
docs/PQC_MULTISIG.mdV3 signature list heading from HF17 to HF1. - Updated
docs/STAKER_REWARD_DISBURSEMENT.mdto reference HF1 activation. - Updated
docs/ECONOMY_TESTNET_READINESS_MATRIX.mdHF naming drift label fromdoc_correctionto resolved. - Added
core_testssection todocs/COMPILING_DEBUGGING_TESTING.mddocumenting the v3-from-genesis test approach and how to run/filter tests.
Genesis initialization compatibility
- Regenerated
GENESIS_TXfor mainnet, testnet, and stagenet to modern coinbase format (tx.version = 2) with tagged outputs. - Removed all legacy genesis compatibility exceptions and enforced strict coinbase version checks (
tx.version > 1) across all network types, includingFAKECHAIN. - Fixed genesis reward validation to accept the hardcoded
GENESIS_TXamount atheight == 0while leaving post-genesis reward accounting unchanged. - Fixed startup edge case where long-term weight median calculations could evaluate with zero historical blocks during genesis initialization (
count == 0), causing daemon boot failure on empty data dirs. - Updated genesis-construction helper (
build_genesis_coinbase_from_destinations) to emittx.version = 2with view-tagged outputs for current HF1 expectations. - Added canonical root build command
make genesis-builder(using the main release build dir withGENESIS_TOOL_SRC_DIR) to avoid split/ambiguous genesis-builder binaries across multiple build trees.
Testnet economy readiness checks
- Added
docs/ECONOMY_TESTNET_READINESS_MATRIX.mdto track design-vs-code status for economy testnet rehearsal with explicit drift tags (doc_correction,code_fix_required,needs_decision). - Added
scripts/check_testnet_genesis_consensus.pyto verify multi-node testnet tuple consistency (height 0 block hash,miner tx hash,tx hex) and optional economy field presence inget_info. - Added Rust parity/invariant tests:
shekyl-economics-sim: validatesSimParams::default()againstconfig/economics_params.json.shekyl-economics: added release monotonicity, burn bounds, and emission-share monotonicity tests.shekyl-ffi: added direct FFI-vs-Rust consistency tests for burn pct and emission share.
- Added functional RPC test
tests/functional_tests/economy_info.pyand included it infunctional_tests_rpc.pydefault test list to assert required economy fields are exposed byget_info. - Corrected documentation errors without changing design intent:
- Clarified
DESIGN_CONCEPTS.mdSection 2 as historical baseline. - Removed duplicate heading in
GENESIS_TRANSPARENCY.md. - Linked
RELEASE_CHECKLIST.mdtestnet section to the rehearsal runbook/checklist and deterministic tuple check command.
- Clarified
BREAKING: Second-pass rebrand (wallet, URI, serialization)
- URI scheme: Wallet URI generation and parsing now use
shekyl:only. The legacymonero:scheme is no longer accepted. QR codes and payment links generated by previous builds will fail to parse. Regenerate all payment URIs before upgrading wallets. - Wallet/export/cache magic strings: All file-format magic prefixes have
been rewritten from
MonerotoShekyl:UNSIGNED_TX_PREFIX→"Shekyl unsigned tx set\005"SIGNED_TX_PREFIX→"Shekyl signed tx set\005"MULTISIG_UNSIGNED_TX_PREFIX→"Shekyl multisig unsigned tx set\001"KEY_IMAGE_EXPORT_FILE_MAGIC→"Shekyl key image export\003"MULTISIG_EXPORT_FILE_MAGIC→"Shekyl multisig export\001"OUTPUT_EXPORT_FILE_MAGIC→"Shekyl output export\004"ASCII_OUTPUT_MAGIC→"ShekylAsciiDataV1"- Wallet cache magic →
"shekyl wallet cache"Old wallet caches, exported key images, multisig exports, signed/unsigned tx sets, and output exports are incompatible and must be re-exported after upgrading.
- Message signing domain:
HASH_KEY_MESSAGE_SIGNINGchanged from"MoneroMessageSignature"to"ShekylMessageSignature". Messages signed with the old domain separator will fail verification. - i18n domain: Translation catalogue domain changed from
"monero"to"shekyl". - Daemon stdout redirect: Daemonized output file changed from
bitmonero.daemon.stdout.stderrtoshekyl.daemon.stdout.stderr. - Log file names: All blockchain utility log files renamed from
monero-blockchain-*toshekyl-blockchain-*. - DNS seed/checkpoint domains: Replaced
moneroseeds.*andmoneropulse.*lookups with 5-domain consensus set:shekyl.org,shekyl.net,shekyl.com,shekyl.biz,shekyl.io. Majority threshold is 3 of 5. Seeshekyl-dev/docs/DNS_CONFIG.mdfor the full infrastructure reference. - Update check: Software name comparison for macOS
.dmgextension switched frommonero-guitoshekyl-gui. - Hardware wallet: Ledger app error message now references "Shekyl Ledger App" instead of "Monero Ledger App". Trezor protobuf namespaces are unchanged (third-party protocol dependency).
- Intentionally preserved: Trezor/Ledger protobuf includes and protocol
namespaces (
hw.trezor.messages.monero.*), Esperanto mnemonic word"monero"(means "money"), academic paper citations, copyright headers,MONERO_DEFAULT_LOG_CATEGORYbuild-internal macros, andMakeCryptoOps.pybuild artifacts.
Operator migration checklist
- Delete old wallet cache files (
.keysfiles are unaffected). - Re-export any key-image, multisig, or output export files.
- Re-export and re-sign any unsigned/signed transaction sets.
- Regenerate all
monero:QR codes/payment URIs asshekyl:URIs. - Update any scripts or integrations that parse URI scheme or file magic.
- Verify message signatures were not created with the old signing domain.
- Update log rotation configs if they reference
monero-blockchain-*paths. - Update DNS infrastructure to serve records under all 5 TLDs (
.org,.net,.com,.biz,.io). Seeshekyl-dev/docs/DNS_CONFIG.md.
Dead Monero legacy code removal
-
Dead HF branch cleanup: Collapsed all always-true / always-false hard fork version branches across
blockchain.cpp(~25 sites),wallet2.cpp(~22 sites),cryptonote_basic_impl.cpp(2 sites), andcryptonote_core.cpp(2 sites). Since allHF_VERSION_*constants are 1, everyhf_version >= HF_VERSION_*was always true and everyhf_version < HF_VERSION_*was always false. Collapsed fee algorithms, ring size ladders, tx version ladders, difficulty target selection, sync block size selection, BP/CLSAG/BP+ gating, dynamic fee scaling, long-term block weight calculations, anduse_fork_rules()call sites. Removed ~500-800 lines of dead conditional logic. -
Dropped v1 transaction support entirely:
- Consensus:
check_tx_outputsnow rejectstx.version == 1outright.check_tx_inputssetsmin_tx_version = 2unconditionally; unmixable output counting and ring-size exemptions removed. v1 ring signature verification code and threaded v1 signature checking removed fromcheck_tx_inputs.expand_transaction_2only handles CLSAG and BulletproofPlus; old RCTTypeFull/Simple/Bulletproof/Bulletproof2 branches removed. - RingCT (
rctSigs.cpp/.h): Removed ~770 lines of dead crypto code:genBorromean,verifyBorromean,MLSAG_Gen,MLSAG_Ver,proveRange,verRange,proveRctMG,proveRctMGSimple,verRctMG,verRctMGSimple,populateFromBlockchain,genRct(both overloads),verRct,decodeRct(both overloads).genRctSimple,verRctSemanticsSimple,verRctNonSemanticsSimple, anddecodeRctSimpleonly acceptRCTTypeCLSAGandRCTTypeBulletproofPlus. Header reduced from 144 to 87 lines. - Transaction construction (
cryptonote_tx_utils.cpp): Removed v1 ring signature generation block and non-simple RCT construction (genRct). All transactions now usegenRctSimple(CLSAG path). - Tx verification utils: Removed
RCTTypeSimple,RCTTypeFull,RCTTypeBulletproof,RCTTypeBulletproof2from batch semantics verification. - Test fixups: Updated all test files under
tests/to match the removed RCT primitives. Stubbed performance benchmarks for MLSAG (rct_mlsag.h,sig_mlsag.h) and Borromean range proofs (range_proof.h). ReplacedverRctwithverRctNonSemanticsSimpleincheck_tx_signature.h. RemoveddecodeRctelse-branches fromrct.cpp,rct2.cpp,bulletproofs.cpp,bulletproof_plus.cpp. Inunit_tests/ringct.cpp: removed Borromean, MLSAG, and RCTTypeFull-only tests; rewrotemake_sample_rct_sigto usegenRctSimple; replaced allverRctcalls withverRctSimple.
- Consensus:
-
Wallet v1 cleanup: Removed unmixable sweep functions, v1 fee/amount paths, v1 coinbase optimization, dead non-RCT creation branches, and replaced
RangeProofBorromeandefaults withRangeProofPaddedBulletproof.sweep_dustRPC returns error;createSweepUnmixableTransactionAPI returns empty result with error status. -
Trezor Shekyl rebrand: Renamed all include guard macros from
MONERO_*_HtoSHEKYL_*_Hin 8device_trezor/headers. Updated derivation path comment and HTTP Origin URL. Protobuf message types and wire protocol identifiers intentionally preserved (must match Trezor firmware definitions).
Epee Phase 1: Rust replacement for security-critical primitives
- SSL certificate generation migrated to Rust (
rcgen): Replaced the deprecated OpenSSL RSA/EC_KEY certificate generation innet_ssl.cppwith Rust'srcgencrate (ECDSA P-256) via FFI. Eliminates allRSA_new,RSA_generate_key_ex,EC_KEY_new,EC_KEY_generate_key, and other OpenSSL 3.0-deprecated API calls. Thecreate_rsa_ssl_certificateandcreate_ec_ssl_certificatefunctions are replaced by a singlecreate_ssl_certificatethat delegates toshekyl_generate_ssl_certificatein the Rust FFI, returning PEM-encoded key+cert for loading into OpenSSL's SSL_CTX via non-deprecated BIO APIs. - Post-quantum hybrid key exchange enabled: TLS context configuration now
prefers
X25519MLKEM768(FIPS 203 ML-KEM-768 hybrid) key exchange groups, falling back to classicalX25519:P-256:P-384when the OpenSSL build lacks PQ support. Also added explicit TLS 1.3 ciphersuite configuration. Removed deprecatedSSL_CTX_set_ecdh_autocall. - Secure memory wiping migrated to Rust (
zeroize): Replaced the platform-specificmemwipe.cimplementation (memset_s / explicit_bzero / compiler-barrier fallback) with a single call to the Rustzeroizecrate viashekyl_memwipeFFI. Thezeroizecrate useswrite_volatilewhich is guaranteed not to be optimized away, replacing the fragile compiler barrier tricks. - Memory locking migrated to Rust (
libc): Replaced the GNUC-onlymlock/munlock/sysconfcalls inmlocker.cppwith Rust FFI functions (shekyl_mlock,shekyl_munlock,shekyl_page_size) backed by thelibccrate. Adds WindowsVirtualLock/VirtualUnlocksupport that was previously missing (#warning Missing implementation). Themlocked<T>andscrubbed<T>C++ template wrappers are preserved unchanged. - New Rust FFI dependencies: Added
rcgen = "0.14",zeroize = "1",libc = "0.2"toshekyl-ffi/Cargo.toml. - C-compatible FFI header: Added
src/shekyl/shekyl_secure_mem.hwith C-linkage declarations for the secure memory primitives, usable from both C (memwipe.c) and C++ (mlocker.cpp) translation units. - CMake wiring:
epeelibrary now links${SHEKYL_FFI_LINK_LIBS}and includes${CMAKE_SOURCE_DIR}/srcfor the FFI headers.
Build fixes
- Boost CONFIG-mode compatibility shim: When Boost is found via cmake
CONFIG mode (Boost 1.85+), old-style
${Boost_XXX_LIBRARY}variables may resolve to versioned.sopaths that don't exist on rolling-release distros (e.g. Arch Linux with Boost 1.90). Added a shim in the rootCMakeLists.txtthat remaps allBoost_*_LIBRARYvariables toBoost::*imported targets when CONFIG mode is active. Fixes linker failures on Arch. - Removed duplicate
parse_amounttest: Two identicalTEST_pos(18446744073709551615, ...)entries intests/unit_tests/parse_amount.cppcaused a redefinition error on macOS Clang. Removed the duplicate. - Boost CONFIG-mode validation: Added a cmake-configure-time check that
verifies Boost imported-target
IMPORTED_LOCATIONfiles exist on disk. Gives a clearFATAL_ERRORwith remediation steps instead of a cryptic linker failure minutes into the build. - Arch Linux CI: Added
boost-libsto the Arch pacman install to provide shared.sofiles alongside theboostheaders/cmake-config package. - Ubuntu 24.04 test matrix: Added Ubuntu 24.04 to the
test-ubuntuCI matrix (previously only 22.04 was tested).
Depends system updates
- FreeBSD sysroot updated to 14.4-RELEASE: The cross-compilation
sysroot was stuck at FreeBSD 11.3 (EOL Sept 2021), whose
base.txzhad been removed from FreeBSD mirrors (404). Updated to 14.4-RELEASE (March 2026), updated SHA256 hash, and fixed clang wrapper scripts from clang-8 to clang-14 to matchhosts/freebsd.mk. Added-stdlib=libc++to CXXFLAGS and LDFLAGS since FreeBSD uses libc++ and the Ubuntu host's clang-14 defaults to libstdc++. Also addedlibc++-14-devandlibc++abi-14-devto CI packages for the FreeBSD cross-build so the host compiler can find libc++ headers when-stdlib=libc++is specified. - Boost: skip CONFIG mode for depends builds: The depends-built Boost
1.74.0 installs CMake config files whose variant detection fails for
darwin cross-builds (
boost_localereports "No suitable build variant").find_package(Boost ... CONFIG)is now skipped whenDEPENDSis true (set by the depends toolchain), falling back to the more robust MODULE mode (FindBoost.cmake). - OpenSSL: disabled
devcryptoengine for FreeBSD: Addedno-devcryptoto FreeBSD OpenSSL configure options. The/dev/cryptoengine requires thecrypto/cryptodev.hkernel header which is not available in a cross-compilation sysroot. - libsodium updated to 1.0.20: The 1.0.18 tarball was removed from
download.libsodium.org(404). Updated to 1.0.20 with new SHA256 hash. Removed the 1.0.18-specific patches (fix-whitespace.patch,disable-glibc-getrandom-getentropy.patch) which no longer apply.
Warning cleanup and dead code removal
- Removed dead fork helpers: Deleted unused
get_bulletproof_fork(),get_bulletproof_plus_fork(), andget_clsag_fork()fromwallet2.cpp. These Monero-era version ladders had no call sites; Shekyl activates all features from HF1. - Removed dead variable: Deleted unused
bool refreshedinwallet2::refresh(). - Removed legacy
result_typetypedefs: Deletedusing result_type = voidfromadd_inputandadd_outputvisitor structs injson_object.cpp. These were required byboost::static_visitorbut are unused bystd::visit. - Fixed uninitialized-variable warning: Zero-initialized
local_blocks_to_unlockandlocal_time_to_unlockinwallet2::unlocked_balance_all(). - Fixed aliasing cast in wallet serialization: Replaced C-style cast of
m_account_tagsfrompair<serializable_map, vector>topair<map, vector>&with direct.parent()accessor, eliminating formal undefined behavior. - Suppressed epee warnings: Added targeted
#pragma GCC diagnosticguards for-Wclass-memaccess(memcpy intomlocked<scrubbed<>>inkeyvalue_serialization_overloads.h) and-Wstring-compare(type_info comparisons inportable_storage.h). - Renamed test target:
monero-wallet-crypto-benchrenamed toshekyl-wallet-crypto-bench. - Trezor Protobuf fixes: Added
std::string()wrapping forGetDescriptor()->name()calls inmessages_map.cpp/.hppto handle Protobuf 22+ returningabsl::string_view/std::string_view. Added missing<cstdint>include toexceptions.hpp.
Rust crypto infrastructure
- New
shekyl-crypto-hashcrate: Implementscn_fast_hash(Keccak-256 with original padding, not SHA3) andtree_hash(Merkle tree) in Rust usingtiny-keccak. Both functions produce byte-identical output to the C implementations insrc/crypto/hash.candsrc/crypto/tree-hash.c. - FFI exports:
shekyl_cn_fast_hashandshekyl_tree_hashexposed throughshekyl-ffiwith C-ABI declarations inshekyl_ffi.h. The C++ side can now call Rust hashing alongside or instead of the C path. - Rust-preferred development rule: Added
.cursor/rules/rust-preferred.mdcestablishing policy for gradual C++ to Rust migration: new modules in Rust, crypto primitives via RustCrypto crates, computational extraction to Rust behind FFI when modifying existing C++ modules.
Hardfork reboot and testnet wallet readiness
- Hardfork schedule rebooted: All
HF_VERSION_*constants collapsed to 1. The chain starts with all features active from genesis -- no legacy migration gates. Hardfork tables reduced to single-entry{ 1, 1, 0, timestamp }for all three networks (mainnet, testnet, stagenet). - Removed all raw numeric HF version gates (
hf_version <= 3,>= 7,< 8,> 8, etc.) from consensus and transaction construction code, replacing them with namedHF_VERSION_*constants. Legacy Monero-era transition logic (borromean proofs, bulletproofs v1, grandfathered txs) removed. - Coinbase transactions always v2 RCT with single output, zero dust threshold.
- Staked outputs excluded from spendable balance:
is_transfer_unlocked()now returns false for staked outputs, preventing them from being selected during normal transfers.balance_per_subaddressandunlocked_balance_per_subaddressskip staked outputs. - Unstake transaction fixed:
create_unstake_transactionnow passes matured staked output indices directly tocreate_transactions_from, properly using the actual staked UTXOs as transaction inputs with standard ring signatures. - Claim reward validation fixed:
check_stake_claim_inputnow looks up the real staked output from the blockchain DB to get the actual amount and tier, replacing the hardcodedshekyl_stake_weight(0, 0)placeholder. - New daemon RPC
estimate_claim_reward: computes per-output reward server-side using the accrual database, returning reward amount, tier, and staked amount. Walletestimate_claimable_rewardnow calls this RPC instead of returning a hardcoded zero. - CLI improvements:
balancecommand now shows staked balance alongside liquid and unlocked balances. Newstaking_infocommand shows wallet staking overview (locked/matured output counts with tier and remaining lock blocks).stake,unstake, andclaim_rewardscommands now include daemon connectivity guards. - Wallet RPC fixes:
unstakeresponse changed from singletx_hashtotx_hash_listarray to support multi-transaction unstaking.stakerequest now acceptsaccount_indexparameter. Newget_staked_balanceRPC returns staked balance with locked/matured output counts.
Post-quantum cryptography
- Phase 4 wallet/core PQC wiring completed: all v3 transaction construction
paths now include hybrid Ed25519 + ML-DSA-65 signing via
pqc_auth. Fixedcreate_claim_transaction(staking reward claims) which previously built v3 transactions without PQC authentication, causing consensus rejection. - PQC verification enforced in both mempool acceptance and block validation for all non-coinbase v3 transactions.
- Multisig wallets intentionally restricted to v2 transactions (no PQC); the PQC secret key is cleared on multisig creation with a documented design note.
- Aligned
POST_QUANTUM_CRYPTOGRAPHY.mdfield naming:hybrid_ownership_materialrenamed tohybrid_public_keyto match the canonical code implementation. - Added three negative PQC test vectors (
docs/PQC_TEST_VECTOR_002–004) covering tampered ownership material, wrong scheme_id, and oversized/truncated signature blobs. Each vector is generated and verified by integration tests inrust/shekyl-crypto-pq/tests/negative_vectors.rs. - Reconciled
POST_QUANTUM_CRYPTOGRAPHY.mdOpen Items: resolved Rust crate selection,RctSigningBodylayout, ownership binding, and max tx size; onlyscheme_idregistry extension remains open. - Added tentative V4 PQC Privacy Roadmap to
POST_QUANTUM_CRYPTOGRAPHY.mdwith four phases (V4-A Research, V4-B Prototype, V4-C Testnet, V4-D Activation) and explicit KEM composition decision milestone (X25519 + ML-KEM-768viaHKDF-SHA-512). - Added payload limit guidance section to
V3_ROLLOUT.mdwith recommended minimum mempool/ZMQ/relay buffer sizes for post-PQC transactions.
Economics and simulation
- Added
rust/shekyl-economics-simworkspace crate: reproducible 8-scenario simulation harness driven fromconfig/economics_params.json. Scenarios cover baseline, boom-bust, sustained growth, stuffing attack, stake concentration, mass unstaking, chain bootstrap, and late-chain tail state. Results archived indocs/economics_sim_results.json. - Provisionally locked
tx_baseline(50) andFINAL_SUBSIDY_PER_MINUTE(300,000,000) inDESIGN_CONCEPTS.mdafter simulation validation; pending final testnet confirmation. - Wired live chain-health RPC fields in
get_info:release_multipliernow computed from rollingtx_volume_avg,burn_pctfrom current chain state,total_burnedpersisted in LMDB and accumulated per block. - Wired
total_stakedinget_staking_infovia newBlockchain::get_total_staked()accessor backed by existing stake cache. - Added
total_burnedLMDB persistence:set_total_burned/get_total_burnedonBlockchainDB, with rollback support via extendedstaker_accrual_record(actually_destroyedfield).
Privacy and anonymity networks
- Updated
ANONYMITY_NETWORKS.mdwith measured v3 payload impact analysis (cell/fragment counts for Tor and I2P), known leak vectors vs mitigations matrix, and recommended pre-mainnet testing checklist. - Extended
LEVIN_PROTOCOL.mdwire inventory with per-command PQC size impact, anonymity sensitivity ratings, and a summary table covering all P2P and Cryptonote protocol commands. - Added privacy considerations section to
STAKER_REWARD_DISBURSEMENT.mdcovering claim timing, amount correlation, and staked output visibility. - Added reward-driven privacy/mixing research appendix to
DESIGN_CONCEPTS.mdevaluating random maturation delay, claim batching, and reward output shaping with adversarial analysis and go/no-go criteria.
C++17 and Boost migration
- C++17 standard bump:
CMAKE_CXX_STANDARDchanged from 14 to 17 in both the mainCMakeLists.txtand the macOS cross-compilation toolchain (contrib/depends/toolchain.cmake.in). This unblocksstd::filesystem,std::optional, and other modern C++ features. Upstream Monero cherry-picks that required C++14-to-C++17 back-ports now compile without shims. boost::optional→std::optional(complete): Migrated ~486 use sites across ~93 files insrc/,contrib/epee/, andtests/. Replacedboost::optional<T>withstd::optional<T>,boost::nonewithstd::nullopt,boost::make_optionalwithstd::make_optional, and.get()accessor calls with*/->. Added astd::optionalBoost.Serialization adapter incryptonote_boost_serialization.hso PQC auth fields serialize correctly. ReplacedBOOST_STATIC_ASSERT/boost::is_base_ofwithstatic_assert/std::is_base_ofin Trezormessages_map.hpp.boost::filesystem→std::filesystem(wallet/RPC layer): Migratedwallet_manager.cpp,wallet_rpc_server.cpp,core_rpc_server.cpp, andwallet_args.cppfromboost::filesystemtostd::filesystem. Combined with the earlier utility-file migration, this covers all filesystem usage outside ofnet_ssl.cpp(epee, deferred due to permissions API coupling).boost::formatremoval (wallet/RPC layer): Replaced allboost::formatcalls inwallet2.cpp(4),wallet_rpc_server.cpp(8), andwallet_args.cpp(1) with stream output or string concatenation.simplewallet.cpp(106 uses, i18n-sensitive) remains deferred.boost::chrono/boost::this_threadin daemonizer: Replaced withstd::chrono/std::this_threadinwindows_service.cpp(PR #9544 equivalent).- Medium-effort Boost removals (completed earlier):
boost::algorithm::string(trim, to_lower, iequals, join) replaced withtools::string_utilhelpers insrc/common/string_util.h.boost::formatreplaced withsnprintf, stream output, or string concatenation inutil.cpp,message_store.cpp,gen_ssl_cert.cpp,gen_multisig.cpp.boost::regexreplaced withstd::regexinsimplewallet.cppandwallet_manager.cpp.boost::mutex,boost::lock_guard,boost::unique_lock, andboost::condition_variablereplaced withstd::mutex,std::lock_guard,std::unique_lock, andstd::condition_variableinutil.h,util.cpp,threadpool.h,threadpool.cpp, andrpc_payment.h/rpc_payment.cpp.boost::thread::hardware_concurrency()replaced withstd::thread::hardware_concurrency().
- Filesystem migration (utility files, completed earlier):
boost::filesystemreplaced withstd::filesysteminblockchain_export.cpp,blockchain_import.cpp,cn_deserialize.cpp,util.cpp,bootstrap_file.h/.cpp, andblocksdat_file.h/.cpp.- Eliminated
BOOST_VERSIONpreprocessor conditional incopy_file().
- Upstream Monero cherry-pick verification: Confirmed PRs #9628 (ASIO
io_service→io_context), #6690 (serialization overhaul), and #9544 (daemonizer chrono/thread) are already absorbed in our tree. boost::variant→std::variant(complete): Full migration fromboost::variantto C++17std::variantacross the entire codebase (~100+ replacements in ~40 files):- Serialization layer rewrite (
serialization/variant.h): Replaced Boost.MPL type-list iteration with C++17if constexprrecursion for deserialization andstd::visitlambda for serialization. Removed allboost::mpl,boost::static_visitor, andboost::apply_visitorusage. - Archive headers: Replaced
boost::mpl::bool_<B>withstd::bool_constant<B>inbinary_archive.h,json_archive.h, andserialization.h. Replacedboost::true_type/false_typeandboost::is_integralwithstdequivalents. - Core typedefs: Changed
txin_v,txout_target_v,tx_extra_field,transfer_view::block, and Trezorrsig_vfromboost::varianttostd::variant. - Boost.Serialization shim: Added a local ~45-line
std::variantserialization adapter incryptonote_boost_serialization.h(save/load with index + payload, wire-compatible with oldboost::variantformat). Removed dependency on<boost/serialization/variant.hpp>. - Mechanical replacements across all
src/andtests/files:boost::get<T>(v)→std::get<T>(v),boost::get<T>(&v)→std::get_if<T>(&v),v.type() == typeid(T)→std::holds_alternative<T>(v),v.which()→v.index(),boost::apply_visitor(vis, v)→std::visit(vis, v). - P2P layer: Updated
net_peerlist_boost_serialization.hto usestd::false_type/std::true_typeinstead ofboost::mplequivalents. tests/unit_tests/net.cppretainsboost::get<N>forboost::tupleaccess viaboost::combine(not variant-related).
- Serialization layer rewrite (
- Remaining deferred Boost areas: ASIO deep plumbing,
multi-index containers, Spirit parser, multiprecision,
net_ssl.cppfilesystem,simplewallet.cppformat strings,boost::thread::attributes(stack size). Tagged withTODO(shekyl-v4)in source. SeeDOCUMENTATION_TODOS_AND_PQC.mdsection 1.11 for the full backlog.
CI/CD and build system
- Boost minimum bumped to 1.74:
BOOST_MIN_VERinCMakeLists.txtraised from 1.62 to 1.74. Thecontrib/dependssystem now pins Boost 1.74.0 (previously 1.69.0) and builds with-std=c++17. Removed legacy Boost 1.64 patches (fix_aroptions.patch,fix_arm_arch.patch) that do not apply to 1.74. - CI containers updated to Ubuntu 22.04 minimum: Dropped Debian 11 and
Ubuntu 20.04 build jobs from
build.yml,depends.yml, andrelease-tagged.yml. Ubuntu 22.04 is now the lowest-common-denominator Linux build environment (ships Boost 1.74+ and GCC 11+). Added Ubuntu 24.04 build matrix entry. - Migrated version identifiers from legacy
MONERO_*symbols to canonicalSHEKYL_*names (SHEKYL_VERSION,SHEKYL_VERSION_TAG,SHEKYL_RELEASE_NAME,SHEKYL_VERSION_FULL,SHEKYL_VERSION_IS_RELEASE) insrc/version.handsrc/version.cpp.in. The oldMONERO_*names are retained as preprocessor aliases so existing call sites and future Monero upstream cherry-picks continue to compile unchanged. The aliases will be removed in a single cleanup after v4 RingPQC stabilises. - Fixed Gitian deterministic build pipeline: replaced all hardcoded Monero
repository URLs and internal package names with Shekyl equivalents across
gitian-build.py, all 5 gitian descriptor YAMLs,dockrun.sh, and thegitian.ymlGitHub Actions workflow. The workflow now passes--urlto ensure the correct repository is cloned. Added checkout error handling with an actionable message when a tag/branch is missing. - Tag-driven versioning:
GitVersion.cmakenow extracts the version string from git tags (e.g.v3.0.2-RC1→3.0.2-RC1). The hardcoded version inversion.cpp.inis replaced with the CMake-substituted@SHEKYL_VERSION@; a default (3.1.0) is used for development builds not on a tag.Version.cmakecentralises the fallback default inSHEKYL_VERSION_DEFAULT. - Updated RPC version string validator (
rpc_version_str.cpp) from Monero's four-number format to Shekyl's three-number semver with optional pre-release suffix (e.g.3.0.2-RC1-release). - Updated gitian descriptor names from Monero's
0.18to Shekyl3series. - Added
release/taggedGitHub Actions workflow: builds static Linux x86_64 binaries, cross-compiles Windows x64 via MinGW, and produces.tar.gz,.deb,.rpm,.zip, and NSIS.exeinstaller artifacts on everyv*tag. - Added
BuildRust.cmakecross-compilation support: detectsCMAKE_SYSTEM_NAMEandCMAKE_SYSTEM_PROCESSORto derive Rust target triples for Windows, macOS, Android, FreeBSD, and Linux cross-targets (ARM, aarch64, i686, RISC-V); automatically configures the MinGW linker for Windows cross-compilation. - Added Rust toolchain installation to all CI workflows (
build.yml,depends.yml,release-tagged.yml) and all 5 Gitian deterministic build descriptors with appropriate cross-compilation targets; required forlibshekyl_ffi.alinking. - Fixed Gitian
gitian-build.pyto fetch tags explicitly (--tags) during repository setup, preventing checkout failures for tag-based builds. - Enhanced
gitian-build.pyerror handling: robustlsb_releasedetection, auto-correction of stale clone origins when--urlchanges, and detailed diagnostics on checkout failure (lists available remote tags and suggests the push command). - Added
workflow_dispatchtrigger togitian.ymlwith configurabletagandrepo_urlinputs, allowing manual re-runs and testing against forks without retagging. - Fixed Doxygen project name from
MonerotoShekylincmake/Doxyfile.in. - Replaced bundled Google Test 1.7.0 (2013) with CMake
FetchContentfor GoogleTest v1.16.0. FixesGTEST_SKIPcompilation errors on all platforms without a system gtest. Removes 34k lines of vendored source. - Upgraded all GitHub Actions workflows to Node.js 24: bumped
actions/checkoutto v5,actions/cacheto v5,actions/upload-artifactto v6, andactions/download-artifactto v7 to resolve the Node.js 20 deprecation warnings. - Trimmed
depends.ymlcross-compilation matrix: dropped i686 Win and i686 Linux (32-bit targets are dead); deferred RISCV 64-bit and ARM v7 until user demand materialises. Active matrix is now ARM v8, Win64, x86_64 Linux, Cross-Mac x86_64, Cross-Mac aarch64, and x86_64 FreeBSD (6 targets, down from 10). Added Cross-Mac aarch64 to the artifact upload filter. - Added Linux packaging files:
contrib/packaging/linux/shekyld.service(systemd unit) andcontrib/packaging/windows/shekyl.nsi(NSIS installer).
Upstream Monero sync (March 2026)
Cherry-picked 62 upstream Monero commits (from monero-project/monero master)
across five risk-phased integration rounds. Key improvements absorbed:
- Wallet: Fee priority refactoring (
fee_priorityenum + utility functions), improved subaddress lookahead logic,set_subaddress_lookaheadRPC endpoint (no longer requires password), incoming transfers without daemon connection, HTTP body size limit, fast refresh checkpoint fix, ring index sanity checks,find_and_save_rings()deprecation, pool spend identification during scan. - Daemon/RPC: Dynamic
print_connectionscolumn width, ZMQ IPv6 support, dynamic base fee estimates via ZMQ,getblocks.binstart height validation, CryptoNight v1 error reporting, batch key image existence check, blockchain prune DB version handling, removedCOMMAND_RPC_SUBMIT_RAW_TX(light wallet deprecated). - P2P/Network: Removed
state_idleconnection state, fixed inverted peerlist ternary, removed#pragma packfrom protocol defs, connection patches for reliability, dynamic block sync span limits. - Crypto/Serialization: Fixed invalid
constexpron hash functions, addedhash_combine.h, aligned container pod-as-blob serialization, fixedapply_permutation()forstd::vector<bool>. - Build system: Removed iwyu/MSVC/obsolete CMake targets, added
MANUAL_SUBMODULEScache option, Trezor protobuf 30 compatibility, fixedFetchContent/ExternalProjectcmake usage. - Tests: New unit tests for format utils, threadpool, varint, logging, serialization static asserts, cold signing functional test fixes.
- Misc: Boost ASIO 1.87+ compatibility, fixed Trezor temporary binding,
fixed multisig key exchange intermediate message update,
constexprcn_variant1_check, extra nonce length fix, removed redundant BP consensus rule.
Skipped commits (deferred to future integration): input verification caching
(conflicts with txin_stake_claim/PQC), wallet_keys_unlocker refactoring,
get_txids_loose DB API (missing prerequisite), complex subaddress lookahead
fixes, and several CMake/depends version bumps that conflict with Shekyl's
build system divergences.
Cherry-picked code was initially adapted to C++14 compatibility; with the
subsequent C++17 standard bump, many of those back-ports are now unnecessary
and can use native std::optional, std::string_view, etc.
Documentation
- Added
docs/EXECUTABLES.md: comprehensive reference for all 17 build artifacts covering usage, CLI options, interactive commands, and examples forshekyld,shekyl-wallet-cli,shekyl-wallet-rpc, blockchain utilities, and debug tools.
Operations
- Added
utils/systemd/shekyld.servicefor Shekyl-native daemon service deployment (/usr/local/bin/shekyld+/etc/shekyl/shekyld.conf). - Updated
docs/INSTALLATION_GUIDE.mdrelated-doc references to include seed operations documentation in the companionshekyl-devdocs set. - Added
docs/BLOCKCHAIN_NETWORKS.mdwith a deep-dive comparison of network models across Bitcoin, Ethereum, Monero, Solana, Polkadot, and Avalanche, and mapped those patterns to Shekyl's mainnet/testnet/stagenet/fakechain usage guidance. - Migrated Shekyl stagenet defaults from legacy Monero ports to
13021(P2P),13029(RPC), and13025(ZMQ), and aligned test/docs references so--testnetworkflows use12029while scripts support overrideable network/daemon variables. - Updated libwallet API helper scripts to call
shekyl-wallet-cli(notmonero-wallet-cli) so test tooling matches Shekyl binary names.
Staking (end-to-end claim-based system)
- Added
txout_to_staked_keyoutput target type for locking coins at a chosen tier (short/medium/long). Outputs carrylock_tierfield enforced at the consensus layer. (Note:lock_untilwas originally stored on-chain but was removed in a subsequent fix — see Bug 13 under Unreleased.) - Added
txin_stake_claiminput type for claiming accrued staking rewards. Claims specify a height range and are validated against deterministic per-block accrual records. - Extended LMDB schema with
staker_accrualandstaker_claimstables plus astaker_pool_balanceproperty for on-chain reward pool accounting. - Per-block accrual logic computes staker emission share and fee pool allocation at block insertion time, with full reversal on reorg (block pop).
- Consensus validation: lock period enforcement on staked outputs, claim amount verification against accrual records, watermark-based anti-double-claim, maximum claim range (10,000 blocks), pool balance sufficiency checks.
- Pure claim transactions (
txin_stake_claim-only inputs) useRCTTypeNullsignatures, cleanly separated from ring-signature transaction validation. - Extended
tx_destination_entrywithis_stakingandstake_tierfields.construct_tx_with_tx_keyemitstxout_to_staked_keyoutputs whenis_stakingis set. - Extended
transfer_detailswithm_staked,m_stake_tier, andm_stake_lock_untilfor wallet-side staking metadata tracking. (m_stake_lock_untilis computed locally fromcreation_height + tier_lock_blocks.) - Implemented wallet2 methods:
create_staking_transaction,create_unstake_transaction,create_claim_transaction,get_matured_staked_outputs,get_locked_staked_outputs,get_claimable_staked_outputs,get_staked_balance,estimate_claimable_reward. - Added simplewallet commands:
stake <tier> <amount>,unstake,claim_rewards. - Added wallet RPC endpoints:
stake,unstake,get_staked_outputs,claim_rewards. - Added daemon RPC endpoint:
get_staking_inforeturning current staking metrics (height, stake ratio, pool balance, emission share, tier lock blocks). - Wired
stake_ratioandstaker_pool_balancein/get_infoto live blockchain state. - No minimum stake amount enforced (matches design doc).
- Fixed compilation errors from
txin_stake_claimmissing in exhaustiveboost::static_visitorpatterns: addedoperator()overloads to the double-spend visitor (blockchain.cpp) and the JSON serialization visitor (json_object.cpp), added JSON deserialization branch for"stake_claim"inputs, addedtoJsonValue/fromJsonValuedeclarations and implementations fortxin_stake_claim, and added Boost.Serializationserialize()free function for wallet binary archive support (cryptonote_boost_serialization.h).
Consensus and mining economics
- Wired Four-Component economics to live chain-state inputs for miner reward
paths:
- block template construction now passes rolling
tx_volume_avg,circulating_supply, andstake_ratiotoconstruct_miner_tx - miner transaction validation now uses the release-multiplier reward path and non-placeholder fee-burn inputs
- tx pool block template estimation now uses the same rolling
tx_volume_avgreward path for consistency
- block template construction now passes rolling
- Added
Blockchain::get_tx_volume_avg(height)andBlockchain::get_stake_ratio(height)(stubbed to0until staking state is consensus-tracked).
Modular PoW
- Added pluggable PoW schema abstractions:
IPowSchemainterfaceRandomXandCryptonightschema implementations- PoW registry-based selection preserving existing behavior by block version
- Refactored
get_block_longhashto route through the PoW schema registry while keeping existing RandomX seed handling and the historical block 202612 workaround. - Updated miner thread preparation to call schema-level
prepare_miner_thread(...)(RandomX prepares thread context; Cryptonight is a no-op).