# Shekyl Changelog ## [Unreleased] ### Changed - **The daemon no longer reads `/checkpoints.json`.** Checkpoints are compiled in and carried by the release binary; the runtime JSON channel (`load_checkpoints_from_json`, the ten-minute reload) is deleted and the compiled-in set is enforced once at startup (`Blockchain::enforce_checkpoints`), rolling a conflicting local chain back or fail-stopping exactly as before. A runtime-loadable pin was a trust channel bypassing the release-carried anchor ([`ARCHIVAL_PRUNED_DAEMON_MODE.md`](design/ARCHIVAL_PRUNED_DAEMON_MODE.md) `PDM-Q-F23` / `PDM-Q5`). An existing `checkpoints.json` is ignored, not refused. In the same PR: `get_curve_tree_path` returns `CORE_RPC_ERROR_CODE_INTERNAL_ERROR` on a leaf, layer hash, or output key the store cannot return instead of zero-filling the sibling data (`PDM-Q-F9`; assembly is `shekyl-fcmp::rpc_path`); the dead ring-era `check_tx_input` / `scan_outputkeys_for_indexes` chain and the write-only `m_scan_table` ring-member pre-fetch are deleted (`PDM-Q-F18`). - **Archival pass countersignature is v2, anchor-bound: `P` signs the decoded 72-byte request header `nonce[32] ‖ anchor_height_le[8] ‖ anchor_hash[32]` followed by `shard_id_le[8]`** under `shekyl/archival-attestation-scheme-v2` ([`ARCHIVAL_SHARD_FETCH.md`](design/ARCHIVAL_SHARD_FETCH.md) `SF-D5` / `SF-D8`, §9.1 (a0)). The anchor is the requester's own chain at `tip − archival_reorg_depth_blocks` (720). Consensus admission of coinbase pass records (`verify_pass_countersignature`, `shekyl_archival_verify_attestation`) takes the block's **validated predecessor height** `h` and the connecting chain's block hashes for `[h − 720 − L, h − 720]` (main chain, or the alt chain above the fork point), requires the record's carried `anchor_height` to fall in that window, rebuilds the transcript with the chain's hash at that height, and verifies under `P`'s bond hybrid key; every pass record is refused while `h < 720 + L`. `L = archival_attestation_anchor_lag_blocks = 4` (**PROVISIONAL**, new consensus constant, `≥ 2` enforced at build; the `shekyl-rpc-types` constants digest is re-pinned for the added key). The v1 nonce-only message `H(block_hash(h−1) ‖ cb_out_key ‖ P ‖ s ‖ E)` and its `shekyl/archival-attestation-nonce-v1` derivation are deleted, not gated (no v1 signature was ever produced). Prunable attestation witness entries are `nonce[32] ‖ anchor_height_le[8] ‖ HybridSignature[3385]`; `attestation_root` commits to `header ‖ nonce ‖ anchor_height ‖ signature`. The FFI verify context drops `cb_out_key`, `cb_out_key_readable`, and `prev_block_hash` and gains `predecessor_height` plus the anchor-hash table (`anchor_hashes_ptr/len`; C++ sizes it via `shekyl_archival_pass_anchor_window`, which writes `(first, len)` or `(0, 0)` and returns `OK` — `BELOW_ANCHOR_THRESHOLD` is a verify verdict only); verdict codes 8 (`CBKEY_UNREADABLE`) and 12 (`PREVHASH_UNPOPULATED`) are retired and never reused; 13 `MALFORMED_ANCHOR_TABLE`, 14 `ANCHOR_OUT_OF_WINDOW`, and 15 `BELOW_ANCHOR_THRESHOLD` are minted. The witness transport cap (`ARCHIVAL_ATTESTATION_WITNESS_MAX_BYTES`, its Rust twins, and the p2p `block_complete_entry` bound) moves 866,568 → 876,808 (`8 + 256 × (32 + 8 + 3385)`); the `shekyl-levin` twin now has a test asserting equality with the retention crate's constant. Binds the signature to the server-parsed `/shard/{id}` so a decoy-route signature cannot be filed as a pass for another shard, and to a chain anchor so a non-colluding witness cannot pre-fetch a signature for a future block (the `P`-side ±`L` gate lands with the serve-side PR). Pre-genesis; no chain state exists under v1. - **The `JoinMarket` bond post carries the persona's serving endpoint** — the raw 32-byte Ed25519 key of its v3 onion service, mandatory, refused on every other kind (`ARCHIVAL_ENDPOINT_UPDATE.md` `EU-D3`) — and the `archival_bond` record stores it (value v7; LMDB schema v13, a v12 datadir refused at open; pre-genesis, delete and resync). The witness reads it from the drawable snapshot through the record (`EU-D4`). **`EndpointUpdate` (bond-post kind 4) is REJECTED** (Rick, 2026-09-13): a bonded persona's endpoint never changes; a new address is a new persona via Release and a fresh `JoinMarket`. The kind-4 half built in PR #717 was excised before merge, and the `ARCHIVAL_P_DERIVE_V1` retirement authorized 2026-09-12 is withdrawn (decision log, 2026-09-13). - **Tor-zone wire-observer cover is operator non-exit relay posture, not the protocol carrier** ([`TOR_COVER_POSTURE.md`](design/TOR_COVER_POSTURE.md), TRC; RULED 2026-09-04, recorded 2026-09-12). Default Tor nodes no longer receive protocol-level constant-rate cover. The carrier remains for encrypted zones other than Tor. Operator recommendation: [`TOR_RELAY.md`](TOR_RELAY.md) — the daemon's Tor client (SOCKS and overlay inbound) must be **that same non-exit relay process**; a sidecar client Tor, including the default managed ephemeral instance, is uncovered. This is a reduction in what the protocol guarantees: cover moves from a protocol mechanism to an operator posture. - **Cover-traffic ~42 GB/month ceiling signed off** after the `COVER_TRAFFIC_RESTORATION.md` §3.1c three-arm measurement (seedusw, 2026-09-12). Defects 1–3 not observed. Honest Tor-only observed ~18–20 GB/month equivalent (7128–7809 B/s), under the two-channel mean of 8192 B/s. The 42 GB figure remains the dual-zone four-channel *ceiling*, not the typical node bill. The §3.1c counter reads the live 29-byte Levin header (`utils/carrier/count_windows.py`). - **Short-term block-weight surge factor is 4**, sourced from `config/consensus_constants.json` as `block_weight_short_term_surge_factor`. The effective-median clamp that consumes it lives in `shekyl-economics::effective_median` (`shekyl_effective_block_weight_median`); C++ gathers the window medians and does not multiply `S` itself. A node holding the inherited ×50 accepts a block a node holding 4 rejects as over-weight. Pre-genesis, so no cutover. **Changing `S` moves no fee number:** the wallet's estimate reads the long-term effective median directly, and the surge clamp applies to the short-term median, which never reaches that path. - **The cold-authority selector for bond-posts is one Rust predicate.** `requires_cold_authority(post_kind, bond_debit)` is an exhaustive truth table — `Release` always, `HoldingsUpdate` iff `bond_debit > 0`, `JoinMarket` / `Rebond` never — and `cold_authority_pin` composes it with the unchanged `debit_auth_pin`. Consensus reaches the composed gate as `shekyl_archival_cold_authority_pin`; the old `shekyl_archival_debit_auth_pin` export is deleted, not aliased. Behavior is byte-identical on every input the arms pass today. A predicate-false call refuses with `NOT_COLD_AUTHORITY_POST` (51, `INTERNAL_FAILURE`) before the keys are compared. Bond-post operator strings now live in `bond_post_err_cstr` (the admission-code pattern); the C++ switch is gone, and the connect helper is marshal + one log line. `ColdAuthorityError` is `NotAColdAuthorityPost | Pin(DebitAuthError)`. - **Relay admission is a lookback-min over the last six floors, at zero slack; the relay floor follows the raw correction `C`; the fee ladder is unrounded (FL-R20 / FL-R22 / FL-R23, PR B).** A transaction is admitted when `fee ≥ mask_round_up(weight · min F(h′−k))` over `k = 0..5`, where `F(h) = R·C(h)·w_ref/M(h)²`. Monero's 0.95 and the 2 % acceptance buffer are gone — they insured the quote-to-broadcast gap probabilistically; the lookback insures it by identity, so a quote taken at height `h` and paid exactly is admitted at every node whose tip is within five blocks. The wallet therefore pays the served rung and nothing more (FL-R22). **What changes for a user or operator.** The economy tier IS the relay floor now — one function computes both (`fees[0] == get_current_fee_per_byte()` by call, not by clamp). `standard` is `4F` exactly. No rung is rounded up to two significant digits any more, so quotes are the arithmetic's own answer: at 10 SKL and the 300 kB zone, `[340, 1400, 67000]` becomes `[333, 1332, 66666]`. The `grace_blocks` RPC parameter no longer affects any tier (the estimate no longer builds a graced short-term median); its deletion from the wire is FL-R26, a separate change. The wallet's absolute fee cap moves with the unrounding, 220,000,000 → 218,453,333 atomic-units/weight — the same structural bound, unrounded. **Two defects fixed on the way.** The fee-quote path (`fee_query`) under-quoted by one varint byte's worth of rate whenever a fee crossed a `2^(7k)` boundary — `converge_fee` ran two passes where the fixed point needs three — which the 2 % buffer had been hiding; and a shared test double returned an empty long-term-weight window, so `median()` read uninitialised storage (a garbage median of ~9.5e18, order-dependent), which the old grace-zero insertion had been masking by accident. Relay policy only; `kept_by_block` is exempt and there is no consensus fee floor. - **The fee estimate returns three tiers, not four, and `CORE_RPC_VERSION` is 3.30 (FL-R25).** `get_fee_estimate.fees` was a four-slot array carrying three priced rates: slot 2 duplicated slot 1. The duplicate existed so a caller asking for Monero's `Elevated` priority would pay the standard rate rather than self-marking on a rung of its own — a sound anonymity argument whose premise was not: `Elevated` had no production callers, in this repo or the GUI, so the cohort it protected was empty. Nothing that speaks Monero's RPC can reach this chain in any case; the address format, transaction format and proof system all differ. **What changes for a caller.** `fees` has three elements — `[economy, standard, priority]` — and a reply carrying four is now a parse error rather than a shorter answer read with the priority rate in the wrong position. Requesting priority `3` used to buy the standard rate through the duplicate slot and now buys the priority rate; the only ways to ask for `3` were the `Elevated` variant, which is deleted, and a custom priority of `3`, which asks for a rung above normal and now gets one that exists. Daemon and wallet ship together and there is no deployed network, so no migration applies. - **LWMA-1 genesis difficulty is 400** (`daa_genesis_difficulty` in `config/consensus_constants.json`). The first 90 blocks still share one constant until the LWMA window fills; 400 is the testnet calibration against `skl-miner-test` (4 threads), replacing the zawy12 example value of 100 that produced ~20s blocks on that miner. This is a consensus-constant change: `CONSENSUS_CONSTANTS_DIGEST` moves, alpha.8 nodes will refuse an alpha.9 daemon on the digest handshake, and any chain that already mined heights 1–90 at difficulty 100 must wipe and resync. Genesis block hash is unchanged (genesis PoW still uses difficulty 1). Retune is that JSON key. - **Archival serving route is `/shard/{id}` (`RF-R1`).** The throwaway `/x-provisional/v0/shard/` path is discarded. The status/header privacy contract (one 404 for every complete-head miss; two personas header-identical) is unchanged. ## [3.1.0-alpha.8] - 2026-09-10 ### Added - **Daemon logical-state digest v0 (DRS-P0d).** A layout-independent cSHAKE256 digest of production LMDB's core chain (height-ordered block hashes), spent-key set, and live curve-tree root, read under one LMDB snapshot. Two engines with the same logical state agree regardless of B-tree layout. The digest is a regression oracle except on CHECKED-CONFORMANT census rows, where a match is correctness evidence (CSR-3). Archival journals are a named exclusion — a core-only match is not archival parity (`DAEMON_REDB_STORE.md` §7.1.1). - **The daemon publishes an ephemeral per-boot onion address by default (PWD-E7).** When a pinned Tor binary is installed (SP-T0c gate: `SHEKYL_TOR_BINARY` → beside the executable → `/opt/shekyl/-/` staging → `PATH`), `shekyld` spawns a managed tor, mints a v3 onion key in memory, publishes it with `ADD_ONION Flags=DiscardPK`, and serves overlay inbound on a fresh address each boot — no persisted secret, no durable identifier, and no reused tor `DataDirectory` (a unique 0700 subdirectory of the daemon config folder is created per boot and wiped on teardown, so entry guards cannot join one boot's onion to the next). The new `rust/shekyl-tor-control-daemon` crate owns the posture; the daemon and wallet tor supervisors share only the neutral protocol crate and never share a tor instance, config, data directory, or log sink (PWD-E9). Opt out with `--no-ephemeral-tor`; configuring `--anonymous-inbound` or a tor `--tx-proxy` yourself also makes the default yield. `--anonymous-inbound`'s help text now states that taking it opts into a stable, durable onion address. A tor start failure logs loudly and the daemon continues without overlay inbound; it never aborts. - **Wallet and daemon console refuse a daemon that is not this build.** On first request they compare the four-axis identity tuple in `get_version` (wire version, consensus-constants digest, network, genesis) against this binary's compiled values and refuse a mismatch in operator language. Comparison is one function in `shekyl-rpc-types`; a down or not-ready daemon is not cached as a contract mismatch. `CORE_RPC_VERSION` 3.28 → **3.29**. The genesis axis compares the daemon's block-0 id to this build's per-network pin (`GENESIS_ALLOCATIONS.md` / `mining_parity`); a remint of genesis updates the pin in the same change. A daemon reporting a foreign DAA target no longer produces a console warning: the digest handshake is the instrument, and figures still use the generated `T`. - **Every ignored regtest e2e test now has a CI disposition.** The live-daemon gate loop (`scripts/ci/run_live_daemon_gates.sh`) arms all fourteen fast wallet/staking/curve-tree e2e gates per PR, runs the two heavy consensus gates (depth-3 FCMP++ spend, emission claim) in a new nightly lane (`nightly-live-daemon-slow.yml`, `GATE_LANE=slow`), and records the one deliberate non-gate (a fixture regenerator); the undecided count is ratcheted to zero, so a new `#[ignore]`d regtest test fails CI until it is armed (observed red first), scheduled slow, or decided dark with its reason recorded. Tree-wide, every `#[ignore]` must now carry its reason in the attribute (`scripts/ci/check_ignore_reasons.sh` on the grep-gates run). - **Staking rewards now claim automatically.** The engine runs a cadence driver (`ENGINE_CADENCE_DRIVER.md`) that submits the emission-claim transaction once each reward epoch settles — no manual step, and the `claim`/`claim_rewards` RPC methods remain REJECTED in the wallet-RPC contract. Rewards below the compiled-in fee floor (`shekyl_economics::EMISSION_CLAIM_FEE_FLOOR`) are held and aggregated until they cover the fee; anything still unclaimed at its claim-window floor is evaluated once and forfeited loudly (operator alarm). A background claim never contends with a user-initiated send: the claim leg yields whenever user work holds the pending lock. - **Stuck-transaction watchdog and serving liveness run unattended.** The same cadence driver fires the submit-lifecycle escape ladder on chain progress (a pending transaction can no longer stall forever just because no RPC poll arrived), re-arms hidden-service serving whenever the obligation exists and the task is not live (covers both failed-at-open and died-later), and raises a `ChainProgress` operator alarm when the observed tip stops advancing — worded to cover both "chain stalled" and "your daemon is unreachable." - **Wallet RPC liveness gate (`ci/wallet-rpc-liveness`).** Sibling of the daemon's `ci/rpc-route-liveness`: `wallet_rpc.yaml`'s `x-shekyl-method-registry` is now CI-enforced in both directions — every SPECIFIED method has a dispatch arm and a production consumer outside the server crate; every REJECTED/RESERVED method has no arm; every arm has a registry row. The reverse direction is what turns "`claim` stays REJECTED in the contract" from a note into an invariant: a handler added while the registry still says refused now fails CI instead of shipping. Grep-cheap (no toolchain), fails closed on empty extraction (rule 47). ### Changed - **Consensus: the transaction-volume operand is the exact window mean, not its integer floor (FL-R24, `FEE_LADDER_DERIVATION.md` §11.6 PR A).** The block reward's release multiplier `M_r` and the fee burn `b` were computed from `tx_count_sum / 720` truncated to an integer, which quantized a consensus operand to ticks of `1/V` (2–3 % of `M_r` under Poisson noise, §11.7 FL-E1) and pinned it to the 0.8 rail on chains whose mean sat just above 40. The daemon now hands Rust the pair `(tx_count_sum, blocks)` (`Blockchain::get_tx_volume_window`, renamed from `get_tx_volume_avg`; `shekyl::tx_volume_window`) and `shekyl_economics::TxVolume` divides once against the baseline. Every block whose 720-block window mean is fractional pays a different reward than before: mid-curve at mean 40.5 the paid reward moves 819 200 000 000 → 829 440 000 000 (+1.25 %), `burn_pct` 223 606 → 225 000 (SCALE 10⁶). Pre-existing reward KATs are unchanged because all of them feed whole-number means; the new KATs pin both values. FFI: `shekyl_block_reward`, `shekyl_calc_release_multiplier`, `shekyl_calc_burn_pct` and `shekyl_fee_correction_quantized` take `(tx_count_sum, window_blocks)` in place of one `tx_volume` scalar. Pre-genesis; no chain to migrate. - **Levin ingress rejects unknown commands and unknown flag bits.** A dispatch (`REQUEST`/`RESPONSE`) whose command is not a `DefinedCommand`, or any flag bit outside the five defined flags, is connection-fatal at ingress. Cover traffic (neither `REQUEST` nor `RESPONSE`, typically command 0) is still admitted and bounded by the packet limit. The table and discriminator live in `shekyl-levin`; C++ `handle_recv` is a marshaling shim (`shekyl_levin_ingress_admit`). `NOTIFY_NEW_BLOCK` (2001) and `COMMAND_PING` (1003) are unknown dispatch; the sole block announce is 2008 `NOTIFY_NEW_COMPACT_BLOCK`. Cap movements vs the inherited table: 1007 support-flags 4096 → 256; 2003 get-objects request 2 MiB → 5056 (hash-list derivation); 2006 request-chain 512 KiB → 1_200_256 (hash-list derivation, the inherited envelope could not fit `BLOCKS_IDS_SYNCHRONIZING_MAX_COUNT`); 2007 / 2008 / 2009 / 2010 keep their inherited envelopes (4 / 4 / 1 / 4 MiB) rather than taking the packet limit; 2002 and 2004 stay at the packet limit until PWD-B12 and the 2004 byte budget land. - **A peer is dropped only when the rejection is attributable to the sender (PWD-B7).** The inherited `m_no_drop_offense` flag meant droppable by *absence*, so our own pool-bookkeeping failures and storage exceptions severed innocent connections. The drop decision is now a typed tri-state verdict in `shekyl-peer-policy` (unclassified / policy-or-state / internal-failure / attributable-form); only the last severs. C++ writes through `shekyl_drop_verdict_classify` / `reject_form|state|internal` at the failure site; unclassified does not sever. `check_tx_inputs` classifies each return itself, including chain-state arms (spent key image, missing/too-recent reference block). `add_tx` does not promote an unclassified inner failure to form. Mixed archival FFI codes (serve-credit, bond-post, admission, debit-auth) classify in Rust; C++ writes the returned byte. Our-state and marshal faults do not sever. The announce-size check declines without disconnecting; a block-sync prepare failure still flushes the failed span so sync can recover. Malformed input still drops. - **Block ingest no longer drops on failed-set-ness (PWD-B7 block twin).** `block_verification_context`'s bag of bools is an opaque `m_outcome` (`BlockIngest` in `shekyl-peer-policy`) plus the same `m_drop_verdict` slot the tx path uses. Announce and sync ask `shekyl_block_announce_action` / `shekyl_block_sync_action`; a rejected block does not sever unless the drop slot severs. Missing-txs re-requests compact transactions. C++ writes through `record_block_ingest` / `reject_block_*` (Rust pairs the drop slot) and never switches on a classification byte. - **`docs/FOLLOWUPS.md` genesis-hold triage.** Every pre-genesis row got a disposition pass: 44 resolved/overtaken/duplicate/won't-fix rows removed (git history is the archive), 3 rows reclassified to post-genesis with named blockers (fee-bump, wallet-decryption MFA, network-filesystem wallets), and the delivered DRS-P0a–P0c legs trimmed out of the DRS-P0 row, leaving P0d as the open blocker. Process-only; no code change. - **Internal "Unbond" vocabulary renamed to "Release."** The terminal bond exit is now called Release everywhere the code and living docs speak about it (matching `RELEASE_COOLDOWN_EPOCHS`), across Rust (`PendingRelease`, `submit_release`, `stake_engine/release.rs`), the C++ submit path, CI scripts, and design docs. Nothing user-visible or on-wire changed: the `BondPostKind` wire discriminant stays `2`, and the user surface stays `unstake` / `collect_unstaked`. The wallet-file schemas that carried renamed field names bumped versions (`PENDING_POST_VERSION` and `PSCAN_STATE_VERSION` 9 → 10; pre-genesis, no migration). The LMDB journal name `archival_bond_unbond_log` and its direct C++ carriers are deliberately excluded — carried by the DRS redb port per the `docs/FOLLOWUPS.md` entry. Historical records (decision log, reconciliation registry, this changelog's released entries) keep the old name as the record of what was. - **The daemon and the Rust port now admit the same `tx_extra` tag set, and an unparseable `extra` is refused rather than skipped.** `shekyl-wire` had long documented merge-mining (`0x03`) and "mysterious minergate" (`0xDE`) as outside the Shekyl genesis grammar and rejected them, while the C++ parser still accepted both. Two parsers disagreeing about which transactions exist is the defect; both tags are now deleted from the C++ variant (rule 60), along with the `add_aux_pow` RPC method that was `0x03`'s only producer, its schema, dispatch entry and python client, and `merge_mining.{h,cpp}`. The tag bytes stay retired in a comment so a future tag cannot reuse a meaning older software would parse differently. **`0x0B` was kept, and that is the finding.** The archival attestation tag had been grouped with the other two as inherited dead code on a "no producer found" basis. It has a live consensus reader deciding `headers_readable` during attestation verification; what it lacks is a *producer*, which is an unfinished feature, not legacy. Deleting it would have removed a consensus reader. It is now modelled in `shekyl-wire` instead, and its missing producer is filed on its own terms. **What that unlocks.** With the tag sets equal, `validate_context_free_pruned` no longer skips the CEN-I19 PQC field-shape rule when `extra` fails to parse — a tolerance that existed only because failing hard would once have refused transactions the daemon accepts. The flip exposed a fixture that could never have existed: a two-output spend whose `extra` was a truncated varint, which no parser accepted and which survived only because the parse was conditional. The wallet scanner no longer re-implements the tag grammar: `shekyl-scanner` parses `extra` through `shekyl-wire`, so it cannot admit the retired tags or stop at a genesis tag it does not consume. - **Wallet-envelope test vectors renamed by oracle tier; pinned vectors rebuilt on a real derived address.** Per the new `50-testing.mdc` vector-oracle rule (external / independent / self-pinned; only the first two are KATs), the `WALLET_FILE_FORMAT_V1` sealed fixtures are now named as the self-pinned drift tripwires they are, their regenerator refuses to run without a decision-log citation (`SHEKYL_PINNED_REGEN_DECISION`), and the published vectors under `docs/test_vectors/WALLET_FILE_FORMAT_V1/` were regenerated so `expected_classical_address` derives from the sealed seed and `seed_format` is a production-valid wire byte. A true tier-2 KAT for the §2.6 wrap-key derivation (raw HMAC per RFC 5869 against the spec's byte-exact labels) lands alongside. Wire format unchanged. (Decision log 2026-09-07.) - **Wallet capability collapsed to `FULL`-only; cold signing rejected permanently.** The `ViewOnly` capability is REJECTED (no product use case; FCMP++ has no view-key chain scan) and `HardwareOffload` is DEFERRED with zero code symbols (the v1 layout was a guess against no real device; the future layout will be designed against one). The wallet envelope now seals and opens mode byte `0x01` only — bytes `0x02`/`0x03` are RETIRED and `0x04` RESERVED in `WALLET_FILE_FORMAT_V1.md` §2.3, all refused fail-closed as `UnknownCapabilityMode` (new splice-tamper tests pin the refusal). Cold signing (`export_unsigned` / `submit_signed` air-gap flow) is rejected permanently, superseding the A4 post-genesis deferral: an FCMP++ witness needs the live curve tree, so the offline half cannot deliver the isolation it claims; cold storage is the seed phrase. API deltas: wallet-RPC error `-29005` (`CAPABILITY_FORBIDS`) is RETIRED — unreachable with one capability, the numeric code is never reused; `wallet_rpc.yaml` gains a machine-readable `x-shekyl-method-registry` (SPECIFIED / RESERVED / REJECTED per method) and `x-shekyl-error-ranges`; the `capability` field on wallet handle responses always reads `"FULL"`. Envelope wire format for existing `FULL` wallets is unchanged. (Decision log 2026-09-07; rule `23-disposition-visibility`.) - **`money_supply` is now `emission_curve_asymptote` — the name says what the number is (FL-R15).** Under the perpetual tail signed as FL-R12′, gross issuance passes `2³² · 10⁹` and keeps going: the accumulator runs *through* that value rather than stopping at it. The old name asserted a ceiling the code no longer enforces, and that mismatch is what produced FL-R16 — two of the constant's four jobs turned out to be *assertions that the cap holds*. **What changed, for anyone reading a config or a header.** The `config/economics_params.json` key is `emission_curve_asymptote`; the generated C++ macro is `SHEKYL_EMISSION_CURVE_ASYMPTOTE` (rule 93 prefix, replacing the inherited unprefixed `MONEY_SUPPLY`); the Rust surface is `EconomicParams::emission_curve_asymptote` / `params::EMISSION_CURVE_ASYMPTOTE`. **Behaviour is unchanged** — the value, the emission arithmetic, and the parameter digest are all identical, and the economics simulator's `--fee-ladder` and `--stage2` reports are byte-for-byte the same across the rename. **If you have an out-of-tree `economics_params.json`**, rename the key: the build fails loudly on a missing key rather than defaulting. - **`ActivityMetric::new` no longer rejects a supply past the asymptote (FL-R16b).** The constructor validated `circulating_supply ≤ MONEY_SUPPLY` as a structural invariant. That invariant is false under the perpetual tail — a chain state past the asymptote is legitimate, not impossible — so the check and its `CirculatingExceedsSupply` discriminator are removed. The remaining structural invariants (`total_staked ≤ circulating_supply`, zero-at-genesis) are unchanged. - **Peerlist trust is earned in-process: nothing restored from disk is trusted, and `--add-peer` is a candidate rather than a trusted peer.** White-list membership now means exactly *"this process dialled it and it answered"*. On startup every persisted address is loaded as a **gray** candidate and earns white by a successful outbound dial; `--add-peer` entries enter gray for the same reason, having never been dialled. **Why it is security-relevant.** A persisted white list made white mean *a file asserts that some earlier process verified something*. White entries are dialled in preference to gray **and are the only ones gossiped onward**, so a supplied or stale datadir — a pre-synced download, a restored backup, a container volume, a shared mount — made a node both a preferential dialler of, and an amplifier for, whatever the file contained. Encrypting the store would not have fixed this: encryption is a privacy mechanism for the peer graph, and verification is what supplies trust. **Operator-visible effects.** A wrong or unreachable `--add-peer` address is **no longer gossiped to other nodes** — gray entries are never disclosed, so a typo stops propagating immediately instead of being handed to every peer that syncs with you. Its **removal is eventual, not immediate**: a failed refill dial only records the address in the recently-failed cache, and eviction waits for the periodic housekeeping probe to draw that entry (one random gray peer per zone per cycle) and fail. A bad address can therefore survive many failed dials and restarts; what changes is that it no longer spreads, and no longer occupies a preferentially-dialled slot. Seed contact is keyed on holding **no candidate at all** rather than no trusted one, so a restarting node dials its stored pool instead of visiting a seed first. The first start after upgrading is a cold-ish start: the persisted store's version is bumped, so the previous file is dropped whole. **One capacity change, stated because it is not obvious.** White and gray had separate caps (1000 + 5000). One trust class means one cap, so a node saturating both now persists up to `P2P_LOCAL_GRAY_PEERLIST_LIMIT` rather than the sum; the entries dropped are the least recently seen. - **Block reward: one Rust owner, and the composition the FL round signed (FL-R12′).** The paid reward is now `max(M_r · curve(remaining), TAIL) · penalty(x)`, computed by `shekyl-economics::paid_block_reward`; C++ marshals to it and computes nothing. The ordering is the consensus-visible part: the release multiplier applies to the **curve** and the tail floors the result (at a perpetual tail there is nothing to pace, and a multiplied floor would pay least exactly when fees are lowest), while the weight penalty applies to the **paid quantity, after the floor** (composed before it, the penalty would be dead at the tail permanently, and there is no post-tail era). - **The supply cap is retired and the accumulator runs through the asymptote.** `already_generated_coins` no longer saturates at the emission-curve asymptote — a past-asymptote state is a legitimate perpetual-tail state rather than an error — which closes the divergence where the estimator and the relay floor dead-lettered at exhaustion. The persisted width stays `u64` (FL-R14), guarded by a build-time assertion on the ≈89,750-year headroom. - **Fee ladder: three tiers, state-computed (FL-R17).** The daemon serves `economy` / `standard` / `priority` from `shekyl-economics::corrected_fee_ladder`, scaled by the whole volume-dependent correction `C_q = Q_ceil((1−σ)·M_r/(1−b))` on the M_r-neutral operand. The `Fh` main arm is unconditional (the inherited surge discount is gone), and the served economy rung is clamped up to the relay floor so a conforming wallet's quote can only err toward acceptance. **Wire shape is unchanged** — the vector still carries four slots, with slot 2 mirroring `standard` as a bridge until the RPC cutover. - **The served correction carries no daemon-local state.** It is the plain pow2 ceiling snap of the correction at the queried height, so every node derives the same rate there; a restarted and a long-running daemon cannot quote differently. The pow2-boundary hysteresis the design round ruled is built and tested in `shekyl-economics` but is **not on the served path yet**: a remembered value makes the rate depend on the process's query history, and a one-step seed from the previous block inverts it. FL-R3 rules that the band stays and is restored, via a grid-anchored previous value that keeps it derivable from chain state; that shape is a design change and lands in its own round. Until then the served correction is the plain snap. - **Wallet fee-rate ceiling raised to a structural bound.** `absolute_fee_rate_cap()` is now derived with every factor at its own extreme (220,000,000 atomic/weight) instead of at the genesis point. The previous 28,000,000 value sat **below honest daemon quotes** from about year 3 and would have refused correct snapshots. The swept peaks are 91,000,000 at ≈ year 7 on the neutral accumulation and 98,000,000 at ≈ year 8 on a dormant-then-busy trajectory — history matters, because a slower-emitting past leaves a larger `R` at the same height. **Neither is the reachable maximum**: arbitrary volume paths are uncountable, so the sweep is a floor under the bound's adequacy, not a proof of tightness. That is why the cap is structural rather than swept. ### API - **`shekyl_block_reward` takes `tx_volume_avg`**, and the release multiplier composes inside the one owner rather than being applied by the caller; `shekyl_apply_release_multiplier` and `shekyl_cap_reward_to_remaining_supply` are gone. New exports `shekyl_fee_correction_quantized` and `shekyl_corrected_fee_ladder` carry the ladder. `shekyl_corrected_fee_ladder` returns `0` on success, `-1` for a null out-pointer, and `-2` for scalars the rungs cannot form in 128 bits — no chain state reaches the last one; it exists so a corrupt caller gets a status instead of an abort across the ABI (rule 40). `shekyl_fee_correction_quantized` is total. `Blockchain::get_dynamic_base_fee_estimate_2021_scaling` gains a `c_q` parameter. ### Removed - **Levin `return_code` is deleted (PWD-B5).** The bucket header is **29 bytes**, not 33: the inherited signed `i32` at old offset 21 is gone, and flags follow command immediately. Notifications always wrote `0`; the three remaining invokes put a handler `int` on the wire. Application callbacks already tested `code < 0` (local timeout `-4` / destroyed `-3`). The epee invoke wrapper still treated `code <= 0` as failure because it expected the handler's positive `1`; that `1` left with the field, so a `RESPONSE` now delivers `LEVIN_OK` (0) — a reply arrived — and the wrapper fails only on `code < 0`. Handshake failures hang up after sending Levin "success." Application success or failure is the payload or the close. A later NACK is a command body, not a header field and not a flag bit (PWD-B4 rejects unknown bits). Local invoke-callback `int` stays for timeout/destroyed — that is API, not wire. No protocol version moves: `SHEKYL_PROTOCOL_VERSION` denotes the crypto era, and there is no wire-command-set version to bump. Pre-genesis: no compatibility with 33-byte peers. The four `SPEC_VERIFY_COST` `msg_bytes` pins follow the 29-byte header (−4 B each); hop rounding (175 / 453) is unchanged. - **One block-propagation path, not two (PWD-B6).** `NOTIFY_NEW_BLOCK` (2001) is deleted; command **2008** is the sole block announce. The two were already one code path — 2001's handler forwarded into 2008 — and nothing ever sent 2001. Command ids **2008 / 2009 are unchanged**. The identifiers are `NOTIFY_NEW_COMPACT_BLOCK` / `NOTIFY_REQUEST_COMPACT_MISSING_TX` (Rust: `NewCompactBlock` / `RequestCompactMissingTx`): `FLUFFY` collides with Dandelion++ stem/fluff (`dandelionpp_fluff` on 2002) and names a Monero rollout this chain has no history of. Compact, not "just block": 2008 is header-first (relay clears `b.txs`); full blocks still travel on `NOTIFY_RESPONSE_GET_OBJECTS` (2004) during sync. `P2P_SUPPORT_FLAG_FLUFFY_BLOCKS` (0x01) goes with 2001; `0x01` is left unassigned. Advertised flags live as `shekyl-levin::SupportFlags::ADVERTISED` (zstd only), pinned against the C++ macros by `check_levin_constant_parity.sh`. The obsolete `--fluffy-blocks` CLI flag is deleted. No protocol version moves: `SHEKYL_PROTOCOL_VERSION` denotes the crypto era, and there is no wire-command-set version to bump. - **The anchor peerlist mechanism is deleted whole** — the persisted anchor section, `anchor_peerlist_entry`, its container and manager methods, the anchor dial arm, and `P2P_DEFAULT_ANCHOR_CONNECTIONS_COUNT`. Anchors existed only to carry peers across a restart, and peerlist trust no longer crosses that boundary. Within a session the mechanism could not produce a connection at all: an entry was in the anchor set only while an outbound connection to it was open, and the dial path skipped every entry it already had a connection to. It also under-delivered against its own constant — the whole persisted set was drained and destroyed to buy at most one connection. **Consequence for operators:** the anchor exemption in the sync-slot drop logic went with the mechanism. `should_drop_connection` still refuses to drop a peer that is not striped, one carrying the stripe we need next, one usable for pruned-block sync, or one holding the next unpruned block — what was removed is the *unconditional* exemption an anchor connection had, not connection protection in general. - **`--hide-my-port` is gone, as an option and as a capability; whether this node advertises a port is now derived.** The flag expressed something the node can determine for itself, and its default meant a reachability downgrade required no conscious act. `get_local_node_data` now announces the listening (or `--p2p-external-port`) port only where a peer could actually reach us on it: the zone must support the back-ping that verifies the claim, and we must accept inbound connections at all. **This also fixes a divergence** — `check_incoming_connections` has always asked the second question, while the announcement site asked only whether the flag was set, so a node run with `--in-peers 0` announced a port that refuses every connection it attracts. Operators wanting no inbound use `--in-peers 0`, which now suppresses the announcement by derivation; operators wanting no p2p participation at all should not run a daemon. The flag never did anything on Tor or I2P, where `m_can_pingback` is false by construction. NAT'd ports are handled naturally by the revised p2p rather than by an operator flag. - **PWD-B8: a p2p timer that was never driven, and two lineage-dead wire structs.** `m_bad_peer_checker` (`once_a_time_seconds<43>`) had exactly one occurrence tree-wide — its own declaration — and `on_idle` never called it; its orphaned `m_bad_peer_check_lock` goes with it. `connection_entry_base` and its `connection_entry` typedef had **zero** references anywhere, and `network_address_old` only two, both in the debug size printer. Nothing observable changes, because none of it was reachable; the value is that a cadence constant no code reads no longer reads as a cadence to anyone auditing the file. ### Fixed - **Build: shared internal libraries are refused; Debug builds link statically.** `BUILD_SHARED_LIBS=ON` (the inherited Debug default) gave every internal `.so` that links the Rust FFI archive its own copy of the Rust image, duplicating process-global Rust state behind the single-image `nm` gate's back. Observed as a Debug `shekyld` refusing every emission claim as not-yet-finalized: `Blockchain::init` armed the regtest settlement-epoch schedule in `libcryptonote_core.so`'s copy while the RPC verifier read the executable's unarmed copy. Configure now fails with the reason (`V3_WALLET_DECISION_LOG.md` 2026-09-10). Release and CI configurations were already static and are unaffected. - **Daemon RPC: a Phase C submit rejection names its leg.** The verifier returns a `VerifyReject` pairing the wire-cause (`Malformed` / `StaleRoot` / `DoubleSpendConflict`) with the failing check; a cause without a reason is unrepresentable. The submit engine logs both at `info` once. Previously a `Malformed` verdict left no daemon-side trace of which battery leg refused. The wallet logs at `error` when a transaction it built fails its own local round-trip parse — a build-path defect, never a daemon verdict — since that outcome is otherwise indistinguishable from a daemon refusal. - **P2P: a node no longer retries outbound to its own public listen address.** Foundation seeds sit in a shared hardcoded list; public zone binds `0.0.0.0` and leaves `m_our_address` unset, so a seed TCP-hairpinned itself, the handshake nonce dropped the connection, and `start_outer_call` logged ERROR on a ~13s cadence. Outbound now skips a candidate whose port is our listen / advertised-external port and whose host is loopback or a local interface address. Detection stays the nonce (PWD-T1); this is the wasted-dial half of PWD-E3(c). Same-host other-port (mainnet + testnet on one VPS) is still dialable. - **Consensus: the `tx_extra` PQC fields have a shape rule, and the storage fail-open that hid its absence is gone (CEN-I19, S1).** A transaction whose `0x07` leaf-hash field was missing, short, long or unparsable was accepted at connect and the DB zero-filled `h_pqc` for the uncovered outputs — a leaf whose post-quantum binding was to nothing (unspendable) and a leaf set a faithful port would not have stored; the `0x06` KEM-ciphertext field had the same gap, leaving a recipient unable to ever see or spend the payment. Ruled by Rick 2026-09-05: with `n = vout.size()`, exactly one `0x06` of `1120·n` bytes and exactly one `0x07` of `32·n` when `n > 0`, neither when `n == 0` (serve-credit transactions), and duplicates are rejected because first-match parsing let the same bytes mean two things. The rule lives in `shekyl-wire` and reaches the daemon through `shekyl_tx_extra_pqc_field_shape`; the C++ adapter runs in `check_tx_semantic` (relay and block, no `kept_by_block` exemption) and `prevalidate_miner_transaction`; the DB collector aborts on the same shape instead of zero-filling. Red-first: every vector observed accepted at all three gates before the rule. `GENESIS_TX_WIRE_FORMAT.md` §9.6a had both lines wrong in mirrored ways (`0x06` "per output"; `0x07` "not self-describing") — corrected under refuted-not-superseded with the serializer lines that refute them. Every producer already emits both fields at the full length, genesis included, so no conforming transaction changes. - **Consensus: the block header's `curve_tree_root` is now checked at admission against the tip root, before the block is added (CEN-B5, S1).** `handle_block_to_main_chain` compared the header against the tree root read *after* `add_block` — the post-drain state — while `create_block_template` fills the header from the root *before* it, the state at the block's own height that the per-height record, the wallet client and the CT-2 KAT all name. The two agree only when nothing matures at the block, so every non-FAKECHAIN chain would have rejected block 60 (the genesis coinbase matures there) and halted; the FAKECHAIN skip around the check hid it from every Blockchain-level test and every `--regtest` run. The compare now runs with the other header checks, after proof-of-work and before the miner-tx prevalidation, and a mismatch rejects the block outright — the post-add compare with its connect-then-pop arm is deleted. Observed red → green by the first non-FAKECHAIN Blockchain fixture in the tree (`curve_tree_header_root_check.cpp`: TESTNET, fixed difficulty 1, real LMDB, block 60 rejected before and connected after). The FAKECHAIN skip around the check is deleted: the core_tests generator now computes real roots by replaying its own recorded chain through the Rust curve-tree client, exposed to C++ as `shekyl_curve_tree_replica_*` in `shekyl-ffi` (the client's parity with the daemon's store was KAT-pinned; every generated block that connects is now a live cross-implementation check at every height), and the fake-DB test doubles report the empty-tree sentinel instead of zeros. `CurveTreeClient::next_block_root` — the one-past-tip read a header producer needs — is added and pinned to the recon fixture. `shekyl-ffi` gains the `shekyl-curve-tree` dependency, which brings `redb` into the daemon image ahead of the daemon redb store that needs it anyway. `FCMP_PLUS_PLUS.md` §5 now states the state the header commits to. Register: CEN-B5 stays DIVERGENT until re-reviewed at the merged sha; both its divergences are fixed. - **Consensus hardening: the curve-tree leaf collector aborts instead of silently dropping an output (CEN-L11/L12).** Three arms in the DB-side leaf collector discarded an output on a `continue` or an unchecked construct verdict, and a dropped output is deterministically unspendable with no verify-time twin. All three are unreachable for any validated transaction — CEN-H12/F8's sole-output-type whitelist, four `outPk.size()` gates, and the canonical/prime-order point gates on output keys and commitment masks — so they now abort rather than skip, each naming the gate that forecloses it. Those gates carry reverse pointers warning that relaxing them surfaces as a connect-time abort, and a Rust falsifier asserts they accept only what the leaf builder can encode. The FOLLOWUPS row that described this as a live fund-loss path is corrected: it was latent. **Both rows are now promoted CHECKED-CONFORMANT by the follow-up re-review at the merged sha** — register 99 / 1 / 2 over P0f's 102-row snapshot, CEN-B5 the sole DIVERGENT row. For anyone grepping history: PR #609's merge commit carries the title *"register 99/1/2"*, written before that PR's round 6 withdrew the promotion (withdrawn-in-PR, records-was); the register was 97 / 3 / 2 at that merge and reaches 99 / 1 / 2 only with this re-review, one PR later than the title claims. - **The drain byte-parity e2e pinned a fixture state that never existed.** `e2e_drain_wire_shape_matches_a_real_transfer` asserted the confirmed sweep-all bond leaves exactly ONE persona funding record; the bond post's change has been a two-output split (`change_lo`/`change_hi`) since before the assertion was written, so the walk was deterministically red on clean `dev` — surfaced by PR-C's full walk battery, verified against a clean dev baseline with the same daemon. Corrected to the assembly's real invariant (two split halves), with the cushion re-sized so the larger half alone funds the drain and the 1-in/2-out byte-parity proposition is preserved; the shared confirmed-bond fixture's wait now also requires the swept records' arm-1 prune before snapshotting (a determinism hole). - **Consensus: PoW acceptance is gated on the verifier's verdict, not the fail-closed sentinel (CEN-D2/D1 S1).** The `0xff…` hash written on RandomX FFI failure passes `check_hash` at difficulty 1, and the validation sites ignored the verifier's returned bool — so under (verifier failure ∧ difficulty 1) a block could connect with its PoW never verified. All three consumer sites (main connect, alt path, longhash worker) now reject on the bool; the sentinel remains as a belt; a local verifier failure rejects without `m_bad_pow` (the block is unproven, not disproven); `get_altblock_longhash` routes through the single `IPowSchema` dispatch point. Five verdict-contract unit tests, two red-observed. - **Consensus: the FCMP++ proof-skip at block connect is now hash-gated (CEN-M8 S0).** The skip was presence-gated — any pool hit skipped the membership/spend-authorization proof — while `add_tx`'s `kept_by_block` tolerance admits admission-failed txs with `fcmp_verified = 0`, so an invalid proof could connect unverified. `take_tx` now reports whether the pool's verification cache affirmatively covers the bytes (flag AND hash match), and connect skips only on that verdict. Admission-verified txs keep their skip (the D++ embargo's `hop` is unchanged); a never-verified tx pays full verification. Unit tests pin the verdict contract (admission-failed false, hash-match true, stale-hash false, failure-return reset); the connect-path wiring regression is a FOLLOWUPS item blocked on the FCMP++ spend builder. ### Added - **A documentation claim audit, and the measurement that argued for it.** PR #633 took thirteen review rounds. The first nine were reactive — a reviewer found an instance, it was fixed, the next round found another of the same class — and from round ten the author ran a mechanical claim check before each push. That pass caught three defects no reviewer had filed (a matrix row routed to a section that did not exist, a register that jumped W-3 to W-6, a stated writer count gone stale), and the rounds after it found only what it could not see. `scripts/ci/check_doc_claims.py` makes that check a gate: a document **declares** which invariants it means to hold (`` and five siblings) and the gate checks exactly those. Declared rather than inferred, because the first cut inferred them corpus-wide and reported **991 findings against a clean tree** — a gate that unusable is convention theatre pointing the other way. Every extraction distinguishes checked-and-passed from found-nothing-to-check and fails on the second, and the pass line states the limit in its own output: it checks numeric and structural claims against source, **not rationales** — four premises refuted by review the same week would all have passed it green. Its 81 failure paths and 24 negative controls are falsified by a committed, runnable matrix (`check_doc_claims_falsification.py`) that builds a synthetic corpus in a temp tree rather than mutating the repo, and which **measures its own completeness**: every one of the gate's discrepancy, non-coverage and refusal sites must be executed by some case, or the matrix fails. That sweep found seven unexercised sites on its first run — including the wrong-commit submodule branch, which no case reached at all. A ratchet in `docs/ci/doc-claims-baseline.txt` holds live dead-citation debt monotone downward and stops a document silently un-declaring a leg, because opt-in without a ratchet is adoption theatre. Both legs assert against the **base revision**: the figure otherwise travels in the same commit as the change it constrains, so one edit could add rot and lift the bar to match, and the registry records the full declaration (`series:DRS-W`, not `series`) so a document holding two of one kind cannot drop either unnoticed — and the registry line is itself base-checked, since dropping a declaration and deleting the token recording it passes every single-tree check. A citation into a submodule that is **not checked out** aborts the run and names the init command rather than counting as rot: absence of the file is first evidence the *subject* is absent, which is the misattribution the link gate makes in that same state. First adopted by the P0b atomicity audit, where it immediately caught a live defect: `V4_DESIGN_NOTES.md` still restated the finding range with its old upper bound after the register had grown past it. It then caught one in itself — the ratchet's own baseline had been measured in a worktree with a submodule absent, and CI went red on the first push. - **DRS-P0b — the atomicity audit covers the store that exists.** The April 2026 `LMDB_WRITE_ATOMICITY_AUDIT.md` was a PASS doing work it was never entitled to do: 22 of the 49 declared tables post-dated it (49 declared, 48 at runtime — DRS-W5), while its covered subjects included the dead claim-era staking paths and two dead tables. Rewritten in place at `dev 2dba46537` over every write path — connect, pop, txpool (re-censused: 14 `LockedTXN` constructions / 13 commits, the read-snapshot `get_transaction_info` recorded as the deliberate baseline; the April D++ fix verified alive and intact), alt blocks, **three** prune shapes — one atomic and two deliberately checkpointed (archival retention rides the block's transaction with its receipt written before the destruction it authorises; `prune_tx_data` commits per ≤256-height batch **with its resumption anchor in the same transaction**; `prune_worker` commits and reopens every 4096 deletions) — and the store lifecycle — `open()` (one transaction, three exits, one of which commits before it refuses: DRS-W10), `reset()` (enumeration wipe — its stale FOLLOWUPS row closed) and `migrate()` (zero writes by design). The §10 coverage matrix is gate-pinned to `SHEKYL_LMDB_TABLES` (DRS §9.1 leg 3 now live; eight new failure paths observed red). The in-code-only conventions are transcribed for the Rust store: A-2 height bases with the F-B5b convert-don't-unify rationale verbatim, A-4's load-bearing revert partial order, A-6's guard census (22× `std::runtime_error` vs 2× `DB_ERROR_TXN_START` for one precondition). Findings DRS-W1…DRS-W11 recorded — none S-graded, no C++ touched — including `txs` (zero write or read sites; inherited-dead candidate) and `hf_starting_heights` (deleted at every writable `open()`, structurally absent at runtime), and the post-pop burn pair living outside the pop funnel — which `blockchain_import --pop-blocks` reaches today by popping straight through the DB — and **DRS-W9**, the connect side of the same architectural fact: the burn pair also runs after the try whose catches set `m_batch_success`, so an LMDB write failure between the row and its aggregate commits the block and the row without the total. That one falsified a PASS this audit had published, and it is the production entry that regrades DRS-W7 from unreachable to live. The dead `staker_pool_balance` properties row left the schema doc, and the workflow carrying the schema gates is renamed `docs-gates.yml` for what it does (Rick's #624 boundary: P0b's item, no other lane's). - **Shard-visual: ruling B's assigned residue CLOSED (2026-09-06), and the single-algorithm fallback RETIRED.** Three items the spec handed to ruling B by name and B never closed, ruled together: `time_density` is **kept** (admitted, deliberately dormant — the paragraph is what makes it deliberate rather than an oversight); the post-rewire aesthetics are **accepted as they stand**; and the single-algorithm palette is **retired** with a rule-21 reopening criterion, because both of its trigger conditions resolved (continuity was ruled the wrong property, and the floor budget was ruled in candidate.v1's favour). Also closes the *Final algorithm palette* open question and points the stale *Algorithm versioning* entry at the ruling that already settled its privacy half. One reported aesthetics finding was **withdrawn under measurement**: the claim that distinguishability collapses at the 128px product size does not survive — pairwise full-image RGB-RMS is min 42.0 / median 53.5 at 128px against min 42.9 / median 57.8 at 512px, so distinguishability is size-independent. It was an impression of a thumbnail strip, not a property of the renders, and the withdrawal is recorded rather than the finding. - **Shard-visual performance targets AMENDED (2026-09-06), and the amendment changes what they assert.** **RATIFIED.** *Authority: the ruling reached this work relayed, and was then confirmed by Rick directly to steering on 2026-09-06 ("Shard visual B is ratified") — confirmed in-channel rather than by an artifact in the tree, which is where the record stands.* The ruling: the floor scores are acceptable, so of budget / candidate / floor device the **budget gives**. New targets, stated as *median on the floor device, warm, otherwise idle* — the quantity `examples/budget_matrix.rs` actually emits: 128px 350 ms, 256px 800 ms, 512px 4 s, 1024px 25 s (2× the corpus-worst floor median; the originals are struck through in place, refuted not superseded). Two things recorded with them: (1) the originals named **no statistic and no device state**, so they were never falsifiable — an unfalsifiable threshold generates no failures, which is why they survived unexamined; (2) the replacements are **regression bounds, not fitness bounds** — at 2× the measured worst nothing the implementation does can breach them, and presenting them in the old voice would ship a check that cannot fail. Enforced by named trigger (any renderer / compositor / entropy-draw change obliges a floor re-run; `docs/FOLLOWUPS.md`) plus a new CI gate `shard-visual-x86-smoke`, whose **pass** line carries its own disclaimer because a green checkmark otherwise reads as "performance is fine" and cannot bound the Pi 4 floor. - **Shard-visual ruling B (measurement half): goldens, KATs, avalanche, floor budget matrix — executed 2026-09-06.** Designated-reference goldens committed once (the run is recorded by the generator inside `tests/goldens/recipes.json` → `_reference_run`, not restated in prose where it would drift; x86_64, release, and the toolchain `rust/rust-toolchain.toml` pins; never regenerated by consuming tests); full-recipe KATs for all nine fixtures on both implementations (shekyl-dev pins a copy of the same artifact); two-limb avalanche on the pixel axis (sweep min RMS 34.165 ≥ floor 20). Floor-device results (skl-pi, Pi 4, thermal bracket 50.6–59.4 °C at stock 1800 MHz): raster parity measured **RMS = 0.000000 on all nine fixtures at 128px** (bit-identical to the x86 goldens — recorded as measured, does not reopen the bit-exactness retraction; 256/512/1024px parity is unmeasured, since goldens exist at the one size); budget matrix **36/36 cells over budget** (1.3×–6.2×; full per-fixture table committed under `docs/benchmarks/`), falsifying candidate.v1's fitness on the stated minimum device at every tier — ruling owed among budget / candidate / floor device (spec *Measurements of record*). Thresholds θ = 2.0 and floor ≥ 20 untouched throughout. - **Shard-visual ruling B (spec half): the layered determinism bar, pre-registered thresholds, and the sensitivity correction.** Ratified 2026-09-05. Hash-derived structure is bit-exact forever (pinned by the twin Rust/Python recipe KATs); the painted raster is held to a pre-registered perceptual metric (RGB-RMS ≤ θ = 2.0, decoded pixels never PNG bytes), fixed before any cross-platform measurement exists. The spec's bit-equivalence prose is retracted in place with its reopening criterion (a deterministic rasterizer pinned across both implementations). The reorg-continuity claim is corrected, not softened: it was wrong as written and in direct opposition to the integrity check's purpose — the ruled property is sensitivity, with a two-limb avalanche falsifier (floor ≥ 20) whose second limb is what makes it a test. The floor-device budget matrix, goldens, and tests are the held measurement half (FOLLOWUPS). Also restores the candidate-compositor § heading eaten by an earlier edit anchor. - **DRS-P0a — LMDB table reconciliation: the pin→HEAD delta is measured, registered, and gate-pinned.** The DRS design doc's Round-2 substrate figures (46 tables, seven undocumented, two phantoms) had aged into today's 49; P0a closes the delta by **set difference, not history search** — births = 3 (both attestation-witness tables and `archival_settlement`, each with owning commit and the schema version-ladder as independent witness), **deaths = ∅ measured**, not assumed by the total adding up. A 49-row reconciliation registry in [`DAEMON_REDB_STORE.md`](design/DAEMON_REDB_STORE.md) records per-table disposition (39 documented-at-pin + 7 since-documented + 3 born-since), the pin-era doc closure (41 claimed = 39 real + 2 phantoms), and the provenance trap that bit twice: a pickaxe on a bare name measures the identifier *family* (`archival_settlement_epoch_at_height`, pre-pin), the quoted literal measures the *table* (post-pin). The schema doc's duplicate `properties`-titled heading — present since before the pin and invisible to the property-row gate's set() dedup — is merged into the one `properties` section, and `check_lmdb_schema_coverage.py` gains the legs that would have seen all of it: section headings as a duplicate-free bijection with `SHEKYL_LMDB_TABLES`, and the registry's rows and stated count pinned to the same macro. Every new failure path observed red before landing. DEL-005 closed; stale DRS figure sites corrected with records-was pins kept. - **Shard-visual ruling A: parameter admissibility closed, with a pre-registered criterion and typed enforcement.** The spec's parameter design-review checkpoint had inverted — the gate existed, never ran, and the borderline set it was meant to gate shipped in the GUI preview, because the feature set was designed against a fake chain that publishes what the real chain hides (cleartext amounts, cleartext tiers). The pre-registered criterion (a feature is admissible iff it is a deterministic function of data any shard holder reads from held block bytes) admits `activity_density`, `output_richness`, count-based `coinbase_ratio`, and `time_density`; rejects the value moments (CT) and `tier_skew_high` (confidential staking / the F-ARCHIVAL tier oracle); and rule-21-rejects the stake-event features with a named reopening criterion. `ShardAggregate` and `Features` now carry only the admitted set; renderer inputs that consumed rejected features draw from their own SHAKE256 namespaces at fresh indices. Companion rulings: the rendering-spec version is chain data pinned at shard creation height (never wallet data), and overridden renders are non-canonical by type (`CandidateRecipe.canonical`). The no-tradeability enforcement-point inventory is verified and codified; the `shard.v1.render.*` / `candidate.v1.*` namespace families are registered in the implementation index. Aesthetics closure and the determinism bar are ruling B (FOLLOWUPS). - **C2-R1c — alt admission + acceptance topology: ruled, and the sync orphan arm stops punishing peers for our own state.** Seven rulings signed ([`CONSENSUS_C2_R1_REORG.md`](completed/CONSENSUS_C2_R1_REORG.md) §5.5): the two-tier admission contract ratified as defense economics under the intervals-not-averages constraint; CEN-K1 split into its two conflated conditions; K9's `relay_method::block` tolerance fixed to three named producers (the third found by the ruling's own falsifier, executed pre-signature); K10 ratified as-composed with M8's hash gate, dependency armed; storage floors ratified with bounds routed to GAP-4 by name; topology ratified with belts named. **The Q3b defect fix:** the sync-loop orphan arm severed and scored every connection from a span's origin on a flag that means "our store lost the parent" — local pops, the checkpoint-rollback discard (reachable mid-span through the 600 s reload), and the Q1a flip-flop discard all reach it honestly. The arm now cleans up and re-walks the chain directly via the new shared `request_chain_history` helper (which also retired a three-copy drift set); the queue-bookkeeping-mismatch drop keeps its teeth. Enforced twice: consensus-invariants **[7/7]** (whole-arm extraction, full punitive token set, both failure paths observed firing) and the new behavioral rig `sync_orphan_arm.cpp` (scripted-core seam, three vectors: the orphan arm's no-punishment-and-healing vector and the cleanup-failure-recovery vector each observed red-first on their pre-fix forms, while the bookkeeping-mismatch vector stays green on both trees BY DESIGN — it guards against over-correction; five review rounds hardened the fix through two Bugbot HIGHs and two Bugbot MEDIUMs). Census: nine rows → bucket 2, counts 86/35/5/46 = 172; the R1 batch is complete and the round doc is closed to completed/. - **The staking exit is REACHABLE: `unstake` + `collect_unstaked` (PR-C — the composed verb, wallet-RPC + CLI).** The reachability gate held since PR-P4 (and narrowed, never lifted, by the engine walk, the submit fact set, and PR-B's dispatch seam + daemon walk) is lifted: `unstake` posts the irreversible `Unbond` exit for the first live-bonded persona (engine-resolved — the wire never names a slot, fee is the canonical P-lane floor, CLI carries the irreversibility confirmation), and `collect_unstaked` sweeps the released collateral to principal with an engine-computed exact payment (`Σ selected − fee`, zero change); a `SWEPT` reply *requires* both halves of the completion fact — `remainder` (the swept persona's) and `another_pool_remains` (the lane's, so a per-persona `0` cannot read as lane-wide completion while a rotation-residue exit is uncollected; optional fields cannot forge "done"). A single overloaded verb was rejected for irreversible-step mis-selection, and the sweep deliberately does not ride `drain`'s firewall-pinned active-persona shape — the RESERVED→shipped reconciliation is recorded in the OpenAPI census. Codes `-29513..-29529`, with the released-vs-held dispatch dispositions on distinct codes (`-29521`/`-29522`) because they demand opposite client behavior, a non-loopback daemon named as operator configuration (`-29528`) rather than an internal fault, and a pre-seal daemon outage while preparing the sweep named retryable (`-29529`, `check the daemon`) rather than an opaque internal fault. Both staging `dead_code` allows retired (rule-21 conditions met); `PENDING_POST_VERSION` v9 stands (no persisted-wire change). - **Finding discharged with it: the funded retirement gate had passing coverage and zero production reach.** Emptying a persona slot requires a drain of exactly `spendable − fee`; the fee is an internal quote over a live daemon estimate — never a parameter, never exposed by any read — and `get_drain_balance` is gross-of-fee, so no user path could produce the zero the gate fires on: the retire engine walk (#575) observed it fire only on synthetically constructed states (a test that constructs a gate's trigger state proves the gate works, and says nothing about whether the state is reachable). The witness-gated terminal sweep is what makes it reachable, and the new composed-arc regtest walk (`e2e_unstake_collect_retire_composed_arc`) retires a persona end-to-end from states produced only by the shipped verbs: post → connect → sweep (remainder 0) → rotation → claim-window expiry → funded-gated retirement on real-chain evidence. The F-D1 amount stage is untouched (the sweep's payment is an output of selection, where per-output amounts already legitimately live); the §12.3 carve exception for the total-shaped exit sweep is recorded in `PRINCIPAL_STAKE_LIFECYCLE.md` and scoped by the `TerminalExitObserved` witness type. - **C2-R0 phase 2 — the census learns to say what is missing.** The consensus census gains **§12, the GAP register**: 8 `GAP-` rows (registered at birth) for consensus rules with no site — 7 grounded from the phase-1 corpus study ([`C2_R0_PHASE1_CANDIDATES.md`](design/C2_R0_PHASE1_CANDIDATES.md), landed as the frozen input) and 1 steering-routed carry (GAP-8: the two supply clamps encode **opposite terminal emission policies**, documented on both sides without either comment seeing the other — the register's worked example; resolution rides FL-R12′). Six candidates ground as **present-already** and are recorded with pointers (C3→R3, C4→the unlock_time triple-divergence, C5→no peer-time mechanism, C9→R4, C11→bootstrap arms, C12→R1b). GAP rows sit outside the §3 denominator — site-anchored sums must not absorb siteless rows. External evidence was spot-checked at primary sources before entering any row (the Qubic campaign figures verify verbatim, with the +461.8/−460.0 separate-ledger-lines decompression recorded). §10's R2 batch now carries Rick's 2026-09-03 **deferral** in the queue row itself (resumes on FL-R12′; the red-test conjunct is already discharged), with pointers on all 8 member rows. - **C2-R1b implementation — the fork-choice/depth contract and the operator-checkpoint surfaces** ([`CONSENSUS_C2_R1_REORG.md`](completed/CONSENSUS_C2_R1_REORG.md) §4b ratified 2026-09-03, §4c execution record). The prune now writes a **monotonic watermark** (its durable receipt, same txn as the deletions; exempt from every revert), and `BlockchainDB::pop_block` — the single funnel all pop writers traverse — **refuses any pop landing below the oldest fully-retained epoch's open height**, converting the silent post-horizon corruption arm into a loud refusal; a watermark-refused network switch leaves the node loudly DEGRADED (sticky flag, new `get_info.following_degraded`, `CORE_RPC_VERSION` 3.26) **without penalizing the peer** — the refusal is a local retention limitation, not block invalidity, so the block stays in the alt store and `bvc` carries no failure (a `m_verifivation_failed` refusal would have both P2P paths drop and score every honest peer advertising the heavier chain, isolating the degraded node onto its own fork; core test `gen_reorg_watermark_refused_switch` pins the false→true transition, stickiness, and recurrence, observed red-first on the pre-fix form) — and a watermark-refused checkpoint rollback fail-stops. The fork-choice comparison and the CEN-D5 alt-window selection cross to `shekyl-difficulty` (`fork_choice`, `alt_window_plan`) behind new FFI exports with shared pinned vectors (`docs/test_vectors/FORK_CHOICE_V1.json`, Rust-native + C++ e2e consumers). Checkpoint wiring is **uniform across all public networks** (both nettype guards deleted — the `return true` silent false positive included; rule-71 allowlist 8 → 4); the checkpoint rollback target is floored at the genesis-only chain (the inherited height-1 wrap is unrepresentable, and the saturated-to-zero form aborted on the can't-pop-genesis guard), a conflict at genesis itself fail-stops as unresolvable, the walk stops after an applied rollback (later height-ordered checkpoints would be read against the stale pre-rollback height), and conflict output names file, height, and both hashes; core test `gen_checkpoint_conflict_rollback` pins the completed low-height rollback with a second conflicting checkpoint in the file, observed red-first on both pre-fix forms. Deleted: the unpopulatable difficulty-checkpoint twin and the weekly accidental full-chain difficulty recompute, and the caller-less no-arg `pop_block` overload. `check_consensus_invariants.sh` gains the watermark single-writer/no-revert invariant [6/6]. - **DRS-P0f row coverage complete — and it found both of the review's S-graded defects.** The conformance register disposes the **102** bucket-1/2 census rows that existed when it ran: **99 CHECKED-CONFORMANT, 1 DIVERGENT, 2 failed closed** (the divergent row is CEN-B5's rule-71 FAKECHAIN skip, which census R9 owns). CEN-L11 with CEN-L12 coupled were fixed by PR #609 and promoted at its merged sha. The bucket-1/2 set has since grown to **121** — C2-R1b promoted nine rows on 2026-09-03 and C2-R1c ten more on 2026-09-04 — and those nineteen are UNREVIEWED until reviewed. **Both S-graded findings ran the full arc — found, ruled FIX, fixed, merged, re-reviewed:** the S0 (CEN-M8, with CEN-G4/J26) by PR #602 and the S1 (CEN-D2 with CEN-D1) by PR #604, so no S-graded divergence remained — until 2026-09-04, when CEN-B5's header-check timing (S1, see the Changed entry) reopened the DRS-0 gate. Each row carries sha-pinned, arm-walked evidence and 15 routed REWRITE-NOTEs for the rebuild. The S0: **CEN-M8** — block connect's FCMP++ proof-skip *was* **presence-gated** where the ratified rule requires **hash-gated**, and the `kept_by_block` admission tolerance meant a tx whose proof failed at pool admission could connect with verification skipped, while the exact required check sat unused on that path. Ruled FIX per the §7.2 ladder and **fixed by PR #602** (merged): the skip is now hash-gated and CEN-M8 with CEN-G4/J26 is re-reviewed CHECKED-CONFORMANT. Detail: [`CONSENSUS_STORE_RECONCILIATION.md`](design/CONSENSUS_STORE_RECONCILIATION.md) §5.4.1. - **DRS-P0f slice 1 — the conformance register's first verdicts.** The register that gates DRS-E2's correctness arm is no longer empty. **CEN-H5 CHECKED-CONFORMANT** — the vin whitelist, carried by one rule site (`check_inputs_types_supported`) reached from both relay admission (`check_tx_semantic` — run once per pool entrant: in `add_tx` for fresh entrants, caller-side for the pre-verified `kept_by_block` re-inserts) and block connect (`ver_non_input_consensus` on the pool supplement, block-fatal on main and alt paths), with `check_tx_inputs`' typed dispatch and the DB write backstop behind it *(evidence as corrected 2026-09-03 — slice 1 originally cited the dead double-spend visitor as connect coverage; the CSR decision log carries the correction)*. **CEN-L12 DIVERGENT**, coupled to CEN-L11 — its maturity arithmetic conforms exactly (60/10, `unlock_time` absent, staked arm retired) but the spec's *universality* clause fails while L11's unchecked construct verdict can silently drop an accepted output, so **L12 cannot be promoted while L11 stands**. Consensus-relevant because correctness-oracle status attaches per row: a digest match on a CHECKED-CONFORMANT row is correctness evidence; on any other row it remains regression evidence only. Detail in [`CONSENSUS_STORE_RECONCILIATION.md`](design/CONSENSUS_STORE_RECONCILIATION.md) §5.4.1. - **The `Unbond` exit lane is dispatched and daemon-walked (PR-B = #601, 2026-09-02) — an `Unbond` has now been assembled by the wallet, accepted by native `/submit_transaction`, and connected on a real regtest chain, for the first time anywhere.** `Engine::submit_unbond` (`shekyl-engine-core/src/engine/unbond_dispatch.rs`, `pub(crate)`) is the claim/drain sibling seam: bond-record facts fetched as one bound read view over the persona-isolated transport (`fetch_claim_source_for`), readiness refused with consensus's own predicates before any curve-tree work, the canonical P-lane floor fee (no knob), sweep-all funding through the bond path's own sweep body, `AssembleUnbond` in the actor, a `PendingUnbond` sealed persist-before-dispatch, then the posture→submitter choke point. The pending-post block gains the fourth reservation-observed kind — **`PENDING_POST_VERSION` v8 → v9** (rule 42; snapshot + paired-bump gate) — deliberately NOT a `PendingBondPost`: the exit draws no decorrelation offset (its trigger, a cooldown expiring, is already public), so it must not enter WI-3's due-check, and it retires by its reservation settling (`remove_settled`), not a pscan match. The **daemon walk** (`e2e_unbond_accepted_and_connected`, the FOLLOWUPS registration it discharges) asserts the RF-D9-class byte proposition — submit-accept via the §8.7.1.1 UB battery, block-connect via the record row read back **present with `bonded_total == 0`** (a transition observed from the pre-submit floor balance) — on the genesis schedule with the cooldown predicates **vacuous by design** (never-served persona; the served-exit arms remain PR-A's unit battery, and the SEB lever cannot cheapen a served exit because the slash watermark advances `CHALLENGE_RESOLUTION_BLOCKS` in blocks). **Reachability is NOT lifted**: no RPC method, no CLI verb, `unstake` RESERVED — the seam's only caller is the `#[cfg(test)]` walk, so "nothing dispatches the assembled bytes" narrowed to "nothing user-facing dispatches"; lifting it is PR-C's composed `unstake` (post + a decorrelated drain), which also inherits the retire-on-a-real-chain arm by its recorded conditional. **Review round 3 hardened the funding sweep with a consensus vin-headroom bound spanning all three retention lanes**: `FCMP_MAX_INPUTS_PER_TX` caps the WHOLE vin, and every tx `sweep_funding_outputs` funds carries exactly one non-funding vin (bond, emission, or Unbond), so an 8-record sweep assembled a 9-vin transaction — accepted on FAKECHAIN (the C++ cap is gated off there, so no regtest walk can observe the boundary) and rejected on every public network. The sweep now owns `MAX_RETENTION_FUNDING_INPUTS` (= 7, pinned by an absolute KAT after a bite proved the relative tests could not see the constant drift) with a per-caller overflow policy: the bond post and the claim's fee sweep refuse by name (their GF-4b consume-everything semantics forbid a silent subset), and the exit caps to the largest subset (no consume-everything obligation; leftovers go to the retired persona's drain). Round 5 classified the refusal on the first-stake surface: `FirstStakeError::FundingFragmented` → wallet-RPC **-29512 STAKE_FUNDING_FRAGMENTED** — its own arm because both standing buckets misdiagnose it (rule 82: "-29500 fund and retry" worsens fragmentation; "-32603 internal" is false, the funding is intact) — rendering the public headroom constant and never the wallet's record count (P-activity volume, the redacted class; the sanitizer reduces the engine arm the same way). - **Rule 71 (network uniformity) + its CI gate.** On the consensus/validation surface, nettype selects data, never control flow; a real behavioral divergence must be named, ratified, and loud (`.cursor/rules/71-network-uniformity.mdc`, minted at Rick's direction during C2-R1a — both inherited checkpoint mechanisms had the `if (nettype == MAINNET) { do the real thing }` shape). `scripts/ci/check_network_uniformity.sh` (wired into `ci/grep-gates`) flags any new public-nettype equality or inequality branch in `cryptonote_core/ + checkpoints/ + blockchain_db/` against an annotated 8-entry allowlist enforced as a bijection (a copy-pasted duplicate of an allowlisted branch fails the total). Matching is spelling- and operand-independent (either comparison direction, any variable name, `if(`/`if (`, wrapped lines, compound conditions, `switch`/`case`), and collection failures are loud (a stripper failure or an empty fence directory fails the gate rather than reading as no-match). Every enforcement arm was observed red on a planted evasion before landing. - **Wallet engine staking product door (`Engine::stake()` → `StakeFacade`).** Staking / drain / claim product calls go through `StakeFacade`; JSON-RPC method names are unchanged. Inherent `Engine` methods are count-frozen (`METHODS_CEILING`). See [`ENGINE_COMPOSITION_DECOMPOSITION.md`](design/ENGINE_COMPOSITION_DECOMPOSITION.md). - **Daemon C++ is not a base — a complete rewrite gates release (countermand, 2026-09-01).** Recorded with its blast radius across `DRS-*` and its row-level census map in [`CONSENSUS_STORE_RECONCILIATION.md`](design/CONSENSUS_STORE_RECONCILIATION.md) (`CSR-`), which also reconciles that program with the consensus rewrite — the two partition the same six C++ files on orthogonal cuts and had **zero** cross-references in either direction. Consensus-relevant effects: the C++ is demoted from *trusted* oracle to a differential reference for rules that are **both** ratified on record **and** carrying an **affirmative conformance record** — absence of a recorded divergence means *unreviewed*, not conformant, so the checked set was empty until **DRS-P0f** (the per-row conformance review, minted here) began populating it (a **conformance-exception register** holds the known divergences, seeded with CEN-L11, whose ratified spec the implementation does not meet); **heed retired** as an intermediate engine (DEL-007), **redb stands**. Design-round detail — the CSR-1…CSR-5 rulings, the 18-row store map, and the arithmetic corrections — stays in the owning document per [`95-documentation-lifecycle`](../.cursor/rules/95-documentation-lifecycle.mdc). - **CI gate: every workflow file must parse and be shaped like a workflow.** A workflow GitHub cannot parse produces a run with zero jobs and no failing named check, so the gates it carries go quiet — that is how one unquoted step name took the documentation-lifecycle gate set offline. The check runs in the unfiltered `grep-gates` job, so it sees every workflow edit. - **P2P wire census (P2P-1, `PWC-`).** `docs/design/P2P_1_WIRE_CENSUS.md` enumerates the peer-to-peer wire surface and the connection-management behavior around it at `dev` `30cd547e2` — 57 bucketed rows, 3 b1 / 6 b2 / 2 b3 / 46 b4, on the same four-bucket bar and evidence-class column as the consensus census. Mechanical denominator (26 C++ KV maps, 72 field lines, 22 Rust `PortableMap` twins) with a sum check whose 4-map residual is accounted row by row, and six named inverse spot-checks. Mints one new evidence class, `inherited-defensive`, for a defence the tree carries by lineage with no Shekyl record examining it. Discharges the requirements register's §7 tasks 1–4 and records task 5 as not verifiable from the repository. Findings of note: `connection_entry_base` has zero callers tree-wide and `network_address_old` only a size printer (both bucket-3 deletion candidates); `network_config`'s never-sent KV map would advertise a 50 MB packet limit against the 100 MB the transport enforces; the Shi et al. (NDSS 2025) eclipse attack's double-spend arm is refused at the tree by an **inherited** Monero guard (`f7fd209ed`, 2024-03-07) that no Shekyl record has examined, while its graylist and whitelist sub-attacks are unaddressed. Docs only, no behavior change. - **Merged consensus-rule census (C1).** `docs/design/CONSENSUS_RULE_CENSUS.md` supersedes the three census walks as the single live instrument and the consensus-rewrite's specification input (171 rows, buckets 87/14/2/68; every RC/CEN/Survey-A id resolved in its §9 accounting; the C2 design-round queue in its §10). Consensus-relevant finding folded in: the MTP timestamp boundary is three-way split (ratified-strict spec, equality-accepting live validator, strict-but-unwired Rust predicate) — flagged for an early ruling. The RC and CEN walks are archived to `docs/completed/`; docs only, no behavior change. - **Independent second consensus-rule census (`RC-`).** 181 independently ratifiable rules, walked from the block and transaction acceptance entry points at `8ba1aae3d` and classified into four buckets. The census is the instrument for the consensus rewrite; no chain behavior change. - **Consensus-rule census (CEN-1).** `docs/design/CONSENSUS_RULE_CENSUS_3.md` enumerates every rule on the block/tx acceptance path at dev `8ba1aae3d` (161 rows; 84 Shekyl-spec'd / 23 inherited-ratified / 54 never examined) as the specification substrate for the consensus Rust rewrite (2026-08-30 ruling: census precedes rewrite). Docs only; no behavior change. ### Removed - **The per-block-checkpoint fast-sync mechanism (consensus)** — C2-R1a, ratified 2026-09-02 ([`CONSENSUS_C2_R1_REORG.md`](completed/CONSENSUS_C2_R1_REORG.md) §3). Deleted whole: the compiled-in hash-of-hashes table and its loader (mainnet's pin was a stale inherited constant no Shekyl blob could match; testnet/stagenet blobs loaded with no verification), the four `fast_check` arms in block connect (PoW skip, pool-supplement NIC skip, per-tx input/FCMP skip, and the CEN-G8 GF-1 belt — retired, its job subsumed by the now-unconditional per-tx pin), the `kept_by_block` early-success wrapper arm (CEN-E4), the p2p hash-chunk expansion and pruned-span weight gating, `--fast-block-sync`, `src/blocks/` (three zero-byte `.dat` blobs), and `shekyl-blockchain-export --blocksdat` (the blob generator). Behavior-preserving: every arm was unreachable with the shipped empty data. Census: CEN-E3/E4/G8 → bucket 3, counts 86/16/5/64. - **P2P wire: the two dead RPC-advertisement fields are gone.** `rpc_port` and `rpc_credits_per_hash` were members of the handshake schema (`basic_node_data`) and of every peerlist entry — parsed and recorded on receipt, though `KV_SERIALIZE_OPT` omitted them from honest wire bytes at their permanent zero — keeping open the capability to advertise what the ratified RPC posture says will never exist: `rpc_credits_per_hash` is Monero's RPC-payment/mining-credit system, and `rpc_port` advertises a public RPC endpoint over P2P — contrary to `RPC_TRANSPORT_POSTURE.md` RT-9, whose RT-W5 slice already deleted the RPC-side half (PR #533). Both fields are now deleted from the C++ wire structs, the peerlist manager, the connection contexts, and the Rust `shekyl-levin` maps in the same change, so the dual stack never disagrees; a peer that still sends them is read as any other unknown field. The persisted peerlist store version is bumped (v7, drop-on-load — a stale cache re-bootstraps, no migration shim). The drop includes the anchor list, so the first restart after upgrade rebuilds the peer view from seeds and gossip without the anchor set's eclipse resistance for that one bootstrap; a *downgrade* (an old binary reading a v7 store) ends in the same empty-peerlist re-bootstrap, but via a load exception rather than a clean version drop. The store loader now also refuses an implausible per-list length prefix (untrusted disk input, ceiling derived from the per-zone peerlist caps) instead of reserving memory of disk-chosen magnitude at startup — a corrupt cache re-bootstraps either way. Pre-genesis the wire is free to change; post-genesis both fields would have been permanent (`CONSENSUS_RULE_CENSUS_1.md` U-5 and the `rpc_port` half of L-6). - **The two offline prune utilities are retired; `shekyl-mdb-copy` replaces the only capability they uniquely held.** `shekyl-blockchain-prune` had been inert since LMDB v6: its private version guard (`MAX_SUPPORTED_DB_VERSION = 5`) refused every VERSION-10 database, and its copy path enumerated 16 of the schema's 49 tables — bumping the guard without fixing the list would have silently dropped `txs_pqc_auths`, `output_metadata`, and every archival/curve-tree table. `shekyl-blockchain-prune-known-spent-data` was a structural no-op on an amount-0 CT chain (its scan skips every zero-amount input and output). Pruning stays daemon-resident (`--prune-blockchain` at startup, the 5-hour timer, the `prune_blockchain` RPC/console command — see the Changed entry for the completed confirmed-prune semantics); file-size reclaim is now the schema-agnostic `shekyl-mdb-copy -c` (upstream LMDB `mdb_copy`, newly built from the vendored source), and the daemon console's prune warning — which pointed operators at the binary that refused to run — now describes that flow. Reversion clause in `FOLLOWUPS.md` (post-genesis): any rebuilt offline prune tool derives its table set from the schema source of truth. ### Changed - **Register: CEN-I12 re-reviewed at the merged reconciliation and promoted CHECKED-CONFORMANT — 100 / 1 / 1 over P0f's 102-row snapshot.** At `667817d47` all three FCMP++ verifier arms read the node's own per-height record under the reference block's height, never the header, and that record is the ruled state (the tree at chain height `ref_height`). The verdict rests on the state definition, not the placeholder rationale slice 7 used. CEN-L8 is the one row still failed closed; CEN-B5 the one DIVERGENT. The per-height root record is recorded as a spec-level requirement on any store the rewrite uses. Register-only; no code. - **Daemon RPC: `get_block_header_by_hash`, `hard_fork_info` and `get_fee_estimate` change shape; `CORE_RPC_VERSION` is now 3.27 (RK-5b).** Those three plus `get_last_block_header` and `get_block_headers_range` (and the `getlastblockheader` / `getblockheaderbyhash` / `getblockheadersrange` aliases) are served natively from Rust. **The last two keep their response shapes** — a client parses them exactly as before, and only their refusals changed. **Operator impact — three replies a client parses differently.** `get_block_header_by_hash` answers **per element**: `block_headers` is now an array of `{hash, block_header?}` slots rather than a bare header array, so a client learns *which* hash was unknown instead of receiving zero headers and an error string, and one unknown hash no longer discards the other nine hundred; the request's singular `hash` field is gone (it had no in-tree caller, and its only effect beside `hashes` was to slip one lookup past the restricted cap of 1000). `hard_fork_info` splits the reply's single `version` in two, and the mapping is exact: **`active_version` is that field renamed** — the deleted handler always filled it from `get_current_hard_fork_version()`, whatever the request asked about — and **`queried_version` is new**, naming the version the `window` / `votes` / `threshold` fields beside it actually describe. A client reading `version` today wants `active_version`; nothing it could read before told it what the voting counts were counting. `get_fee_estimate` drops the `fee` scalar, which the C++ handler set to `fees[0]` and which therefore carried nothing the tier array did not. Also corrected while porting: the restricted header-range cap bounded `end - start` rather than the count, so a restricted caller could obtain 1001 headers against a cap of 1000; and a restricted caller asking for `fill_pow_hash` is now refused rather than handed an empty field with status OK. `get_block_headers_range` refuses a request that names no range (absent `params` or `{}`) instead of answering for block 0 as the C++ did — a client that omits its heights is told it omitted them — and bounds both endpoints against the chain tip before reading anything, which the C++ also did and the first port did not. `get_last_block_header` **refuses with `CORE_BUSY` (-9) on an unsynchronized node**, where the C++ answered `status: BUSY` with a zero-filled header — a shape that let a client reading the header without checking the status report a fork version of 0. `hard_fork_info` no longer accepts `version: 0`: it was the C++'s spelling of "absent", and omitting the field is now the only way to ask about the next fork. - **The daemon console reads the DAA block target from the build, not from the daemon it is talking to.** `T` is genesis-frozen and single-sourced through `config/consensus_constants.json`, which generates both the C++ header and the Rust constant. The console previously computed its block-statistics and hash-rate figures from `/get_info`'s `target`, which gave one constant two sources — and over a remote connection, a source the daemon controls. **Operator impact:** `shekyld status`, `print_blockchain_dynamic_stats` and `alt_chain_info` now print a warning when the daemon reports a different target, naming both values, and compute from this build's. A daemon reporting a different `T` is running different consensus rules, so this most likely means a mismatched binary or a different chain. - **FCMP++ spec: the membership anchor is a state property, not a header read (CEN-I12 reconciled).** `FCMP_PLUS_PLUS.md` §7 step 2's prose said the verifier reads the reference block's header `curve_tree_root`; that was the pre-2026-04-13 code, replaced by the per-height root record (`292c00aff7`) without the prose following. Ruled: the anchor is the curve-tree state at chain height `ref_height` — after the reference block's parent connects, before its own drain (boundary corrected on review) — with the header field and the per-height record as its two witnesses; the verifier reads its own record, the prover reads the header. The in-code FAKECHAIN comment is a consequence, not the rationale. Step 1's rationale also drops the retired claim-era staked-maturity arm (CEN-L12). Found-not-ruled: step 2b/2c's depth pseudocode is split the same way (routed to CEN-I13). The register's CEN-I12 row stayed failed-closed until re-reviewed at the merged sha — promoted 2026-09-05, see the entry above. - **CEN-B5 has a second, live divergence — the post-connect header-root check compares the wrong state (S1; found, graded and routed — not fixed here).** `handle_block_to_main_chain` compares the header `curve_tree_root` against the root read *after* `add_block`, but `create_block_template` fills the header from the root *before* the add — the state at chain height N that the per-height record, the wallet client and the CT-2 KAT all name. The operands differ at every block where a leaf drains, observed on real LMDB by a new keying pin in `archival_substrate_lmdb.cpp`; the genesis coinbase drains when block 60 connects, so mainnet/testnet/stagenet would reject block 60 and halt (derived; no non-FAKECHAIN fixture exists to observe it). The rule-71 skip R9 owns is the cause, not the context: nettype gating the check left every test and every `--regtest` run blind to it. Fix-or-accept is Rick's (DRS §7.2); FOLLOWUPS carries the sketch. The "no S-graded divergence remains" claim is withdrawn at every surface it reached; DRS-0 is gated again. *(Fixed 2026-09-05 — see the CEN-B5 entry under Fixed above.)* - **Consensus: the block-timestamp rule is ratified and single-sentence (C2-R3, `docs/completed/CONSENSUS_C2_R3_TIMESTAMPS.md`, ratified 2026-09-01).** A candidate timestamp is valid iff it is at most `local_clock + 540 s` (checked at main connect AND alt admission — the alt store previously had no future-time bound) and strictly greater than element index 5 of the sorted window of the 11 timestamps immediately preceding it on its own chain (equality was previously accepted; the alt path previously medianed the whole alt chain, averaging even-length windows; below 11 blocks of history the window is now right-padded with the genesis timestamp instead of skipping the check). Miner templates floor their timestamp at `median + 1`. The rule has ONE implementation — `shekyl-difficulty`'s `check_timestamp_rule` (built on its `is_above_mtp` / `is_timestamp_below_ftl` predicates) — exported through `shekyl-ffi` beside the LWMA-1 difficulty entry point; the C++ validator is a marshaling shim per rule 20 (the crossing was re-ratified 2026-09-01, replacing the round's original C++ owner). It is pinned by the shared vectors `docs/test_vectors/MTP_BOUNDARY_V1.json` (new; rule-30 pinned) natively in Rust and end-to-end through the FFI, and is exercised red-first in `core_tests` (strict boundary, bootstrap padding, alt FTL, alt newest-11 truncation). Consensus-forking vs the inherited behavior; the pre-genesis stressnet is genesis-only and the estate rebuild required by the V11 schema + genesis remint absorbs the regenesis. `DAA_LWMA1.md` §5.5 carries the refuted-premise correction. - **The four p2p read methods are served from Rust, and four things the C++ was doing are not reproduced.** `sync_info`, `get_connections`, `/get_net_stats` and `/get_peer_list` answer natively (RK-5a of the daemon RPC cutover), along with the five console commands that read them — `sync_info`, `print_cn`, `print_pl`, `print_pl_stats`, `print_net_stats`. Their handlers, wire structs, dispatch rows and console bodies are deleted, and so is the C++ nothing else reached: `connection_info`, the protocol handler's `get_connections()`, and the `json_object` (de)serializer pair that was its last reader. **Reply shapes are unchanged** — every field set was compared before and after on two connected regtest nodes — so `CORE_RPC_VERSION` does not move. What changed are four value edge-cases the port would not carry: `get_connections` read the clock **twice** per connection, so a connection's reported `live_time` could disagree with the divisor behind its own `avg_download`; elapsed times now saturate instead of wrapping into centuries when a clock moves backwards; the download `rate` and each connection's current speeds are converted totally rather than through casts that are undefined for a negative or out-of-range float; and `sync_info`'s queue overview no longer sizes its gap run from peer-advertised heights without a ceiling. Also removed: `print_cn`'s **SSL column**, which had no wire field behind it and therefore always printed `no` — p2p SSL is disabled by construction, so it could not have said anything else. One **request semantic** does change, deliberately. `/get_peer_list`'s `public_only` is `KV_SERIALIZE_OPT(…, true)`, and the C++ bridge skipped deserialization for an empty body — so a bodyless request got the whole peerlist while `{}` got the public subset. Native Rust treats an absent body and an absent field alike, at the declared default. `utils/fleet/read_anon_histogram.sh` was the one caller relying on the bodyless form and now asks for `public_only: false` explicitly, so the anonymity histogram keeps counting stored candidates rather than the publicly shareable ones. - **`engine_trait_bench_key_dispatch_baseline_iai` collects via Callgrind client requests** (the `ledger_iai` pattern) instead of gungraun's wrapper toggle, which reported `instructions=0` deterministically on CI once rustc folded the wrapper — retries and fresh runners reproduced the build and so reproduced the zero. Count moves by 38 instructions in ~14.6M; the post-merge `update-baseline` run absorbs it. The facts shim's pool remap is also linear now — one forward cursor over `missed`, valid because `get_transactions_info` returns hits in request order — instead of a rescan that was quadratic on the uncapped unrestricted listener; a cursor/contract violation refuses as `ERR_INTERNAL` rather than reporting a pooled transaction missing, and a `[H, H]` duplicate-slot test pins the remap (shared v3 spend fixture, `pqc_spend_fixture.h`). Developer-facing and daemon-internal; no wire change. - **A tx proof's `confirmations` is the daemon's gather-lock count, not a number the wallet re-derives from a later tip.** `confirmations_of` issued a second `get_height` and subtracted the `block_height` captured in the earlier `get_transactions` — two chain snapshots for one answer, so a block landing between the two requests inflated the count, and a reorg could make it describe a chain the block is no longer on. The native handler already computes `chain_height - block_height` against the tip it reads once for the whole gather (the one-lock rule RK-4c introduced); the wallet was discarding exactly that guarantee and paying an extra round trip for a worse answer. It now carries the value. Taking the daemon's number is no more trusting than the arithmetic was — both operands were always its to choose — and it is self-consistent. `FetchedTx` holds a `TxChainState` arm rather than `in_pool: bool` beside `block_height: Option`, so "pooled at 12 confirmations" is unrepresentable and `confirmations_of` is total: no RPC, no `async`, no unreachable error branch. Found by Copilot on #576. - **A doc row claiming a slice `**landed**` must now name its PR (`scripts/ci/check_landed_rows_stamped.py`).** Stamping the row is step 6 of `DAEMON_RPC_KV_CUTOVER.md`'s per-slice checklist and it was skipped for six consecutive merged slices, leaving that document's `Status:` banner reading *"design open for RK-4a"* while RK-4a, RK-4b and RK-4c had all been written. Because rule 95's banner is what a grep-driven reader uses to classify every claim below it, one stale banner misclassifies a whole file. The gate binds only the knowable half — a merge sha cannot exist pre-merge, so an in-flight row passes — and it strips code spans before matching, so a log entry *naming* the placeholder is not read as one *using* it. Developer-facing only; no runtime or wire effect. - **The confirmed `prune_blockchain` command now completes both pruning phases before returning.** `Blockchain::prune_blockchain` previously ran only the stripe prune; the output-metadata pass (`prune_tx_data`, which deletes the `txs_pqc_auths`/`txs_prunable` rows) was reached only by the five-hour `update_blockchain_pruning` tick, so an operator who pruned and immediately stopped the daemon to compact reclaimed almost nothing. Both phases now run in the same locked call, the startup path's duplicate `prune_tx_data()` invocation is deleted, and on a tx-data failure the first-prune startup branch now fails hard exactly like the already-pruned branch always did. - **The covert carrier now carries real transactions, not dummies alone** (`COVER_TRAFFIC_RESTORATION.md` §3.1a). `dandelionpp_notify` consumes `shekyl_relay_zone_plan_dispatch_with_refresh` and enqueues on the noise carrier instead of sending directly. **API:** `shekyl_relay_zone_noise_enqueue` takes a caller-minted `token`, and `shekyl_relay_zone_poll` takes a `ShekylRelayCarrierResolvedCb` reporting whether the transport accepted every window of each enqueued message — acceptance into epee's asynchronous write queue, not proof a peer received it, since a socket failing afterwards never revises the verdict. The pool is told a carrier-borne transaction was relayed **only on completion** — an enqueue is not a send, and a discarded message reads as *not relayed* so the origin retries on the short grid rather than waiting out the derived interval for a transaction that was never sent. **Still behind the development opt-in, defaulting off.** - **Covert cover cadence is now `3.333 s + U[0, 3.334 s]` — a mean of exactly 5 000 ms, down from `10 s + U[0, 5 s]` (12.5 s).** Operator-visible: an armed dual-zone node carries **~42 GB/month** of cover traffic, up from ~21 GB. The per-node ceiling is ruled **per node** and raised to **16 KiB/s sustained** (was 8 KiB/s per node against a per-zone figure — three denominators for one quantity, now one), with a **24,579 B/s** burst at the shortest interval. The ceiling is a compile-time assert in `params::carrier`, so a shorter cadence, a wider window, or a third encrypted zone is a build break rather than a stale table entry. `CRYPTONOTE_NOISE_MIN_DELAY` / `_DELAY_RANGE` are **deleted** — zero readers, and the cadence now lives in `params::carrier` in milliseconds because 3.333 s is not a whole number of seconds. **Nothing pays this today**: the carrier is behind a development opt-in that defaults off. Privacy effect re-measured (`DAEMON_RELAY_PRIVACY.md` §56.7): the bounded family's residual linkage channel falls from 0.120 to 0.058 against chance 0.050 at a 10 s observation gap. See `COVER_TRAFFIC_RESTORATION.md` §3.3. ### Fixed - **A reserve proof whose locator names a pooled transaction is refused, not counted.** `check_reserve_proof`'s batch fetch parsed whatever the daemon served without inspecting `location`, so an unconfirmed transaction's outputs verified cryptographically and entered `total - spent` — mempool money, still erasable by a competing spend, presented as live confirmed reserve (`CheckedReserveProof` has no pool dimension a verifier could consult). The batch fetch now refuses pooled entries with the new `-29304 PROOF_TX_UNCONFIRMED`, carrying the txid so an honest-but-early prover knows to wait for confirmation and regenerate; the tx-proof paths are deliberately unchanged, since they report `in_pool`/`confirmations` honestly. Registered in `docs/api/wallet_rpc.yaml`; bite-verified — with the guard removed, the pooled-locator fixture proof verifies. Found by Copilot on #576. - **`--prune-blockchain` no longer deletes the hash of a pruned transaction.** `BlockchainLMDB::prune_tx_data` dropped `txs_prunable_hash` and `txs_pqc_auths` together with the prunable body. The body going is the point — those bytes live in shard archival (`docs/V3_STAKER_ARCHIVAL.md` set C). The hash staying is why the table exists: a pruned v3 txid is `cn_fast_hash(prefix, base_ct, pqc_auth_hash, prunable_hash)`, and there is no `pqc_auth_hash` table, so dropping either operand left a chain that could not name what it kept. Stripe pruning (`prune_worker`) already retained the hash; the depth pass now matches. `/get_transactions` on a pruned node can therefore still bind the prefix to its txid, and a store that holds a body with no hash beside it remains `FactsFault::Inconsistent`. Because a datadir pruned by the old code already lost those rows — and would otherwise open silently and answer `INCONSISTENT` for every previously pruned transaction, forever — the LMDB schema version is bumped **v10 → v11**: retention semantics, not layout, and the standing remedy (delete and resync) applies at open instead of surfacing weeks later as RPC errors. - **A confirmed emission claim or drain now releases its seal — before this, neither ever did.** Both paths seal a one-live-per-persona record before dispatch (the record *is* the input reservation), and only the bond post's seal was ever retired. `remove_claim` and `remove_drain` existed with callers only under `#[cfg(test)]`, so nothing in production released either gate. The drain leak was visible: drain once, and that persona's lane refused `-29511 DRAIN_IN_FLIGHT` across sessions, permanently, even though the money had settled normally. **The claim leak was worse because nothing surfaced it** — claims are engine-automated, so the persona simply stopped claiming, with no user action to correlate the silence against. `PendingPostBlock::remove_settled` retires either kind once **every** funding gindex it reserved has left the accrual's live funding set, called from the existing WI-3 dispatch tick inside the same locked mutate, so the record and its reservation are released in one seal (R2-4). The evidence is the wallet's own verified scan at the reorg depth the bond post's confirmation already uses. It is deliberately **not** the bond post's confirmation set: that set is filtered to the JoinMarket post kind, and crossing the two would retire a live drain because the persona's *bond post* confirmed — a different transaction. Two edges are handled rather than assumed. A **partly** spent reservation is not settlement (a drain spends all its inputs in one transaction, so a half-gone reservation is a state the confirming spend cannot produce) and is held. An **empty** reservation is never settled: `[].all(..)` is `true`, so the Q11 zero-fee-input claim — which reserves nothing, paying its fee from the mint — would otherwise retire the instant it was sealed, before its bytes reached the network. - **A seal whose snapshot predates a retirement is refused, not sealed against spent inputs** (`PENDING_POST_VERSION` v7 → v8). Retiring on reservation-absence needs one live record per gindex, and the seal-time union check alone does not give it across an assembly: A snapshots the reservation set, and while its proof work runs, B seals A's chosen input, confirms, and is retired. B's reservation is now gone, so A's union check sees the input free and admits — sealing a transaction whose input B already spent, whose absence the *next* tick then reads as A's own confirmation. A record the network rejected would be booked as settled. The block carries a monotonic count of reservation **releases**, bumped by every method that drops a live record and compared at the seal against the value the assembly read with its snapshot. The union check sees reservations that are present; the generation makes reservations that were *released* visible. Comparing the reservation sets themselves cannot: the release returns the set to exactly its snapshot value, so snapshot and seal read identical while the inputs behind them have been spent. Persisted rather than in-memory because the pending-post store reloads the block from the seal on every read and every mutation — an unpersisted counter would reset to zero on each load and never refuse anything. Same remedy as the existing input race (retry against a fresh snapshot), so it surfaces through the same refusal, whose message now names every cause rather than only a concurrent post. The two reads that form that snapshot are **ordered**, which the first cut of the guard got wrong: all three seams read the pending block and the pscan seal concurrently in one `join!`, so the pscan load could return a funding set from *before* a settlement while the pending read returned the generation from *after* it. Stale funding paired with a current generation passes the seal — the same admission the counter was added to prevent. The pending block is now read to completion first, then the pscan seal, so every release either precedes both reads (and the spent input is never offered) or moves the generation (and the seal refuses). The ordering lives in one place, `load_seal_basis`, whose result type has private fields and no other constructor, because a source pin over the seams can only prove the function is called — never that a hand-rolled equivalent got the order right. The raw inserters `push_post` / `push_claim` / `push_drain` are now `#[cfg(test)]`, and the public `PendingPostBlock::new(posts)` constructor is **deleted**. They admit one live record per persona per kind and nothing else — no cross-kind overlap check, no generation comparison — so each was a public way to break the retirement's premise, whatever the in-workspace callers happened to do. Once their visibility was made honest the compiler enumerated the rest: with `seal_*` inserting directly, all four had **zero** production callers. `new` had none at all and is gone (rule 15); the three inserters survive only as test seeding, compiled out of the library, because staging a state the guards prevent is a legitimate thing for a test to need and an illegitimate thing for production to reach. Every seeding path now goes through `seal_*`, which also makes the fixtures more faithful: the seal stamps `Dispatched` as it inserts, so a `Pending` claim or drain is a state production can no longer persist, and fixtures that constructed one were staging an unreachable shape. ### Added - **`POST /submit_transaction` accepts `Unbond` bond-posts.** The battery covered the `JoinMarket` kind only and refused every other one `Malformed`; it now carries the Unbond fact set (`DAEMON_SUBMIT_VERDICT.md` §8.7.1.1) and dispatches `verify_unbond_bond_post`, the same function the block path already runs. `HoldingsUpdate` and `Rebond` still refuse — no wallet builds either kind, so a fact set for them would be untestable guesswork. **No wallet can reach this yet.** The staking exit stays deliberately unreachable: no RPC method, no CLI verb, nothing dispatching the assembled bytes. What changed is that a dispatched Unbond would now be accepted rather than refused. Security-relevant, and the reason this is not a copy of the credit arm: a bond-post *credit* is authorized by `P`'s identity key, but a *debit* moves bonded collateral out, and the identity key is the one a serving host holds in order to sign responses at all. The debit is therefore authorized against the record's committed cold `bond_spend_pk`, and that pin now lives in one place (`shekyl-archival-retention::debit_auth_pin`) called by both the daemon's block path and its submit path, rather than being implemented twice. A record committing no canonical-length key authorizes nothing — there is no identity-key fallback. - **A stall alarm for pending claims and drains.** A record dispatched but not settled past the alarm horizon is named in the operator log, keyed by kind *and* persona so a persona holding both a stuck claim and a stuck drain gets both alarms. The record is HELD, matching the bond post's funds-safety-over-liveness posture — the alarm reports the stall, it does not clear it. Two cases still produce a permanent stall and both remain named FOLLOWUPS items: a **terminal rejection**, and an **ambiguous submit** whose bytes never reached the network — the seams seal before a single submit and keep the record on a transport error by design, and nothing resubmits it because the driver selects bond posts only. - **The wallet can read the four `Unbond` verify operands and assemble the full `Unbond` exit transaction (PR-P4).** Before this it held *none* of `record_bonded_total`, `record_bad_interval_count`, `last_served_epoch` or `last_settled_slash_epoch`, so an exit producer could only have assembled blind. `/archival_claim_source` now marshals all four — the interval-log count was the one missing from the original three, and it is the operand with no absent state, which is exactly why it fell out of a list — and the decoder makes every one a REQUIRED field: absence is a decode error, never a default. The permissive reading is the dangerous one here. `release_cooldown_elapsed` and `slashes_settled_through` both treat an absent serve anchor as "clear", and `0 < MAX_BOND_BAD_INTERVALS` passes, so a field that never arrived would have told a user an irreversible exit was safe to take. `ServeAnchor` and `SlashWatermark` keep "never served" / "nothing settled" distinct from "never arrived" at the type level, and are the single place either becomes an `Option` for consensus. `build_unbond_vin` returns an `UnbondVin` witness whose sole constructor establishes the genesis-frozen invariants, and `UnbondRecordState::ensure_exit_ready` mirrors `verify_unbond_bond_post`'s refusals **in the verifier's own order**, so a wallet refusal and a consensus rejection cannot disagree about why. Around that vin, `AssembleUnbond` assembles the whole persona-bound transaction: funding from the typed `P`-space pool (cover + earnings — a principal output is unrepresentable in the selector's input type), the released collateral entering as a **source** (`sum(funding) + bond_debit == sum(outputs) + fee`, and the side is genesis-frozen in the type — `debit_term()` returns an `InputTerm`, so it cannot be placed on the output side), payout split across two outputs to `P`'s **own base address** — never the principal, because the composed `unbond()` is the post **plus a decorrelated drain** and paying the exit straight out would put the P↔principal edge in one transaction. The surface-A `pqc_auths` slot is signed under **`bond_spend_pk`**, not the identity key: that is the whole of GF-1 debit authorization, which consensus pins in `archival_debit_auth_pin` and which is the only thing stopping a compromised serving host — it holds `hybrid_sign_sk` — from authorizing a collateral-draining exit. `wire_bond_post_input` gained its `Unbond` arm here; the refusal it replaced said the kind "has no wallet-side producer yet", which was true when written and is not now. **Deliberately not reachable.** `assemble_unbond` is `pub(crate)` with no RPC method and no CLI verb behind it, and wallet-RPC `unstake` stays RESERVED. The producer exists; what remains is reachability and dispatch of the assembled bytes. Native `/submit_transaction` admission was a third item on that list when this entry was written and is not one now — the Unbond fact set landed in this same release (see the submit entry above), so the two must be read together. The walk lands as its own PR, so the producer merging is not the event that lifts RESERVED. ### Changed - **`/get_transactions` and `/is_key_image_spent` are served natively in Rust, and their C++ handlers are deleted (RK-4c).** Both console commands — `print_tx` and `is_key_image_spent` — render in Rust on both arms, the wallet's refresh and proofs paths read through the shared wire types instead of hand-rolled JSON, and `COMMAND_RPC_GET_TRANSACTIONS`, `COMMAND_RPC_IS_KEY_IMAGE_SPENT`, their dispatch rows and both restricted caps are gone. The facts export answers **per request slot** rather than batching two lookups and re-sorting them, which removes the two internal errors that existed only because that sort could disagree with itself ("tx hash mismatch", "internal error - txs is empty"). `entry`'s KV map branches on `in_pool`, so the Rust type makes the branch a type: `Mined` or `Pooled`, with `in_pool` derived from the arm. A flat struct of optional members round-trips every captured vector while still able to emit documents the daemon cannot produce. Two divergences are deliberate: the reply's `tx_hash` is canonical lower-case rather than an echo of the request's casing, and a `spent_status` outside 0/1/2 is a malformed reply rather than a fourth state a caller has to guess about. - **`get_transactions` no longer returns `txs_as_hex` / `txs_as_json`, and `CORE_RPC_VERSION` is 3.25.** The handler filled them "in case an old wallet asks" and the old wallet is `src/wallet/`, deleted — so they duplicated `txs[i].as_hex` and `.as_json` for a reader that does not exist (rule 60). The `_v2` oracle vectors were captured from the edited C++ struct before it was deleted, so the new shape has a real oracle rather than a hand-written one, and the `_v1` files stay beside them so the removal itself is assertable. `get_version` earns a `_v2` for the same bump, by a different route: its reply carries `CORE_RPC_VERSION`, and that constant moved to Rust, so the new value cannot be re-captured from C++ at all. The vectors are never hand-edited, so the bump gets a file beside the capture, held honest by a pair test that substitutes the live constant into `_v1` and demands the result equal `_v2` exactly — it may differ by that constant and nothing else, and it cannot go stale against it either. - **The facts export reads the prunable hash unconditionally, and refuses a store that contradicts itself.** Two defects, one root: the exporter was written against what the deleted C++ handler did, and pruning is a Shekyl system that Monero never had, so that handler is not authority for it. The hash was read only when the prunable **blob** was present. But `prune_worker` and `prune_tx_data` delete `txs_prunable` (and the worker, `txs_prunable_tip`) and never `txs_prunable_hash` — retaining the hash after dropping the bytes is the entire reason to store it, since it is what still binds a pruned body to its transaction. So on a pruned daemon every pruned transaction would have reported an all-zero `prunable_hash`. Pruned-daemon mode is node-local and ships post-genesis without coordination (rule 75); its absence today is not a reason to encode its absence. The hash is now read for every transaction the chain holds, and the blob's absence is what it actually means — pruned, a fact about this node, not a fault. A **missing** hash, and a `get_tx_outputs_gindexs` that returns false, are now `SHEKYL_RPC_FACTS_ERR_INCONSISTENT` rather than an all-zero field and an empty index list. Both had a plausible-looking fallback that would have reached the caller as a fact about their request instead of a fault of this node — which is the property `FactsFault::Inconsistent` exists to carry, and its doc now names all three reads that raise it rather than only the height case it was written for. The entry vector is left to `vector(n)`'s value-initialization instead of being `memset` afterwards. Value-initialization zeroes every scalar and gives the pointer members real null pointers rather than an all-zero byte pattern that is only null by convention, so the `memset` added nothing — while being undefined at `txids_len == 0`, where `data()` may be null and passing null to `memset` is undefined even for a zero count. An empty `get_transactions` request is valid, so that length is reachable, and it now has a test. - **The release checklist's testnet consensus script reaches `get_transactions` again.** `scripts/check_testnet_genesis_consensus.py` wrapped the call in a `/json_rpc` envelope, but `get_transactions` is a REST endpoint — it has no row in either JSON-RPC dispatch table and never did on this bridge, so the script died at "Method not found" before its genesis comparison ever ran (RK-4c's field-name update sat on a call that could not succeed). It now POSTs to `/get_transactions` directly; the JSON-RPC helper stays for the methods that are dispatched there (`get_block_header_by_height`, `get_info`). Release-tooling only; no runtime or wire effect. - **A daemon refusal is no longer read as data.** `Rpc::rpc_call` and `json_rpc_call` only deserialize — they do not enforce the wire's `status` — so a daemon could answer a non-OK status *and* a complete, plausible body in one document, and five typed consumers read the body first: both `block_fetch` calls, both `proofs_chain_facts` fetches, and the reserve proof's `is_key_image_spent`. It matters most where the reply feeds a judgement rather than a display: a refusal carrying one plausible-length `spent_status` array changes a reserve proof's total, and one carrying a plausible entry lets a proof verify against a transaction the daemon just declined to vouch for. The own-node default narrows who can send such a document; it does not make it evidence. One `refuse_unless_ok` rather than five checks, so the refusal reads the same way everywhere and a sixth typed consumer has something to reach for. The test builds a body that clears every check *downstream* of the status — right count, matching `tx_hash`, a pruned blob `parse_pruned_tx` accepts — so without the check the call **succeeds** and hands back a transaction; that is the hazard, not a parse error arriving late. - **An undefined `where_found` is refused, not read as "not found".** The FFI contract permits 0/1/2; anything else was mapped to `Missed`, which answers the caller successfully about a transaction this daemon may well hold — an ABI violation rendered as a fact about their request. It now frees the owner and raises an internal facts error, as the block path already did for a length no allocation could have produced. The mapping moved to `slot_of`, a pure function beside the `unsafe` walk, because the walk needs a live `core_rpc_server` and this is the part with a decision in it. The pointer arithmetic stays at the call site; what crosses is already owned data — which is what makes the refusal testable at all. - **`key_images_spent` has the three-way test its logic needs.** The status values and the slot re-association are the whole of that function: unspent images are gathered with their positions, asked of the pool, and written back through `unspent_slots`. A request whose images are all one kind cannot tell a correct re-association from an off-by-one, so the fixture mixes chain-spent, unspent and pool-spent and puts the **pool hit last** — writing pool answers in arrival order then lands it on the wrong image, and does. The pool entry is seeded through the DB so `tx_memory_pool::init` builds `m_spent_key_images` the way production does, rather than the test asserting over a hand-set field. - **The C++ RK-4c replaced is deleted, not left callerless.** Retiring `on_get_transactions` was the last caller of `Blockchain::get_split_transactions_blobs` and of the `core::` wrappers `get_split_transactions_blobs`, `get_pool_transactions_info` and `are_key_images_spent[_in_pool]`. The first of those carries a pruning model that is **wrong for Shekyl**: it reads the prunable hash only when the prunable *blob* survived, and sets the hash to null when it did not — the exact inversion of what `prune_worker` does, and the same defect this slice corrected in the facts export. Leaving it callerless would be worse than never having written it. Pruning-and-serving-from-archive has no Monero counterpart, so the inherited C++ is a *first draft of a Shekyl system*, and a wrong first draft sitting in the tree is what the next port reads as the design. Deleted with its `core::` wrapper, its declaration and its explicit template instantiation. The reads underneath stay: `have_tx_keyimges_as_spent`, `check_for_key_images` and `get_transactions_info` all have live callers — the facts shim calls the last two directly, which is precisely what left the wrappers dead. `get_transaction_version` went with them and is disclosed as a different case: it was **already** dead on `dev`, with no callers and no header declaration, so it was swept under rule 15 rather than orphaned here. - **The docs that described the old trust boundary were swept with it.** The binding below left seven statements asserting the opposite — that a pruned body is "not re-hashed" and the daemon's label is "the only association handle" — across `block_fetch`, its suite, and the cutover design doc. A doc that contradicts an enforcement is worse than a stale one: it tells the next reader the boundary is weaker than it is, and a design section tells the next *slice* to build it that way. Review flagged three; a sweep for the claim found five in code plus two in the contract, and all seven are corrected at source. Two of those were the canonical contract rather than commentary: RK-D8's shape-preservation rule now records RK-4c's field retirement as a **narrow, satisfied exception** (parity green first, removal as its own commit, so the divergence still bisects — the discipline, not a waiver), and §6's `CORE_RPC_VERSION` baseline says 3.25 rather than the 3.22 it froze at, since a baseline that stops tracking the constant reads as a freeze. - **A pruned transaction body is bound to the hash that was asked for.** The batch parser checked the reply's `tx_hash` **label** against the request and never the body, so a daemon could serve any canonical transaction under the requested label: shape validation passes, the label matches, and a proof consumer then verifies outputs belonging to a transaction that may not be on this chain. The identity is now recomputed from the bytes. `Transaction::hash_with_supplied_prunable` is the Rust equivalent of `get_pruned_transaction_hash` — a pruned body has no prunable section, so plain `hash()` substitutes the null hash and returns an identity no transaction has. The reply's `prunable_hash` is the daemon's to choose too and gains it nothing: choosing freely leaves it solving `H(prefix ‖ base ‖ pqc ‖ X) = txid` for `X`, a keccak preimage rather than a substitution. The full form is bound the same way, via `hash()` directly. Both hashes share one construction rather than two, so the pruned and unpruned paths cannot drift into hashing the same transaction two ways. Two existing fixtures had to change, which is the finding confirming itself: they paired arbitrary labels with arbitrary bodies, and one served **the same body under two different requested hashes** — the substitution case, sitting in the happy-path test, passing only because the label was the whole check. They now derive the label from the body. - **The console stops replacing the daemon's reason with its own complaint.** A native handler reports failure as a `RestErrorEnvelope`, which the success types do not model, so two success-only decodes reported "malformed reply" over the server's stated cause. Refusing unknown fields made that certain rather than incidental — the envelope's `error` is precisely the field the success type does not model. Both sites now use `decode_reply`, which tries the envelope second, as `print_height` already did. - **The RPC read surface refuses unknown fields.** `shekyl-rpc-types::chain` and `::transactions` tolerated them because "additive daemon-side evolution must not break an older wallet" — not a constraint this tree has, since there is no network and every client ships with the daemon. What the tolerance bought was a **renamed** field arriving unnoticed while the name we look for defaults: a wrong value wearing the shape of a legitimate one, on replies that feed proof verification. Free to remove, and checked rather than assumed: every captured epee vector still parses with the denial on, so the types already modelled everything the daemon emits. It also aligns the daemon surface with the wallet-RPC decision (F-1), where an unknown key is `-32602` rather than a guess. `SubmitVerdict` keeps its tolerance, on its real reason rather than the compat one: a verdict arrives mid-submit, where a daemon and wallet from different in-tree builds must still agree on whether the transaction was accepted, and failing that parse turns an informational field into an ambiguous submit — the outcome the §2.3 skew design exists to prevent. `skew_c` pins it. **Not a fix for silent defaults**, and the docs say so: `#[serde(default)]` still lets an *omitted* field become its zero value, and 27 fields across the two modules do. That audit is filed separately because it needs per-field judgement — some absences are legitimate `KV_SERIALIZE_OPT` omissions the vectors depend on. - **`print_transaction` binds the reply to the request, not just its arity.** It accepted whatever single entry came back, so a daemon could answer with a different transaction — every field well-formed, only the identity wrong — and the console printed it under the operator's hash. It could also answer with an entry *and* report that hash missed, and the entry won. Now the entry's `tx_hash` must equal what was asked and `missed_tx` must be empty, which is the rule `parse_tx_batch` already holds the wallet's consumers to. These two cases need a canned reply rather than the projection-backed fixture: the projection only ever answers about the hash it was handed, so it cannot state a reply that contradicts the request. - **The pruned-spend test carries a real spend, so the retained PQC segment is actually exercised.** `has_pqc` is false for a coinbase (`txin_gen`), so a miner-only chain leaves `txs_pqc_auths` empty — the sibling test proved the prunable-hash half of `prune_tx_data`'s contract and was silent on the other. The new case adds a v3 spend with non-empty `pqc_auths` and asserts the property both retained items exist for: after the body is dropped, the chain can still **name** what it kept, via `get_pruned_transaction_hash(pruned, prunable_hash) == txid`. Re-deleting the segment fails it on that assertion. - **`print_transaction` stopped calling every confirmed transaction pruned.** The console asks whether the daemon still holds a transaction's prunable half, and reads `prunable_as_hex` to decide — a field only the **split** form fills. It was requesting the whole transaction, so the halves arrived concatenated in `as_hex` with `prunable_as_hex` empty, which is indistinguishable from "the daemon pruned it": every ordinary mined transaction printed as `(pruned)`. `split` here is not a display preference but what makes the label answerable, which is why the C++ console set it too. Covered end to end, by a fixture that answers with the **real projection** over fixed facts rather than a canned reply — a canned reply is blind to this defect, since it would return split-form data whatever the console asked for, and the test would pass over the bug. Reverting `split` reproduces `Found in blockchain at height 3 (pruned)` for a transaction whose prunable half is present, while the genuinely-pruned case stays green, so the fix is not the label being disabled. - **`get_transactions` and `is_key_image_spent` requests omit their empty sequences.** epee drops an empty sequence rather than emitting `[]`, for plain `KV_SERIALIZE` members as well as OPT ones — the response types in this module already said so, and the request types did not. The rule is the wire's, so it binds requests this tree *sends* exactly as it binds replies it serves. A sweep of every `Vec` field on every request type in the crate found these two and no others: `GetBlocksByHeightRequest.heights` derives no `Serialize` at all, so the attribute would have been decoration there. - **The `get_transactions` projection matrix is covered.** It replaced a C++ matrix and nothing reached it — the parity vectors build `TxEntry` directly, and the live console test only ever hits the genesis transaction's empty-prunable, `decode_as_json = false` corner. Table-driven now across `split` / `prune` / `decode_as_json`, the chain/pool/miss slots, and a renderer failure, with an injected renderer that echoes what it was handed so the assertions pin *what* was rendered and under which `base_only` — the `prune` case must render base-only, or the json leaks the half `prune` withheld. - **The proofs workflow's daemon-facing half is its own module** (`engine/proofs_chain_facts.rs`). Typing the daemon replies pushed `proofs.rs` to 1204 lines, over the decomposition ratchet's 1200 cap, and the gate offers two ways out — carve it, or baseline it. Neither was taken literally: the split is by responsibility, not by line count. Everything in the new module answers "what does the chain say about this transaction?" across an **untrusted boundary**, where a daemon may lie, omit, reorder, or answer with a body nobody asked for; what stays in `proofs.rs` is cryptographic work over facts already established. Different job, different failure mode. The seam is five items wide — four functions plus the record two of them return — and the not-found reporter stays private, since a caller needing it would be doing the new module's job somewhere else. The split also surfaced coupling worth removing: `proofs_tests.rs` had been reaching wire types (`Ct`, `Transaction`) through `use super::*`, inheriting a workflow module's third-party imports rather than naming its own. - **Documentation lifecycle is now a first-class process.** `docs/README.md` and `.cursor/rules/95-documentation-lifecycle.mdc` classify every doc (living contract / open plan / closed-as-record). Work-item targets are only **pre-genesis**, **post-genesis** (tiny, named blocker), and **V4** (NIST lattice-only, 2–5 years). Claim-era staking specs are deleted, not stubbed. Closed Stage-1 / Stage-0 plans and named audits moved to `docs/completed/`. `docs/FOLLOWUPS.md` is a one-liner queue again. - **`/get_blocks_by_height.bin` is served natively in Rust, and the binary FFI dispatch bridge is deleted (RK-4b).** With RK-4a's `/get_o_indexes.bin` this was the last `.bin` route reaching C++, so `dispatch_bin`, `bin_handler!`, `CoreRpc::bin_endpoint`, `core_rpc_ffi_bin_endpoint`, `core_rpc_ffi_free_buf`, the `DBIN` macro, `bin_fn` and `get_bin_table` all go: the binary half of the epee dispatch bridge is gone ahead of RK-X. `free_buf` had no caller once the endpoint that allocated through it was deleted, and the header still advertised binary endpoints returning buffers through it. The oracle capture settled a question the C++ declaration would have answered wrongly. `block_complete_entry` has five KV members and this handler sets two, so `pruned`, `block_weight` and `attestation_witness` never reach this wire — and with `pruned` false the map serializes `txs` as an **array of strings**, dropping each transaction's prunable hash. The array-of-objects form exists only on the p2p path, so modelling it here would have carried a variant the daemon cannot emit. Two behaviours are recorded rather than silently carried. A height the chain cannot produce still returns the blocks read **before** it, as the C++ did — it cleared its list once before the loop and returned from the failure without clearing again. And the restricted listener's 1000-block cap now **fires**: the C++ gated it on `m_restricted && ctx` while the bridge always passed a null `ctx`, so the check was dead and a restricted listener accepted any number of heights. That is a deliberate fix, not parity. The engine's timing rig stops hand-rolling the request and reply walk; `Section::collect_bytes_named`, a tree-scanning helper that existed for that walk, goes with its only caller, and `shekyl-engine-core` drops its direct dependency on the codec. ### Removed - **`core_rpc_ffi_is_restricted` and the accessor it was the only caller of.** The export had no caller in any language — the Rust server takes its posture from its own configuration, never by asking C++ — and it was the sole user of `core_rpc_server::is_restricted()`, so both go (rule 15). Found while fixing the restricted gate above: an export whose whole purpose was to report restrictedness across the boundary, in a tree where the restrictedness had never crossed it. - **`/get_transaction_pool_hashes.bin` is retired.** The binary spelling of a route that is called; nothing called this one. The two handlers made the same two core reads and differed only in raw-versus-hex output, so the surviving JSON route keeps them and nothing else went callerless. `CORE_RPC_VERSION` is **3.24**. Found by `ci/rpc-route-liveness` on its first run and held as an allowlist entry pending disposition; that entry is now gone rather than permanent — an allowlist entry is a claim someone has to defend, and there was no defence to make. Reopen clause in `docs/DAEMON_RPC_RUST.md`. ### Fixed - **A restricted RPC listener disclosed transactions the node had not broadcast.** Every C++ handler decides its caller's posture from `m_restricted && ctx`, and the dispatch bridge passed `nullptr` for `ctx` on every JSON and JSON-RPC route, so that expression was false however the daemon was configured. On the pool paths it is not a request cap: the sensitive flag selects `relay_category::all` over `::broadcasted`, and the DB's iteration skips what does not match the category — so the flag decides whether a transaction is enumerated at all, not which of its fields are shown. A `--restricted-rpc` listener therefore answered with transactions in the `stem` and `local` states: still-stemming ones, and the node's own submissions before they were relayed. `/get_transactions` disclosed them by hash; `/get_transaction_pool` and `/get_transaction_pool_hashes` enumerated them with no argument at all, which hands over the identifiers to ask about. The listener's own help text is "do not return privacy sensitive data in RPC calls". The bridge now passes a shared origin context, which restores the intended meaning for every bridged handler at once; the unrestricted listener is unaffected, because `m_restricted` is false there and the expression was false before and after. No wire shape changes, so `CORE_RPC_VERSION` is untouched. (`do_not_relay` transactions are *not* in the leaked set — they cannot exist in Shekyl: no RPC accepts the flag and the pool's only writer of it hardcodes 0.) - **The restricted-RPC gate now has a guard that CI actually runs.** The only assertion that can observe whether the C++ dispatch bridge passes a connection context is a live-daemon test, and live-daemon tests are `#[ignore]`d because the Rust lane builds no `shekyld`. The `build-ubuntu` job compiles one with `BUILD_TESTS=ON` and installs the Rust toolchain, so the gate runs there against the tree just built — no extra build, about three seconds. The step asserts that exactly one test ran: `--exact` matches the full test path, and `cargo test` exits 0 reporting "0 passed" when a filter matches nothing, so a moved or misspelled name would otherwise turn the gate into a green no-op. - **`relay_tx`'s C++ restricted gate is deleted, not repaired.** The handler computed `m_restricted && ctx` and skipped the `relay_category::all` arm when it held. That expression cannot hold in any reachable state: `relay_tx` is admin-only, decided once in Rust at the only transport (`RESTRICTED_METHODS`), which answers 403 before C++ is entered — so the restricted listener never arrives, and on the admin listener `m_restricted` is false. An earlier draft of this entry reported it as a live unauthenticated relay, and a later one kept the check as defence in depth; both were wrong. It defended nothing, and membership of the Rust list is itself pinned against an independent specification by a test, so loosening that gate fails in Rust rather than falling through to here. Removed under rule 15, which takes the site count in the design doc from eleven to ten. Of those eleven methods `relay_tx` is the only one Rust gates per-method, so this is a bounded sweep rather than an open class. - **Every password rotation on Windows failed, and the crate that owns the defect was tested on no Windows machine anywhere.** `rotate_password` replaces `.wallet.keys` through the atomic writer while the wallet handle still holds `LockFileEx` on byte 0 of it. `NamedTempFile::persist` issues `MoveFileExW(MOVEFILE_REPLACE_EXISTING)` and nothing else, and a byte-range lock on the target makes that `ERROR_ACCESS_DENIED` — surfaced as `AtomicWriteRename` carrying os error 5, flattened by the RPC's classifier into `-32603 "password rotation failed"`. `std::fs::rename` issues the same call *and* retries on `ERROR_ACCESS_DENIED` through `SetFileInformationByHandle(FileRenameInfoEx)` with POSIX semantics, which supersedes the open target rather than deleting it, so step 4 is now `TempPath::keep` + `rename`. The `keep` is not a cleanup formality: it is the `SetFileAttributesW(FILE_ATTRIBUTE_NORMAL)` that `persist` performed as its own first step, and without it the staged file's `FILE_ATTRIBUTE_TEMPORARY` — which asks the cache manager to avoid writing the data back while cache is available — rides onto `.wallet.keys`, leaving a permanent wallet artifact permanently marked temporary. It does **not** undo the preceding `sync_all`, which is `FlushFileBuffers` on Windows and flushes unconditionally; what it misdescribes is the file's whole subsequent life, as false metadata and as a standing hint to any later writer that does not flush explicitly. `keep` also disarms `tempfile`'s cleanup, so a failed rename now unlinks the staged file explicitly instead of stranding it beside the wallet. This is the same class as the mandatory-lock bug in the keys-file read: POSIX-shaped reasoning about file replacement, invisible on Linux because `imp::keep` is a no-op there and `rename(2)` never cared about an advisory `flock`. Four tests pin it — `persist` must stay refused over a locked target on Windows and `rename` must stay able to supersede it, the target must never be left marked temporary, and the whole rotation shape must work under a live `KeysFileLock`. Known limitation, stated rather than discovered: the `FileRenameInfoEx` path needs NTFS, so rotation with a live lock still fails on exFAT or a share without POSIX-semantics rename. - **Two `shekyl-engine-file` test oracles read the keys file through a second handle**, which is `ERROR_LOCK_VIOLATION` on Windows for the same mandatory-lock reason, so they could not run there at all. They now read with no handle open. Reading the handle's cached envelope would have been worse than leaving them broken: it would make a test whose entire purpose is detecting an on-disk rewrite blind to one. - **`AtomicWriteRename` was reported for a step that may never have run.** A `TempPath::keep` failure — the staged file's `FILE_ATTRIBUTE_TEMPORARY` clear on Windows — was wrapped in the variant whose own documentation says it carries the error from `rename(2)` specifically, so an error naming the swap could be raised before any swap was attempted. Split into `AtomicWriteFinalizeStaged`, because the two have different causes and different remedies and one name cannot carry both. This is not only tidiness: `keep()` leaves `tempfile`'s cleanup guard *armed* while a failed rename does not, so the conflation had made the cleanup-arm test unable to tell, on Windows, whether the arm it guards had run at all. Fixing the error model removed that gap instead of documenting it. Also corrected: the module and variant docs claimed the staged file "is removed" on failure — cleanup is best-effort from both owners (`tempfile`'s guard ignores unlink errors; ours logs and continues). The guarantee is that the **target is byte-unchanged**; a stray `.shekyl-tmp` sibling is possible, and is clutter rather than a wallet artifact. - **`BuildRust.cmake`'s Windows skip was justified by a blocker that no longer exists.** Its comment said the Rust wallet stack was Unix-only, that the Windows port was still an open design question (named-pipe ACLs vs. UDS), and that building there would fail on the first of many sites. WP-Q1 ruled the pipe, WP-W2 shipped it, and both binaries were observed building and linking for `x86_64-pc-windows-msvc` with `rpc_session_e2e` passing. The rationale is corrected; **the gate is deliberately not flipped**, because compiling is not installing and the CMake path, staging, `install()` and the archive layout are all still unverified. WP-W5 owns that removal. - **`shekyl-engine-file` joined the Windows scouting step.** It owns the atomic write, the keys-file lock and password rotation — every Windows-specific file primitive the wallet has — and no Windows lane covered it. The rotation bug therefore surfaced as an opaque `-32603` from a `shekyl-wallet-rpc` lifecycle test instead of as four red tests in the crate that owns it. - **The submit-shim fixture paid a fee it never put in the transaction.** `make_tx` derived a floor-clearing fee, asserted it cleared the floor, and stored it beside the transaction — while the transaction itself kept the shape builder's hardcoded `txnFee = 1000000`. Both readings then existed at once: `commit_tx` is handed the derived fee as an argument, so every shim test paid it, and `tx_memory_pool::add_tx` reads `get_tx_fee(tx)`, so the one test exercising that path paid 0.001. The divergence was invisible until the dynamic minimum rose past the constant, at which point `legacy_add_tx_double_spend_pin` failed on a fee rejection while claiming to pin double-spend classification. The fee now settles **into** the transaction — writing it changes the blob length, which changes the fee required, so it iterates to a fixed point — and the assertion is on the value `add_tx` will actually read rather than on the local the test computed for itself. The bond-post builder, which had the same derive-and-discard, shares the settle. ### Changed - **`/get_o_indexes.bin` is served natively in Rust, and the `.bin` wire now has a typed schema layer (RK-4a).** `shekyl-portable-storage` is the codec; nothing mapped RPC commands onto it, and `shekyl-levin`'s schema layer is for p2p by design. `shekyl-rpc-types::bin_commands` is that layer, beside the JSON types, because one method has one definition whichever encoding it speaks. Two rules came from capturing epee's bytes rather than reading its declarations, and neither is visible in the latter: a `KV_SERIALIZE_VAL_POD_AS_BLOB` hash crosses as **32 raw bytes** where the JSON side spells it as 64 hex characters, and an **empty sequence is absent** rather than `[]`. Both are pinned against epee's own bytes, compared as bytes — a byte-exactness claim checked through a text pipe tests the pipe. The wallet client's hand-rolled request build and reply walk are gone for the shared map, and it no longer depends on the codec directly. The reply shape is unchanged, including that an unknown transaction is a 200 carrying a non-OK `status` rather than a transport error. ### Removed - **`/get_blocks.bin` (+ `/getblocks.bin`) and `/get_hashes.bin` (+ `/gethashes.bin`) are retired (RK-4x).** They were wallet2's batch sync; `src/wallet/` is gone and nothing called them — not Rust, not C++, not the python framework, not the functional tests. The Rust wallet fetches one block per height over `get_block`. Daemon-to-daemon sync is unaffected: that is Levin, through a different `find_blockchain_supplement` overload. The retirement reaches further than the routes. `on_get_blocks` was the only caller of `core::get_pool_info`, which was the only reader of the transaction pool's **departure history** — a timestamped in-memory record of when each transaction left the pool, each entry flagged sensitive or not, kept to serve incremental pool deltas to that batch sync. That is the timing correlate the relay-privacy work exists to deny, and it was being retained for an endpoint nobody called. It is gone, along with the 9-argument `find_blockchain_supplement`, `get_pool_info` at both levels, and the two `COMMAND_RPC_GET_BLOCKS_FAST_MAX_*` limits. `CORE_RPC_VERSION` is **3.23**: retiring a served route is a wire change. **Reopen** iff a named live consumer needs batch sync over RPC, and then as a typed Rust route designed against what FCMP++ scanning needs — never a restoration of the epee structs (`docs/DAEMON_RPC_RUST.md`). ### Added - **Archival staking parity on wallet-RPC and CLI (WI-RPC-5).** Three new wallet-RPC methods over Engine surfaces that were built but unwired: `stake_in` (fund the staking balance with an ordinary principal transfer; same pending-tx confirm/submit path as `build_pending_tx`; carries the GF-7 change-co-presence disclosure in the contract and the CLI pre-confirm print), `get_drain_balance` (two-armed: `ready` with the spendable amount, or `syncing` — never `"0"` while unanchored), and `drain` (`{amount}` only — **no fee, destination, or `p_slot` parameters**; the fee is the canonical P-lane floor computed engine-side, the destination is engine-pinned to the principal, and the persona is resolved from live actor state by the new `Engine::drain_to_principal` façade, which keeps the DS-4 `EXIT_FEE_RESERVE_ATOMIC` gate on live-persona drains). The three schemas set `additionalProperties: false` and the params structs use `#[serde(deny_unknown_fields)]`, so extra keys answer `-32602` instead of being silently dropped; drain application errors are pinned at `-29507..-29511`. `get_balance.staked` and `.claimable_rewards` are now live projections from the staking view instead of hardcoded `"0"`. New CLI commands: `get_tx_note`, `set_tx_note`, `abandon`, `stake_in`, `drain_balance`, `drain`. Claim-era registry names resolved: `claim` and `get_stakes` are REJECTED (engine-side claims; archival firewall); `unstake` stays RESERVED — its gate is now **reachability plus the regtest walk**, not the producer, which exists (`build_unbond_vin` / `AssembleUnbond`) and is deliberately unreachable. The drain confirmation/prune driver remains a FOLLOWUPS item — the receipt is a dispatch fact, not a settlement fact, and both surfaces say so. - **A CI gate that makes the RPC liveness rule executable (`ci/rpc-route-liveness`).** `DAEMON_RPC_RUST.md` has always said a method is live iff it has a route *and* a live consumer. Stated in prose, nothing enforced it, which is how two callerless endpoints survived an audit that had the rule written down. The gate now requires every path in the Axum route table to have a reference outside route registration, the FFI dispatch table, and comments — or an allowlist entry stating why it is served without one. It found a third dead route on its first run, `/get_transaction_pool_hashes.bin`, which is retired above — the allowlist it was held in is now empty of exemptions. ### Changed - **CI PR fan-out cut (`chore/ci-fanout-cut`).** A typical non-docs PR was standing up ~15 workflows / ~30 jobs / ~500 billed minutes, and four concurrent PRs plus a `dev` push queued the shared runner pool for hours. The merge-blocking graph is now: Ubuntu 22.04 compile-only + Ubuntu 24.04 build whose tests consume the artifact (no second cmake/ctest rebuild) + macOS + Windows MSVC; ARM v8 qemu KATs only when the three cross-arch crates change; `make depends` only when `contrib/depends` (or Makefile) changes, with the six-target matrix on a nightly; eight grep/source-scan gates in one job; CodeQL Rust on PR, C++/Python weekly. Arch Linux and Windows MSYS2 moved to `weekly-os.yml`. `source archive` still runs on PRs into `main` (required check) and is skipped on PRs into `dev`. The MSRV cargo check is the rust-audit-test job at `rust-toolchain.toml` 1.94, not a second compile. Push-to-`dev` no longer double-runs the OS matrix or the rust job (the PR event already tested the merge). Dispatch-only leftovers: `bench-runner-bisect.yml` deleted; `rust-stable-canary.yml` schedule removed until `cargo +stable fmt --check` has an owner. Economics C2a′ paths glob by naming convention (`src/shekyl/economics.*`, `tests/unit_tests/economics*.cpp`, `tests/unit_tests/archival_*emission*.cpp`) rather than a frozen file list or `src/shekyl/**`. The Ubuntu 24.04 CTest-artifact verify step fail-closes if CMakeFiles/objects/archives leak into the tarball. - **`get_block` is served natively in Rust, and the two console commands that read it move with it (RK-3b, `docs/design/DAEMON_RPC_KV_CUTOVER.md`).** Both aliases answer from `shekyl-daemon-rpc`, by hash or by height, over a new facts export that reads the block under one acquisition of the chain lock. `print_block` renders in Rust now — the same lines in the same order, with the reward in SKL, the timestamp in UTC keeping its `` cutoff, and the difficulty decimalised from the wide form. Porting the console in the same change is the point: it was the last C++ caller that would otherwise have parsed a Rust-produced reply through the old struct. Every refusal keeps its wording as well as its code, including the inherited `Hash = .` a by-height failure produces. - **The first variable-length facts payload.** A block's blob, transaction list and rendered `json` have no size the caller can know in advance, so they are owned by one C++ allocation and released by one paired free — the shape the hard-fork table already used, now written down as the rule for every payload of this kind. epee's `json` rendering is carried through untouched rather than reimplemented: it renders the whole transaction structure, and it is already queued for deletion along with the field itself, which duplicates `blob`. - **`get_block_header_by_height` is served natively in Rust (RK-3, `docs/design/DAEMON_RPC_KV_CUTOVER.md`).** Both aliases answer from `shekyl-daemon-rpc::methods` over a new facts export that reads the bound, the block, its weights and both difficulties under **one** acquisition of the chain lock, so every field of a header describes the same chain state. The reply is unchanged, pinned by oracle vectors captured from epee first — including the wire's three-field rendering of each 128-bit difficulty and the asymmetry where `block_weight` and `long_term_weight` vanish at zero while `block_size`, filled from the same source, stays. A height past the tip keeps its `-2` refusal naming the top height; a store that reports a height it cannot produce the block for keeps the contract's `-5` and wording, and is logged. The wallet client's private header struct retires for the shared type and now rejects a non-OK `status` instead of reading past it. This carries the shared `BlockHeader` that the remaining header methods will reuse; they stay in C++ for now because each is read by a console command that also needs `get_info` (see the design's console matrix). - **Hash fields on the daemon RPC wire are typed (`HashHex`).** Every 32-byte hash the RPC carries was a `String`, which admitted "not a hash at all" as a value and left each consumer to parse hex for itself. `HashHex` moves that parse into the deserializer once: 64 hex characters of either case in, always lowercase out, anything else refused. It is a wire-level type over raw bytes, not a wrapper around a domain hash — a single `block_header` carries block, transaction, root and proof-of-work hashes, so consumers name the kind at their edge, where they know it. `block_header.pow_hash` becomes `Option`, keeping the daemon's "was it filled?" flag alive to the wire while emitting the same `""` epee did. Emitted bytes are unchanged throughout, which the oracle vectors check. Two hand-written test fixtures that described replies the daemon could never send were caught by the new type and now build through it. - **`--daemon-address` says §1's thing at the point of configuration (RT-W7).** A daemon address that is not loopback now draws the operator statement of `RPC_TRANSPORT_POSTURE.md` §1 when the wallet starts — whoever operates that daemon sees which blocks the wallet requests, when, and what it broadcasts; that is what a daemon is told in order to serve, and no proxy or encryption changes it; there is no recommended configuration with a daemon somebody else controls — in `shekyl-cli` (on stderr) and `shekyl-wallet-rpc` (in its log) alike. `--proxy` silences the existing network-path warning, never this one: a proxy hides the path, not the wallet from its daemon. The statement asserts only what an address can say ("is not a loopback address"), so the operator of their own remote node reads it as true of themselves; loopback and unix-socket daemons stay silent, and no configuration draws an assurance. The disclosure module moved from `shekyl-cli` into `shekyl-rpc-transport` (`network_posture`) so both binaries say it in the same words — the outbound twin of the shared `listen` classifier — and it is pure: each binary emits in its own voice. The two sides now share one loopback predicate (`listen::is_loopback_ip`), so `[::ffff:127.0.0.1]` is loopback for a daemon address as it is for a bind; a zone-scoped literal (`fe80::1%eth0`) is a literal, not a name to leak; and bare `socks://` counts as local-resolving, as the dialers that accept it read it. **Also:** both wallets' `--daemon-address` defaults named ports no daemon serves (`localhost:11028`, `http://127.0.0.1:28581`). The flag is now optional and its default follows `--network` — this machine's daemon at that network's RPC port (`Network::daemon_rpc_port`, pinned by a test to the daemon's `cryptonote_config.h`) — so a testnet wallet finds a testnet daemon without the operator knowing the port. - **The daemon RPC binds loopback only; `--confirm-external-bind` is retired (RT-W2).** A wildcard bind (`0.0.0.0`, `::`, the IPv4-mapped spellings) is refused unconditionally — consent to interfaces that do not exist yet — and a bind on a specific network address is refused because the daemon RPC has no authentication of any kind: every RPC leg is operator-to-operator, and the daemon's remote leg is the onion service or a reverse proxy outside the daemon — the crossing is never the daemon's own socket. The refusal lives at the one Rust seam every daemon listener passes through (`shekyl-daemon-rpc::bind::bind_listener`, the restricted listener included — it had no gate at all), on a strictly parsed address (hostnames are not resolved), and is logged with its reason before the daemon exits. `--confirm-external-bind`, a permission slip for exactly these binds, is refused by name through the removed-flags shim: confirmation is not refusal. One listen classifier, `shekyl_rpc_transport::listen`, now serves the wallet RPC and the daemon RPC, so "wildcard" and "loopback" mean one thing across the tree. IPv6 is a first-class family: `::1` is loopback; `--rpc-use-ipv6` binds `--rpc-bind-ipv6-address` (default `::1`) as a second socket on the same FFI start, not a second C++ server. A network IPv6 bind is refused for the same reason as IPv4 — no authentication. **The FFI boundary moves with it:** the daemon's `--rpc-bind-ip` / `--rpc-bind-port` / `--rpc-bind-ipv6-address` go to Rust as given, which parses them and validates the connection caps; the C++ IP-parse blocks, bracket stripping, `host:port` composition and cap checks are deleted. One server serves every socket of a start (`serve_listeners`): the connection caps are per server as the flags say, `get_info` counts every family, and the stop signal is a level-triggered `watch`, so a stop cannot be lost to an acceptor that has not been polled yet. `--rpc-ignore-ipv4` — which parsed into a field nothing read — is retired by name alongside `--confirm-external-bind`, and the removed-flags shim now answers for a config-file flag too, the route every shipped unit file uses. The container image binds RPC on loopback and no longer exposes 11029; the documented recipe is `docker run --network host`, so the container's loopback is the host's and nothing in the daemon changes (a daemon UDS listener is filed with its trigger, not built). An explicitly empty `--rpc-bind-ipv6-address` under `--rpc-use-ipv6` is refused by name rather than silently dropped, and a refusal on the separate restricted listener names the `--rpc-restricted-bind-*` flags it came from. **One behaviour change in that move is a fix, not a port:** the C++ cap check refused any `--rpc-max-connections-per-public-ip` / `-per-private-ip` value under `--rpc-max-connections 0` (unlimited) as "bigger than" the total; an unbounded total contradicts nothing, so `ConnLimits::checked` accepts it and refuses only a per-IP cap above a *bounded* total, by flag name. - **An unencrypted `enc_label` or `enc_amount` is now unrepresentable, not merely wrong.** Both are XOR ciphertexts under a one-time per-output key, and both were plain `[u8; 9]` fields on `OutputInfo` assembled by a hand-written "copy eight bytes, append the tag" block repeated at eight call sites — including the genesis block builder. The live path was already correct (`sign_bridge.rs` took its values straight from `construct_output`), so this fixes no leak; it removes the possibility. `EncryptedOutputField` (`shekyl-crypto-pq/src/encrypted_output_field.rs`, re-exported from `output`) has no public byte constructor. The in-process constructors are `OutputData::enc_label_wire()` / `enc_amount_wire()`, which return the value `construct_output` assembled at the moment of encryption — the field on `OutputData` *is* the type, not an 8+1 pair a late accessor re-wraps. `Deserialize` is the other route, and it exists for the FFI JSON boundary. The hazard was never a weak plaintext — an all-zero *plaintext* encrypts to `k_label[..8]` and is uniform, exactly as the sentinel does. It was bytes that never met the encryption, which are a constant on the wire and mark every output carrying them, breaking the `SUBADDRESS_UNDER_PQC.md` §5.7.10 indistinguishability invariant. "Reject zeros" was considered and rejected as the remedy: it treats one symptom, admits any other unencrypted constant, and can fire on a legitimate ciphertext that happens to be zero. The guarantee is compiler-enforced on three axes, all verified by attempting the forgery from outside the crate: `EncryptedOutputField([0; 9])` is `E0423` (the inner field is private), overwriting a real `OutputData`'s stored field is `E0616` (`pub(crate)`), and building an `OutputData` literal is `E0451`. Those attempts are committed as compile-fail tests (`shekyl-crypto-pq/tests/trybuild/`), which **retracts this entry's earlier judgement** that a `trybuild` harness was not worth adding for them. Two things made that judgement wrong. The cost was overstated — `trybuild` was already a dev-dependency of `shekyl-logging` and so already in `Cargo.lock`, adding no package to the supply chain. And the alternative it chose was documentation: a comment cannot fail, and widening a field back to `pub` would have left every existing test passing while the guarantee evaporated. A property this crate names in its own title deserves a check that goes red. The routes live in separate fixtures because a field-privacy error aborts the compilation before later bodies are type-checked — sharing a file, later routes vanish from the snapshot and would have been guarded by nothing. One exception survives, and it is a boundary rather than a constructor: the type's `Deserialize` impl, used where `shekyl_sign_fcmp_transaction` takes its outputs as JSON and the far side computed the encryption. The byte constructor behind it is private to the defining module, so no crate can call it. That path's only non-test caller is the C++ `construct_tx*` chain, which has had no production caller since `wallet2` was deleted, and it goes with the consensus-oracle harness. The JSON encoding is unchanged — still an 18-character lowercase hex string, now pinned by a test — so the FFI contract is untouched. - **`get_block_count` and `on_get_block_hash` are served natively in Rust (RK-2, `docs/design/DAEMON_RPC_KV_CUTOVER.md`).** Both aliases of each (`getblockcount`, `on_getblockhash`) answer from `shekyl-daemon-rpc::methods` over the typed facts FFI; the C++ handlers, their `COMMAND_RPC_*` structs and the hand-rolled JSON-array parser in `core_rpc_ffi.cpp` are deleted. Replies are unchanged, pinned by oracle vectors captured from epee first — including that `on_get_block_hash` answers with a bare JSON string rather than an object. Its refusals keep their JSON-RPC codes (`-1` wrong parameter, `-2` height too large) and messages; the too-large message still names the top height, now read in the same call as the hash so the two cannot disagree. **One deliberate change:** a *negative* height is answered `-1` (wrong parameter) instead of `-2` — the old classification came from `std::stoull` wrapping the value in the deleted parser, not from a decision. - **Daemon RPC Phase 2 begins: `get_height` and `get_version` are served natively in Rust (RK-1, `docs/design/DAEMON_RPC_KV_CUTOVER.md`).** The wire types are now `shekyl-rpc-types::{GetHeightResponse, GetVersionResponse}` — one definition for the daemon that serves them, the wallet that reads them and the console that renders them — and the handlers read core state through a typed facts FFI (`shekyl_rpc_chain_tip`, `shekyl_rpc_hardforks`) instead of C++ `COMMAND_RPC_*` structs marshaled by epee. Parity with the old handlers is pinned by oracle vectors captured from epee before the C++ was deleted; the JSON documents are parsed-equal (same keys, values and `KV_SERIALIZE_OPT` omissions; key order and whitespace are no longer epee's). `CORE_RPC_VERSION` (3.22, unchanged) moved to Rust with its only reader. The console's `print_height` renders in Rust on both arms (`shekyld print_height` and the interactive console). Also deleted as dead: `COMMAND_RPC_GET_OUTPUTS{,_BIN}`, `COMMAND_RPC_FAST_EXIT` and the `core::get_outs` / `Blockchain::get_outs` chain behind them — none had a dispatch row or a caller. No change to any reply a client parses; the reason text inside a transport-level failure (the 500 envelope's `error` string) now names what actually failed instead of "FFI dispatch failed" — diagnostic text, not contract (RK-D8 scope). - **The legacy FCMP++ prove seam is gone.** `genRctFcmpPlusPlus` — the C++ end of the retired C++ → Rust → C++ → Rust proving round-trip — had no caller: production signing moved to `shekyl_sign_fcmp_transaction` (one call), and `core_tests/chaingen.cpp` stopped calling it when the `gen_fcmp_*` tests were deleted. It is deleted with the exports that existed only to serve it (`shekyl_fcmp_prove`, `shekyl_fcmp_proof_len`) and their C++ declarations, including `SHEKYL_PROVE_WITNESS_HEADER_BYTES`, `ShekylFcmpProveResult` and `ProveInputFields`. **No production path changes** — the Rust prover is untouched; this removes a C++ orchestration wrapper and an FFI surface, per [`CT_SURFACE_NAMING_PIN.md`](design/CT_SURFACE_NAMING_PIN.md) §5 step 1. The witness *format* survives and moved rather than died: the FROST multisig coordinator still parses it, so `parse_prove_witness`, its helpers, `SHEKYL_PROVE_WITNESS_HEADER_BYTES`, the `shekyl_fcmp_build_witness_header` writer, `ProveInputFields` and the `WITNESS_HEADER.json` round-trip test are all now `#[cfg(feature = "multisig")]`, which puts the dependency where the compiler enforces it instead of leaving a parser that nothing in a default build reads. Its wire-format spec — the only prose description of the layout — moved onto `parse_prove_witness` **with a correction**: it had documented a 224-byte, 7-field header, omitting the commitment mask, where the real header is 256 bytes and 8 fields. `fill_construct_tx_rct_stub` is deliberately **not** deleted: the `construct_tx*` chain that calls it is the C++ consensus oracle's transaction factory, and retiring that is the oracle lane's work. - **`shekyl-wallet-rpc` refuses two listen configurations it used to accept** (RT-1 / RT-2, `docs/design/RPC_TRANSPORT_POSTURE.md`). A wildcard `--rpc-bind` (`0.0.0.0`, `::`, `[::]`, and their IPv4-mapped spellings) is refused at startup: a wildcard bind is a bind to interfaces that do not exist yet — the VPN that comes up tomorrow, a hotspot, a container bridge — so it is consent on behalf of the operator's future self, which is why it is a refusal rather than the daemon's one-time `--confirm-external-bind`. And a non-loopback bind with authentication disabled is refused: there is no deployment in which an unauthenticated wallet RPC that the network can reach is acceptable. Both rules live in one `validate_listen`, consulted by both bind paths (`run_server` and `spawn_in_process_with`), and the tests prove the wiring at each. Loopback with auth disabled and the Unix socket are unchanged. `--rpc-login` must be `NAME:PASSWORD` with both halves non-empty: `:` used to yield a blank Basic credential that the listener counted as authentication (`Basic Og==` satisfied it), and a value without `:` used to yield a username with an empty password; both are now refused by name at the parser, and the credential type (`BasicCredential`) cannot be constructed blank, so the bind seam has nothing to re-inspect. `--rpc-login` together with `--disable-rpc-login` is refused as a contradiction rather than resolved by precedence; `--rpc-login=` is a given (and refused) value, not an omission. Off loopback, Basic over cleartext is permitted until RT-W4 and logged as a warning naming what it costs (RT-3). The listener now accepts `Content-Type: application/json` only (415 otherwise): a browser can send a cross-origin `text/plain` POST to loopback with no preflight, and default-deny CORS hides only the response — so this gate, not the credential, is what keeps a web page from driving the auth-less loopback listener. Its other half closes DNS rebinding: where authentication is disabled, the `Host` must be an IP literal or `localhost` (421 otherwise), because a rebound hostname makes a page's JSON fetch same-origin with no preflight to refuse. This is slice RT-W1 of the RPC transport posture (RULED 2026-08-21): every RPC leg is operator-to-operator and the adversary is the network path; remote legs become pinned mutual TLS with a server-side fingerprint allowlist (RT-4, external PSK rejected on four hazards with RFC 8446 / 9257 / 9258 anchors read), Tor in addition to TCP under one auth story, and `--public-node` slated for removal — with two probes (RT-P1, RT-P2) pre-registered before they run; a third, RT-P3, was withdrawn when its premise proved false. The `shekyl-wallet-rpc` sections of `EXECUTABLES.md` and `USER_GUIDE.md` are rewritten against the Rust binary: they had described the retired C++ server (`--rpc-bind-port`, `--rpc-ssl*`, `--confirm-external-bind`, digest auth), none of which exists. - **The inherited `rct` confidential-transaction surface is now spelled `ct`.** `src/fcmp/rctTypes.{h,cpp}`, `rctOps.{h,cpp}`, `rctCryptoOps.{h,c}` and `rctSigs.{h,cpp}` are now `ct_types`, `ct_ops`, `ct_crypto_ops` and **`ct_semantics`**; `namespace rct` is `namespace ct`; `rctSig` / `rctSigBase` / `rctSigPrunable` are `CtSig` / `CtSigBase` / `CtSigPrunable`; `transaction.rct_signatures` is `ct_signatures`; and the `serialize_rctsig_*`, `verRctSemantics*`, `is_rct_*` families follow. The name asserted a construction Shekyl does not have — there is no ring, and the module it named does verification, not signing, which is why the verifier is `ct_semantics` and not `ct_signatures` (that spelling would have re-committed the "signatures" half of the misnomer). Disposition and the full was→now table: [`CT_SURFACE_NAMING_PIN.md`](design/CT_SURFACE_NAMING_PIN.md). **Rename-only; no consensus byte moved.** The `binary_archive` variant tags are numeric (`0x90`–`0xa0`) and untouched, so no serialized byte depends on a renamed identifier. **JSON-visible:** the `json_archive` / `debug_archive` variant-tag *strings* changed with the type names (`"rct_key"` → `"ct_key"`, `"rct_rctSig"` → `"ct_CtSig"`, `"rct::key"` → `"ct::key"`, …), affecting `decode_as_json` / tx-pool JSON — the same pre-genesis-free public surface the 2026-07-11 tag rename disclosed. The dead `using ct_signatures = rct::rctSig;` alias in `cryptonote_basic.h` was deleted with the sweep. `genRctFcmpPlusPlus` and `fill_construct_tx_rct_stub` keep their names deliberately: they are deletion targets, not rename targets, and are the only `Rct`-spelled identifiers left under `src/fcmp/`. - **`shekyld ` reaches the running daemon through the Rust transport.** The control client (`shekyld status`, `exit`, `print_height`, …) dialed the daemon's loopback RPC with epee's `http_simple_client`; it now goes through `shekyl_daemon_ctl_post` (`shekyl-daemon-rpc/src/ctl_client.rs`, over `shekyl-rpc-transport` — the same client the wallet dials the daemon with). The C++ side keeps only its request/response structs and their JSON framing (`src/daemon/rpc_client.h`). Scope is exactly the control path's: plaintext loopback, no credentials, no TLS — the daemon registers neither `--rpc-login` nor `--rpc-ssl*`, and the login/TLS parameters that were threaded through `t_command_server` → `t_command_parser_executor` → `t_rpc_command_executor` only to be passed as empty are gone with the client they fed. A failed request now names its cause (`connection error (… Connection refused …)`) instead of "Couldn't connect to daemon". No operator-visible change on the happy path. - **`shekyl-rpc-transport` builds no TLS layer for a plaintext endpoint.** Every endpoint used to be wrapped in a native-roots TLS connector, so a host with no root store (a minimal image, a freshly provisioned device) could not construct a client for a plaintext `http://` daemon at all. An `http://` endpoint now gets a bare connector with `enforce_http(true)`; only `https://` loads roots. A plaintext client refuses an `https://` request URI before the connector (direct and SOCKS arms share the check). - **The block-reward weight penalty moved to Rust; `get_block_reward` is now a marshaling shim.** It was the last economics arithmetic C++ performed itself — `mul128` plus two `div128_64` on an amount — while the base subsidy, burn and emission split already delegated to `shekyl-economics`. `shekyl_block_reward` now performs the base subsidy, the "make it soft" median clamp, the penalty and the above-`2 × median` rejection in one call. It is the first fallible entry in the economics FFI family (rejection is a consensus outcome, not an error, and is reported as a positive status; caller misuse is negative). The supply-advance clamp, previously written out at both the main-chain and alt-chain connect paths, moved with it as `shekyl_advance_already_generated`. Values are pinned across the boundary by an 81-vector KAT asserted from **both** languages, derived independently of the C++ implementation and landed *before* the move. - **`compute_fee_burn` and `compute_emission_split` no longer take an `hf_version`.** Their `hf_version < HF_VERSION_SHEKYL_NG` guard could not execute: the constant is 1, each network declares exactly one fork entry at version 1, and no code path produces a lower version. The dead branch and the parameters that existed to feed it are removed. A future hard fork that changes economics semantics reintroduces gating in `shekyl-economics`, beside the math it selects. ### Fixed - **Opening a wallet on Windows failed with an internal error (`-32603`) on every attempt.** `WalletFile::open` took the keys-file lock and then read the file through a *second* handle (`std::fs::read`). The lock is `fd-lock` over `LockFileEx`, which on Windows is a **mandatory** byte-range lock — the locked byte cannot be read through any other handle, the same process included — so the read failed with `ERROR_LOCK_VIOLATION`, surfaced as a generic I/O detail, and reached the RPC as `-32603`. POSIX `flock` is advisory, so nothing on Linux or macOS could see it; the Windows CI scouting run of PR #526 did, on the first wallet `open` ever executed there. The read now goes through the handle that holds the lock (`KeysFileLock::acquire_and_read`) — one handle, and the bytes of the file that was locked rather than whatever sits at the path a moment later. A unit test pins the platform fact (a path read while locked fails on Windows, succeeds on POSIX), and `lock.rs` no longer claims the lock is advisory everywhere. The review of that fix found the sibling: `WalletFile::verify_password` — the first-stake entry's verify-before-close — read the path lock-free *by design*, which is the same second handle, so every first stake on Windows would have failed the same way. Verification now runs on a snapshot of the sealed envelope taken from the open handle (`WalletFile::sealed_keys_envelope`) — the keys bytes that handle read under its lock — and, in the RPC, only after the engine lock that guards the handle is released, so the Argon2id derivation never holds the engine's writers up. Review then found the lock's oldest gap: it is held on an inode, and a password rotation replaces the keys file's inode, so after `change_password` a second open of the same wallet succeeded on POSIX. The rotation now locks the staged file before renaming it into place (the atomic writer gained a pre-persist hook), so the lock follows the file; a regression test pins it. The Windows scouting step that found it reported **success** on the run page — `continue-on-error` hides its command's result — with the failure ~8,000 log lines deep. It now runs every command, publishes a per-command exit table to the job summary, and fails the step (still non-blocking) when any command failed. Results recorded in `docs/design/WINDOWS_WALLET_PROBE_SHEET.md` §4.3: P-16 passed first time; the self-hosted pipe under axum served its first real request. - **`shekyld ` exits `1` when the request failed.** A recognized command whose RPC could not be completed — daemon not running, connection refused, a JSON-RPC error, a non-OK status — printed the failure and then exited `0`, indistinguishable to a script or service manager from success (`shekyld exit` against a dead daemon "succeeded"). The control client now records a failed request and the process exit status reports it; `0` means the daemon answered. - **The block-reward supply-headroom cap underflowed past full emission.** `get_block_reward`'s 6-argument overload capped a block's reward at `MONEY_SUPPLY - already_generated_coins`, computed in `uint64_t`. Once `already_generated_coins` passed the cap that subtraction wrapped to a near-`UINT64_MAX` headroom, so the comparison guarding the cap was never true and the cap silently did nothing — the inverse of its purpose. No production path reached it (the connect path caps the stored total), but both Rust reward entry points already defended against an out-of-range total and this was the one member of the family that did not. Now `shekyl_cap_reward_to_remaining_supply`, saturating, beside the supply-advance clamp it is the twin of. - **The four `ci/economics-c2a-prime` layer gates had never run a test.** The layer jobs' runtime package list was missing `libunwind8`, `libboost-program-options1.74.0` and `libboost-serialization1.74.0`, so the test binaries exited 127 at the dynamic loader; the gate discarded both the loader error and the exit status and reported "no test cases found", which read as a missing harness. The harnesses existed throughout. The gate now executes each binary before counting, so a binary that cannot start is never again reported as a missing test. ### Added - **The Windows wallet transport, self-hosted only** (WP-W2, second implementing slice of the Windows wallet round — `docs/design/WINDOWS_WALLET_SUPPORT.md` §8.1, ruled 2026-08-20). On Windows `shekyl-cli` hosts `shekyl-wallet-rpc` in-process over an owner-only named pipe: descriptor applied at creation on every instance, `first_pipe_instance(true)` so a name someone else already holds fails loud instead of being joined, and a client dial (`open_verified`) that runs the owner + integrity peer check **before** handing back a handle, so no request byte can reach a pipe that has not passed. Windows ships **no** external local form: there is no `npipe://`, and `uds://` is refused at parse on both the CLI and the server with a message naming the platform — `RpcUrlForm::Uds` and `ListenAddr::Uds` are `cfg(unix)`, so a Windows arm offering the transport cannot compile. The scripted seed export (`--seed-out`) is created owner-only at `CreateFileW` (user-SID grant, one ACE, protected DACL, `CREATE_NEW`), the `0600` equivalent, pinned by probe P-16. The design record carries a correction worth reading: the ruling's first form called the peer check "pre-positioned, not load-bearing" on the self-hosted path, on the premise that the client dials a handle. It dials a *name* through the OS namespace, and the 0700 directory that contains the Unix socket has no pipe analogue, so the check is the replacement for that containment on a path that runs with auth disabled. Recorded as an error corrected by grounding. **Gate asymmetry, stated plainly.** The `shekyl-win-sec` half is checked, clippy'd, documented and probed for a Windows target. The `cfg(windows)` arms in `shekyl-wallet-rpc` and `shekyl-cli` are seen by no compiler on a Linux box (their graphs reach `ring`) and are observed only by the Windows runner's informational scouting step, which now also runs the two end-to-end tests. **No Windows wallet ships yet:** `BuildRust.cmake` still skips the Rust binaries for Windows targets, and Windows release archives still carry the daemon and no wallet. WP-W5 owns both the build gate and turning the scouting step blocking, once it has been seen green. - **`shekyl-win-sec`: the Windows security primitives for the wallet transport** (WP-W1, first implementing slice of the Windows wallet round — `docs/design/WINDOWS_WALLET_SUPPORT.md`). Owner-only security descriptors built from SDDL, the current-user SID the pipe name is derived from, and the client-side peer check that requires the server's owner SID to match **and** its integrity level to be at least Medium. Its own crate because both sides of the transport need the same Win32 token/SID calls and one of them (`shekyl-wallet-rpc`) is `#![deny(unsafe_code)]`. Compiles to nothing off Windows; no new supply-chain surface (`windows-sys` was already in `Cargo.lock`). The wallet's disk-headroom probe gains a Windows half in the same slice — `GetDiskFreeSpaceExW`'s `lpFreeBytesAvailableToCaller`, the exact analog of `statvfs`'s `f_bavail`, chosen so both platforms answer the same question: how many bytes the process filling the disk may actually write. **No Windows wallet ships yet.** The transport itself (WP-W2/W3) is not built, `BuildRust.cmake` still skips the Rust binaries for Windows targets, and Windows release archives still carry the daemon and no wallet — unchanged from the C1/C2 cutover. What this adds is the foundation plus the CI gates that will catch it regressing. - **The CMake build now produces the Rust `shekyl-cli` and `shekyl-wallet-rpc` binaries** (C1/C2 install cutover). `cmake/BuildRust.cmake` previously invoked `cargo` only for the two staticlibs, so `shekyl-cli` — listed as a build output in `INSTALLATION_GUIDE.md` for as long as that guide has existed — was never actually produced by a CMake build, and `bin/shekyl-wallet-rpc` was the C++ `wallet_rpc_server` wearing that `OUTPUT_NAME`. Both Rust binaries are now built, staged into `bin/` (which is what `contrib/gitian/` tars into the release archives), and installed. **Not on Windows:** the Rust wallet stack is Unix-only today (UDS transport, 0600 socket backing "auth rides the transport", 0600 seed export), so `BuildRust.cmake` skips the binaries for Windows targets. Combined with the `wallet_rpc_server` deletion below, Windows release archives carry the daemon but no wallet until that port lands — tracked in `docs/FOLLOWUPS.md` ("Rust wallet stack: no Windows support"). macOS is unaffected: it is Unix, and needed only cross-linker wiring — rustc's link step now inherits the depends toolchain's `-B` / `--target` / `--sysroot`, which it never got while this file built staticlibs only, because an archive is never linked. ### Removed - **epee's HTTP client is deleted.** `http_client.h` / `http_client_base.h` / `abstract_http_client`, digest auth (`http_auth.{h,cpp}`, both the client and the server halves), `net_helper` (the blocking asio connector), the `invoke_http_*` templates (`http_abstract_invoke.h`, `http_server_handlers_map2.h`), and the HTTP server request parser (`http_server_impl_base.h`, `http_protocol_handler.{h,inl}`) that had no production includer since the listener deletion and was kept compiling by its own unit test. Its last two production consumers moved off it in the preceding changes (the `shekyld ` control client onto the Rust transport; the bootstrap forward deleted). With the listener already gone, epee has no HTTP surface left; the `http-client` fuzz target and the `HTTP_*Auth` / `http_server` unit tests go with the code they exercised. `net_ssl` stays — the P2P TCP server still links it. - **The bootstrap-daemon forward is gone.** `--bootstrap-daemon-address` / `-login` / `-proxy`, the `set_bootstrap_daemon` RPC and console command, and the forward that — while the local node was more than ten blocks behind — re-issued a wallet's queries (`/getblocks.bin` from its restore height, `/is_key_image_spent`, `/gettransactions`, …) to a third-party node, in `auto` mode one the daemon picked from peer gossip. It inverted Shekyl's posture (own node by default; a remote is the *wallet's* explicit, visible choice) by moving the most identifying query pattern to a node the user never chose and flagging it with an `untrusted` field no wallet reads. `get_info` loses `bootstrap_daemon_address`, `height_without_bootstrap` and `was_bootstrap_ever_used`; every response loses the constant-false `untrusted`; `shekyld status` no longer prints a bootstrapping clause. Ruling and rule-21 reopen (wallet-side, explicit, never a daemon forward) in `docs/DAEMON_RPC_RUST.md`. **Operator impact:** a config carrying any `bootstrap-daemon-*` key fails to start with a named migration message (`removed_flags`); point the wallet at a daemon you operate while IBD runs. - **`--public-node` and `/get_public_nodes` are gone.** shekyld no longer advertises an RPC port over P2P for other people's wallets, and the discovery RPC whose only production consumer was bootstrap-daemon `auto` is deleted. RPC is operator-to-operator: both ends are machines you control; the adversary is the path between them, never the peer. Restricted RPC (`--restricted-rpc`, `--rpc-restricted-bind-port`) remains, for your own wallet on a less-privileged port. Phone-only users have no supported configuration — a deliberate product boundary; rule-21 reopen is a light-client protocol, never restore `--public-node`. **Operator impact:** `--public-node` in a config fails to start with a named migration message. `CORE_RPC_VERSION` 3.22. See `docs/DAEMON_RPC_RUST.md`. With nothing left to set them, `node_server` no longer carries an RPC port / credits-per-hash to advertise (the handshake fields stay on the wire at zero), and the `print_pl publicrpc` console filter — which could only ever list strangers' advertised RPC ports — is gone. The same ruling applies to the RPC readout: `get_peer_list`'s `peer` and `get_connections`' `connection_info` no longer carry `rpc_port` / `rpc_credits_per_hash` (values only a non-conforming peer could supply; rides the same 3.22 bump). - **The C++ wallet stack is deleted — `wallet2` and everything that existed only to serve it** (Phase 5 of the Rust wallet rewrite, `docs/design/WALLET_REWRITE_PLAN.md`). 27,579 lines: all of `src/wallet/` (24,239 — `wallet2.{h,cpp}`, `wallet2_ffi.{h,cpp}`, the two remaining `wallet_rpc_server_*` headers, `wallet_args`, `node_rpc_proxy`, `fee_priority`, `fee_algorithm`, `wallet_errors`), `src/device/device_cold.hpp` (typed entirely on `tools::wallet2::*`, included only by `wallet2.cpp`), the `account_base` SHKW1 transitional helpers, and the two Rust FFI modules whose only consumer was `wallet2.cpp` (`engine_file_ffi.rs`, `wallet_envelope_ffi.rs` — 15 exports, 6 of which already had no C++ caller). `src/shekyl/shekyl_ffi.h` loses its wallet section; one constant survives, relocated, because a daemon-side call site uses it as a byte offset. Nothing shipped changes. The C++ wallet has been unshipped since the C1/C2 install cutover handed its binary name and install slot to the Rust `shekyl-wallet-rpc`, and it never carried a staking surface at all — so deleting it removes no capability and unblocks staking exit being built once, in Rust. The Windows CI job drops the `wallet` target and is renamed accordingly: Windows wallet support was already absent before this change and is not affected by it (see `docs/design/WINDOWS_WALLET_SUPPORT.md`). - **The C++ `wallet_rpc_server` executable is deleted**, in the same PR that handed its binary name and install slot to the Rust `shekyl-wallet-rpc`. Keeping it would have left an uninstalled second implementation of a surface the Rust binary already owns — built on every CI run, shipped to nobody, and available to accrete fixes that would then have to be paid for twice. `wallet_rpc_server.{h,cpp}` and the CMake target are gone; `wallet_rpc_server_commands_defs.h` and `wallet_rpc_server_error_codes.h` remain **only** because `wallet2_ffi.cpp` still includes them, and go with `wallet2` at Phase 5. The Windows removed-flag CI assertions now cover `shekyld` alone (they grep a message emitted by C++ `src/common/removed_flags`, which the Rust binary's argument parser does not produce). - **Trezor hardware-wallet backend deleted** (`src/device_trezor/`, `tests/trezor/`, `cmake/CheckTrezor.cmake`, and the `TREZOR_DEBUG` / `USE_DEVICE_TREZOR` build arms). It was dead code under the V3 default — protobuf generation never ran, `protocol.cpp` carried a `#error` against accidental compilation — and Trezor firmware has no post-quantum support, which V3 addresses require from genesis. Reopen criterion: Trezor ships firmware implementing the primitives in `docs/HARDWARE_WALLETS.md` §V4 Roadmap. The dormant Ledger backend under `src/device/` is unaffected (separate retirement track). ### Changed - **C++ noise carrier deleted; the epoch-budget guard lives at `Zone::new` (`COVER_TRAFFIC_RESTORATION.md` §2.9, #515).** `NoiseQueues` is the carrier. C++ no longer builds a noise zone: `make_relay_zone` never sets the flag, the channel deque and `send_noise` are gone, and the two remaining noise callbacks fail loudly rather than no-op. A full-size message must fit in one epoch (`carrier::noise_windows_in_epoch` against `MAX_FRAGMENTS`) or `Zone::new` refuses — the C++ `static_assert` that went with `send_noise`. The unused `CRYPTONOTE_NOISE_MIN_EPOCH` / `_EPOCH_RANGE` `#define`s went with `noise_zone_params`; every C++ zone now passes the Dandelion++ epoch. - **The serve-credit response wire is drafted, and it is TWO artifacts (`RF-D1`/`RF-D2`/`RF-D4`).** A scoping premise was corrected before drafting: TJ-H's reserved padding belongs to the **Tor-served shard payload** (its attack is an observed response size), not the on-chain vin, so `RF-D1`/`RF-D2` and `RF-D4` govern different wires and cannot share a boundary. They stay one slice because they share a *validation surface*: does each artifact make its hashed region unambiguous to a verifier? On-chain: the kept/pruned boundary is structural (the vin/prunable split), with the **pairing** stated rather than inferred — pruned entries in serve-credit-vin order, one per serve-credit vin — and stating the pairing is what removes the count field, which would have been a MUST-equal check on a length the tx already determines. `segment_subroot_rk` and `leaf_index_in_segment` come off the wire entirely (`RF-D6`): both are derivable by the verifier, and a wire-supplied verification target is *unsound if trusted*, not merely redundant. `hybrid_signature` becomes `ed25519_countersignature: [u8; 64]`, a **fixed array**: a name that no longer asserts contents it lacks, with the length prefix and bound check gone because a wrong length is now unrepresentable. Served payload: `leaf_count varint ‖ padding_len varint ‖ segment_bytes ‖ padding_bytes`, with only `segment_bytes` hashed against `R_k` — a leaf **count** makes multiple-of-128 structural, and an explicit `padding_len` keeps the frame self-delimiting rather than deriving padding extent from `content-length`. `RF-D7`: "read-anything" is about **content, never length** — `padding_len` is adversary-declared, so readers MUST reject `padding_len > leaf_count × LEAF_BYTES` **before allocating or draining**, bounding a body at 2× a segment. An earlier cut claimed the response was "bounded by the transport cap"; it is not — `shekyl-p-serve` has no response-length cap and the fetcher's is 256 MB, 77× a segment. `content-length` cannot stand in — it does not locate the split, verification is by reconstruction rather than delimiter, and it is a property of the transport rather than the format. Forward-compatibility posture ruled **write-zero, read-anything**, so a future padding scheme needs no flag day. Pinned now because `recompute_segment_r_k` has **no production consumer** — the definition has one side to change, not two. - **Relay axes stay un-collapsed (`DAEMON_RELAY_PRIVACY.md` §93, #513).** Dandelion++ runs on every zone; noise is a carrier on encrypted links only, never a reason to demote a stem. `LinkSecrecy` is constructed only from a `RelayZone` (`of`), so a caller cannot mint "encrypted" beside the wrong reach. `Zone::new` returns `Result<_, ZoneNewError>` — noise-on-cleartext and a mismatched channel count stay distinct refusals; the FFI maps both to null. The inherited covert branch in `send_txs` is deleted, not repaired. - **Covert executor is a real port (`COVER_TRAFFIC_RESTORATION.md` §2.9 step 2, #508).** `CovertQueues` owns the fragment window (`dummy.len()`), refuses a non-multiple, and emits exactly one window per take — real or cover. `CovertSend` is epoch-bound so a dropped token restarts and a stale resolve cannot advance a later message. CV-4 still hands distinct queues to `covert_cadence`; `Driver::poll` still takes no queue. C++ `send_noise` still executes in production until step 4 wires notify and deletes the inherited covert branch. - **Rule 91's sweep discipline gains its two boundaries.** *(1) A constant's doc is part of the edit, not adjacent to it* — when a value changes, its own doc comment changes in the same edit, because **proximity implies authority** and a declaration whose comment and value disagree is the worst place for that disagreement to sit. *(2) The sweep covers claims about the present, never the record of what was* — a released CHANGELOG entry describing a value as it shipped is history, not staleness, and a mechanical sweep that "fixes" it corrupts the record **while reporting success**. Both landed from a transport-cap sweep that found four live stale comments (including a constant whose own doc described the pre-change fields) and one released entry that was correctly left alone. Stated plainly in the rule: the tree's **executable** surface is checked continuously and its **descriptive** surface is checked never — eight stale numerals in one round, none where a gate reads. - **`r` deletes from the attestation nonce and the witness — replaced, not removed (`RF-D3`/`RF-D5`).** The nonce's first term becomes the block's **validated** predecessor hash: `r` and `cb_out_key` were chosen by the same party, so `r` never provided pre-signing resistance, while `block_hash(h−1)` cannot exist before block `h−1` does. The witness blob drops its 32-byte prefix (`count ‖ sigs`), and the transport cap moves 866,600 → 866,568 in three places with three enforcement mechanisms. `RF-D5`: the deletion is **not** a removal — it requires the anchor to *arrive*, so the `#[repr(C)]` FFI ctx widens by `prev_block_hash` and C++ populates it. All-zeros is refused with its own verdict code rather than a generic malformed-ctx path, and there is deliberately **no readability flag**: a verifier holding a block has parsed its header, so that arm could never fire, and a flag is itself caller-populated so it would be forgotten alongside the hash. Tested against real callers — **five C++ call sites forgot the field and were refused loudly at build time**; under the flag design all five would have passed silently on a consensus path. Preserving the term's **arity** let the frozen cross-language KAT re-anchor with nonce, countersignature and `attestation_root` byte-identical — rule 30 satisfied by construction rather than by regenerating a vector checkable only against the code it tests. Rule 42 was checked and has **no instance** here (globs, CI-gate scope, and purpose all exclude a consensus transport blob whose integrity is the mined root); no bump owed, recorded so none is added. - **The serve-credit response-format round is open (`RF-D1…RF-Dn`).** Unblocked by the carrier round's ruling (merged in PR #501), and **now pure transcription: every input is pinned.** `RF-D3` — whether `r` survives — was carried as the round's one open test and **dissolved on grounding**. The disposition is unchanged (`r` deletes) but the round did not settle it: the 2026-08-10 ruling did, on an argument this round had restated incorrectly. The ruling's argument is *shared chooser plus an existence bound* — `r` and `cb_out_key` are chosen by the same party, so `r` never provided pre-signing resistance — where the round had converted "not choosable" into "ungrindable" and written a falsifier for the paraphrase. Recorded rather than quietly fixed, because it is the third dissolve-on-grounding in this arc and the class is identical each time: a residual carried into a new round without re-grounding its reasoning. `CR-F2`'s `prefix_hash`/tx-id change is carried as a first-class input rather than a cross-reference to the round that discovered it. - **`shekyl-wallet-rpc` now names the Rust binary, everywhere.** The C++ `wallet_rpc_server` keeps building under its own target name through the transition but is no longer installed and no longer claims that `OUTPUT_NAME`; it is deleted wholesale with `wallet2` in the Phase-5 commit. Two different programs sharing one installed name was the failure this cutover exists to prevent. The Windows CI removed-flag assertions follow the C++ binary (the V3.1 migration message they grep for comes from `src/common/removed_flags.{h,cpp}` and cannot transfer to the Rust binary's argument parser); the shipped Rust binaries gain their own runs-and-responds check. The gitian packaging copies for `shekyl-cli` and `shekyl-wallet-rpc` now hard-fail instead of `|| true` — they are guaranteed products, and tolerating their absence meant a release with no wallet-rpc could be staged silently. - **Cover restoration is PR 1 of the relay-logic Rust cutover, not a C++ feature with a Rust mirror.** `COVER_TRAFFIC_RESTORATION.md` §2.9 names the five-step series; the C++ `broadcast_all_zones` loop is condemned glue that step 3 deletes. Review polarity: Rust owns remaining relay logic, C++ is the shim the series removes. `RelayCarrier::Covert` carries `SlotIndex`; `CovertQueues` slices at the fragment window so CV-1 is a restart, not a name; CV-4 threads the queues into the cadence collector; the dispatch FFI crossing is in `shekyl_ffi.h`. - **The pass-record carrier round is a record partition, not a signature split (`CR-D2`).** Sizing the whole record was arithmetic, not measurement: the segment layout (`SEGMENT_LAYER_J = 2`, Selene/Helios 38/18) fixes the opening at two branch layers (`1,792 B`) plus a leaf chunk (`4·38·32 = 4,864 B`) that is on-wire cost despite no struct declaring it — `verify_segment_path` takes `leaf_layer_scalars` as a separate parameter and cannot derive it. The whole record is ~10,243 B ≈ 262 GB/yr, so **`path` is the dominant term and the signature is not**: pruning only the ML-DSA leg keeps ~177 GB/yr, twice the figure that disqualified doing nothing. The surviving option prunes the leaf chunk, branch layers, and ML-DSA leg together, keeping ~278 B ≈ 7.1 GB/yr (**refined to ~230 B ≈ 5.9 GB/yr by `RF-D6` later in this same release**, which drops two verifier-derivable fields from the record) — enough to identify the record and check the classical signature, **not** enough to re-verify the opening, which is stated on the ruling rather than left to be discovered. The same read corrects `shekyl-economics-sim`'s `RESPONSE_BYTES`, which understated the opening **2.16×** (three branch layers where the assembler yields two; the leaf layer as 38 scalars where it is 152). It moves one end of the Stage-2 **A5/W10** finding: re-fetch still undercuts holding at bulk transit (`q* ≈ 0.274`), and at retail egress the proxy now **loses** at loose grace rather than free-riding. The two proxy tests are restated to assert both ends separately — the strong form they encoded (failure at the proxy-unfavourable end implies failure across the band) no longer follows. Whether A5/W10 survives overall is not settled here. `CR-D1` resolved by reading, completing the round: there was **no tension** between `blockchain.cpp:3726` and `cryptonote_basic.h:620-621`. The count check is skipped for serve-credit txs precisely because `:3746-3754` imposes a **stricter** rule twenty lines later — `pqc_auths` must be *empty*, error string *"signature is on the vin"* — and `:3630` skips the spend arm as non-spending. So the slot is **forbidden, not occupied**, and it is a category error besides: `pqc_auths` is per-input *spend* authorization while a countersignature attests to a *read*. The kept Ed25519 leg therefore stays on the vin, no consensus assertion is inverted, and the carrier round is **RULED** — kept (header + `leaf_bytes` + Ed25519) on the vin in the prefix, pruned (ML-DSA leg + `path`) as a parallel structure keyed to the vin **inside** `serialize_rctsig_prunable`, since a vin cannot straddle `unprunable_size`. - **The Foundation CompleteTree posture is reachable, warned, and served — and it is no longer the default anything.** Round record: [`docs/design/COMPLETETREE_ACTIVATION.md`](design/COMPLETETREE_ACTIVATION.md). Before this, `first_stake` **hardcoded** `CompleteTree` holdings, so every wallet that staked silently took on the whole-corpus obligation — while the serving side refused to serve it, because a shard-id serve-set cannot express "all". Both halves are closed, in the same round, so no code state ever existed where an ordinary staker silently owed the corpus. - **The obligation is derived, never stored.** A CompleteTree persona's serve-set is the store's frozen prefix `[0, next_freeze_seg)` — one growing number, re-read every refresh, so shards join the served set as they freeze with no list maintained anywhere that could drift. Retention is a **one-way prune-disabled posture** on the store rather than per-shard pins: `prune_frozen` refuses outright under it, which makes "a foundation node prunes bytes it owes" unrepresentable instead of merely guarded against. - **Choosing it is now an act, not a default.** `first_stake` takes a mandatory `StakePosture`; the hardcode is deleted. The RPC requires `acknowledge_non_earning_unbounded`, and the refusal body when it is missing (`-29506`) **is the full statement of terms** — never earns, grows forever, slash side live — so a client either shows the operator those terms or deliberately echoes an acknowledgment it was handed. The CLI's `stake --complete-tree-foundation` prints the same text and will not proceed until the operator types `serve without reward`. The GUI never sends the field. - **Market staking refuses honestly in the meantime.** `-29505 STAKE_NO_SHARDS_AVAILABLE`: shard assignment is its own round, so a well-funded wallet is told so rather than posting an empty holdings set that consensus would reject. - **Operators can see it.** `staking_info` and the CLI report the live serving posture (absent renders "not serving" — the honest reading for a host that has not started), and a disk-headroom alarm watches the volume the corpus grows on, for **any** serving posture. A failed probe reads *disarmed*, never healthy. - **The prunable region's sole-occupant invariant now has a name and a test.** `calculate_transaction_prunable_hash` computes the prunable hash two ways — the blob tail after `unprunable_size`, or a re-serialization of `rct_signatures.p` — and they agree **only because `ctsig_prunable` is the last thing the transaction serializer writes**. That equivalence is positional, and it had no name in the code and no test. An append after `ctsig_prunable` diverges the two paths silently: a node that kept the blob hashes the new bytes, a node re-serializing from the parsed struct does not, and the disagreement surfaces as a `"tx hash cash integrity failure"` throw on blob-holding nodes only. `tx_prunable_region_sole_occupant.cpp` asserts tail length *equals* re-serialized length — naming the defect ("the prunable region has a second occupant") rather than reporting a hash mismatch — plus the blob-present/blob-absent hash equality it implies. Deliberately C++ despite rule 20: the invariant is a property of the C++ serializer's ordering, so a Rust test would be blind to the append it exists to catch. - **Fee sanity ceiling is live (interim form) — `-29109 DAEMON_FEE_UNREASONABLE`.** Named fee tiers were previously accepted from the daemon unchecked (only `Custom` rates were capped). The wallet now refuses a snapshot that is non-monotonic or whose effective weight-1 charge exceeds the era-maximum legitimate fee — the daemon-rounded genesis-condition `Fh` (14,000,000 atomic units/weight), derived from the economics crate and KAT-pinned rather than hand-picked — on both the build path and `get_default_fee_priority`, through one `ValidatedFeeEstimates` constructor. (A 100,000 mid-regime literal was caught in review refusing honest young-chain snapshots from block 1.) Caller-side `Custom` band violations are `-32602`, not a daemon defect. The intra-snapshot 10×-economy lock is **withdrawn**: honest `shekyld` 2021-scaling `Fh / Fl` is 65×–1000×, so that lock banned the production Priority tier. The historical median-multiple ceiling remains the V3.x `WalletSideEstimator`'s job (2026-08-16 decision-log entries). - **`custom` fee rates can reach the daemon's own `PRIORITY` tier.** The `custom` band's "100× economy" ceiling was the withdrawn 10× lock in another place: on the pinned 2021-scaling row `(340, 1400, 67_000)` it refused `custom = 67_000` as *the caller's* error while `PRIORITY` returned that exact rate. `custom` is now bounded by the economy floor and the same absolute cap every named tier obeys, and nothing else, so "priority, plus a little" is expressible (2026-08-17 decision-log entry). - **The bond fee obeys the ceiling too (`stake` can now return `-29109`).** `first_stake` was the one production `get_fee_estimates` consumer that read `economy` straight off the raw snapshot, so the new ceiling did not cover it. That is the lane where it matters most: the bond fee is charged to persona working capital and carries no user-facing fee control by design, so a daemon quoting an absurd rate was paid with nobody positioned to notice — 6.5 billion atomic units for a 200,000 per-weight `economy` tier, in the pinned vector. It now goes through the same `ValidatedFeeEstimates` gate, and a refusal is `FirstStakeError::FeeUnreasonable` → `-29109` rather than being folded into `-29102` ("check the daemon connection and retry"), which is the wrong remedy when the connection worked. Nothing durable is written on that path (W1-clean). `FeeRate::calculate_fee_from_weight` — which multiplies unchecked and documents that it may panic — now has zero production callers. - **Fee-rate units in the API say per *weight*, not per byte.** `FeeRate`'s contract is per weight unit, and the two diverge by the Bulletproofs+ clawback once a transaction has more than two outputs — so a client that sized a `custom` rate against the serialized length pinned the wrong number. Corrected on the `Feerate` scalar, the `FeePriority` description, and `FeePriority::Custom`'s rustdoc. - **An ambiguous submit no longer reports a manufactured transaction hash.** `SubmitPendingResolution.tx_hash` is `Option`: when the reservation is no longer in flight there are no bytes to hash, and the previous code synthesized one from the `ReservationId`. The field is documented as the submitted tx's hash and is consumed by the V3.x mempool monitor, so a synthesized value was a plausible, *nonexistent* txid — the monitor would have read "never in the mempool" as "disappeared from it", a confident wrong verdict where `None` is a correct absent one. Correlation runs through `reservation_id`, which the same event always carries; the synthesizer is deleted rather than renamed. - **Legacy scalar-`fee` tier synthesis deleted.** A daemon reply without a `fees` array was treated as pre-2021-scaling and turned into `(fee×1, fee×5, fee×1000)`. No such daemon exists on this chain (`HF_VERSION_2021_SCALING = 1` from genesis), and the invented `×1000` priority meant any base fee above 100 blew the absolute cap and got the whole snapshot refused — Economy included. A missing `fees` array is now a malformed reply. A daemon that grows the ladder past four tiers still works. - **Dust boundary corrected: marginal input weight 3457 → 9136.** The provisional `MARGINAL_INPUT_WEIGHT` stub (zero FCMP proof increment) outlived the KAT that was meant to replace it; the dust threshold now composes the shared fee formula with the weight model's own `marginal_input_weight_at_d_ref`, so outputs that cost more to spend than they carry are correctly classed as dust. - **Fee-surface scaffolding retired.** `STUB_FEE_ATOMIC_UNITS` and the pre-PR-5 `*_in_state` reference bodies are `#[cfg(test)]`-gated; "Phase 1 stub" doc-prose on the live estimator swept; the grace-blocks constant has one owner (`shekyl-rpc-client`); daemon fee-error classification is prefix-anchored instead of substring-open. `phase1_tx_hash` is deleted outright (an intermediate rename to `correlation_tx_hash` did not survive review): an *ambiguous* submit whose transaction bytes the engine no longer holds now reports `tx_hash: None` — a synthetic reservation-derived id is not a transaction hash, and emitting one as if it were was the defect. - **Fee-path invariants moved into the types.** `ValidatedFeeEstimates` carries its mask's non-zero proof and its constructor is crate-internal, so outside the engine it is an opaque token minted only at the fetch boundary — no consumer can hand the build path a snapshot that skipped the ceiling. `FeeRate::from_nonzero` makes `Custom` construction total, retiring a `CustomFeeBand` variant that was unreachable and, had it been reachable, would have blamed the caller for a daemon-supplied mask. `marginal_input_weight_at_d_ref()` drops its `tree_depth` parameter: every caller passed `MAX_TREE_DEPTH`, and fixing it is the point — a dust bar that tracked live tree depth would reclassify outputs a wallet already holds each time the tree grew a level. - **An indivisible-value requirement is only as strong as its field list, so the shape is now "derive every field", not "these N fields".** `ARCHIVAL_CHALLENGE_MECHANISM.md` §9.7 item 6 asked for a provenance record that cannot be constructed without all three attestations. That was implemented to spec and the gate still **faded open**, because the defect arrived through a *fourth* field nobody had listed — leaving a producer-shaped batch on any `aarch64` box able to certify itself a rule-76 floor datum. Completeness of an enumeration is not checkable from inside the requirement, so the revised property is that **every field is derived from the apparatus that ran and none is declared by the caller**: it holds for fields nobody anticipated. The superseded text is retained — "necessary but not sufficient" is only legible next to what it was. - **Rule 91 gains an index sweep.** A correction is not landed until every surface *pointing at* the corrected text agrees with it — headings, tables of contents, `IMPLEMENTATION_INDEX.md` rows, `FOLLOWUPS.md` entry titles, and cross-references from other docs. A corrected body under a stale heading is worse than an uncorrected item: long docs are read by heading, so the stale title routes the correction to exactly the readers who did not need it while the file looks fixed to anyone grepping the body. - **Genesis tx-key remint (`shekyl/genesis-txkey-v2`).** The founding transaction key is now derived from payment identity (spend ‖ view ‖ ML-KEM encap key ‖ amount), not the Bech32m address spelling. A layout correction can no longer rotate `GENESIS_TX`. The v1 customization is retired unused; pins in `cryptonote_config.h`, `GENESIS_ALLOCATIONS.md`, `mining_parity`, and the shekyl-wire h0 corpus move with the remint. - **Wallet-owned address encode rides `shekyl_address_encode`.** `account_base` keeps the blob-derived `msg_sign_pk` (public, not persisted, not on `account_public_address`) so `get_public_address_str` — including `start_mining` — emits a real fork-(ii) address. Struct-only `get_account_address_as_str` stays fail-closed. - **W₂ is pinned, and what unblocked it was a ruling rather than a measurement.** `CHALLENGE_RESPONSE_BLOCKS` collapses from its `Option = None` staging slot to `SETTLEMENT_EPOCH_BLOCKS / 20` (500 blocks, ≈16.7 h), written as a fraction so it tracks if the epoch is ever re-pinned, and the interim match-assert is replaced by the bare `CHALLENGE_RESOLUTION_BLOCKS >= CHALLENGE_RESPONSE_BLOCKS` the slot's own pin shape specified — no `Some(n)` left behind as scaffolding (rule 21). **The ruling: W₂ has no surviving upper bound.** Every argument for keeping it small was clock-burn, and that attack needed a commitment record (superseded by derived assignment) and an abandonment penalty (killed by the impossibility result — §6 keeps its sizing arithmetic "as the record of what a penalty would have had to achieve, not as pending work"). Under derived assignment there is no occupancy to extend: the witness is the producer of block `h`, and a witness that sits on its assignment wastes exactly one of the pair's three draws whether W₂ is 500 blocks or 5,000. Clock-burn is a draw-count attack, not a duration one. The rest is slack — settlement bookkeeping already grants a full epoch of grace, outstanding-challenge count is bookkeeping, `P`'s availability burden is unchanged, and a longer window is a DDoS *defense*. One further candidate raised in review, **outsourcing resistance**, is rejected: the economics invert it, half of it is not an attack, and decisively a short W₂ does not prevent the cheap (local) form anyway. With a hard lower bound and no upper one, the shape is pick generous, not optimal — the band ≈200–500 is the ruled part and the divisor is its consequence. **No measurement is owed.** Demoting the unused rig to a "floor check that can only raise W₂" was the half-step that kept the question open: a check whose only possible outcome is a safe-direction move, applied to a constraint that is already one-sided, confirms what the asymmetry already settled. The value is ruled, not provisional. Reopen only if a *premise* of the ruling returns (clock-burn regains both a commitment record and an abandonment penalty, or `SETTLEMENT_EPOCH_BLOCKS` is re-pinned out of the band). Also corrects the design doc's stale clause "every extra block of W₂ widens the clock-burn window §6 prices" — **corrected rather than deleted**, because a silent gap there is what let this keep circling: every re-derivation read it and reconstructed an upper bound that no longer exists. - **A persona now releases the pins of shards it no longer owes** — closing §9.7 item 5's leaking direction (SH pin-release). A pin means one thing, "`prune_frozen` must not delete this segment's bytes", and until now nothing ever removed one: the pin table was written by `pin_serve_set` and cleared by tree truncation and **nothing else**. So every shard that left a persona's holdings — an ordinary `HoldingsUpdate` drop, not only unbonding — kept its ~3.33 MB forever, up to ~13.6 GB against a rule-76 Pi-4 floor. Not self-correcting, and the doc's own words for it were "a leak, not a policy". `LeafStore` gains `pinned_shard_ids` and `release_pins`; `EngineServeSetPinner` gains the departure ledger and the gate. The asymmetry §9.7 ruled is what shapes it: **acquiring a pin needs no finality; releasing one does**, because the reclaim is disk (recoverable) and the risk is a slash (not). **The gate is epoch-shaped, not reorg-shaped.** A reorg depth answers "has this departure settled"; the obligation asks something else. §4 quantizes drawability to epoch boundaries — a pair is drawable in E iff it held the shard at **E's open** — and that fixed pre-challenge evaluation is the WS-1 constraint that stops `P` dropping a shard after the fire to escape. A mid-epoch drop therefore leaves the pair drawable through E's close. It bites at the pin because `StoreShardProvider` is **serve-set-blind** — it answers for any shard whose bytes are in the store — so the leak is currently what keeps the obligation met, and a 720-block release would have reclaimed the bytes ~9,280 blocks early, turning a disk leak into a miss. A pinned-but-unowed shard is released only after **two consecutive epoch opens** of absence, and its clock is **cleared if the shard returns**. `W₂` is deliberately not an operand: it has no landed constant (it is the rig's output), and a full epoch of slack covers any `W₂` shorter than `SETTLEMENT_EPOCH_BLOCKS` — with the stated reopen if the rig ever derives one at or above an epoch. Pin runs before release in the same actor turn, so the only reachable transient is an extra pin, which fails toward retention. The store does not enforce the gate: it has no clock, no view of the bond record, and no memory of when a shard left one. A half-check there would look guarded while the real condition went unenforced. Two stated residuals: the ledger is in memory per session (a restart restarts the clock — slower reclamation, never an early release), and the release lags the pin by one refresh (the reconcile learns the store's pin set from the reply it is answering; against an epoch-scale gate that is not a lag that means anything). - **The signature-alignment round is closed; its last rule is enforced, not remembered (SA-R-7 tail).** The FFI boundary ratchet (`shekyl-ffi/tests/ffi_boundary_ratchet.rs`) pins every raw `slice::from_raw_parts` and raw `Vec::with_capacity` in the crate per file, failing in both directions — a net-new raw site fails CI, an improvement must tighten its pin — with `legacy_util`'s `slice_from_ptr`/`bounded_capacity` as the seam obligations (rule 40); the second byte-identical unguarded `make_slice` clone (`engine_file_ffi`) was absorbed into the seam. House conventions landed as rules: one-domain-string-one-context (rule 30, SA-R-2), envelope-vs-payload version naming (rule 42, the SM-R-5 split), refuse-never-panic on the untrusted FFI path (rule 40), and the new `46-shell-gate-exits` — gate verdicts never travel through a pipe (five swallowed-exit exhibits in one round, including one that pushed a clippy-red commit and one that masked a broken C++ link). FOLLOWUPS' thirteen stray plus-bullet wraps mechanically rejoined (whitespace-normalized content verified identical). - **The persona serving host is wired: a staker's wallet now starts one** (SH-2b-2). `Engine::start_serving_if_staker` is the sibling of `start_pscan_if_staker`, and the embedder parks both handles together in an `OpenTasks` bundle rather than as separate positional handles — they have one lifetime, are created together and torn down together, so a third task costs a field rather than a parameter on every lifecycle signature. Shutdown is **serving first**: stopping the advertisement before the scan, for the same reason the host stops tor before its listener — a published descriptor must never outlive the ability to answer at it. Fail-closed at open, like the P-scan. A staker with no *active* persona gets `Ok(None)`: there is nothing to serve until one is activated, and first-stake mid-session already collapses onto reopen under Model D. Serving derives its serve-set over a persona-isolated transport, which today means the ① local posture — so a daemon that is not on loopback is refused with the remedy named, rather than serving a set derived over the principal's shared connection. The `

.wallet.tor` path helper joins `shekyl-engine-file`'s companion family (`.curvetree`, `.pscan`, `.pending`) rather than the prefs crate: it is wallet *data*, and the family appends to the full base path. - **The wallet can configure a Tor service, and almost none of it is a setting** (SH-2b-2). `PersonaServingHost::start` needs a `TorServiceConfig`, and the wallet had no surface for one. The organizing principle for the surface it now has: **every knob is a fingerprint** — DQ-T0.7's argument about data-directory rotation, that deviation from defaults is itself a signature, generalizes across the whole struct — so the default answer is derive-or-inherit and exposure needs a reason. The three fields get three different answers. The **data directory is derived**, `

.tor/` off the same stem `

.prefs.toml` uses: that satisfies DQ-T0.7's wallet-adjacent / wallet-controlled / non-world-writable requirements by construction, and it forecloses the linkage a configurable path invites — a shared Tor directory is a shared entry-guard set, which cross-links every persona served through it. The **binary source is discovery**, with a single override (`device.tor_binary_path`, `PREFS_SCHEMA_VERSION` 3→4) because it is the one item where the operator holds information the wallet cannot derive; the hash gate runs on whatever it resolves to, so an override selects which candidate is gated, never whether. The **supervisor policy gets no surface at all** — backoff, `degrade_after`, `bootstrap_deadline` and `trust_retry` are observable in retry timing, so an operator-tuned supervisor is a distinguishable client. There is deliberately **no serve-or-not toggle**: a staker with holdings that does not serve accrues misses and slashes, so the bond is the decision, not a preference. - **A staker whose serving path cannot be configured does not open** (SH-2b-2). Same fail-closed posture as `wrap_and_start_pscan`, for the same reason: opening with serving dark is opening into an accruing slash the operator cannot see. The derived directory is created **private** rather than umask-shaped — `create_dir_all` honours the umask, which typically yields `0o775`, leaving entry-guard state group-writable — and a *pre-existing* directory that is group- or world-writable is refused rather than silently tightened, because the wallet did not create it and does not know what else relies on its mode. A `device.tor_binary_path` that names no file on this machine falls back to discovery with one warning rather than failing: the prefs file travels with the wallet cluster, and a moved wallet must not break. - **Untrusted numeric conversions: one live over-reservation fixed, the cast gate made real at the trust boundary (SA-6, SA-R-7 — ratified 2026-08-14).** The census behind the ruling (298 non-test `as` casts across the exposed crates) found the reject-via-`try_from` convention already lint-enforced and the decode crates cast-free — and three gate defects: (1) `shekyl-ffi::parse_prove_witness` reserved `Vec` capacity directly from three C-ABI wire counts, so a hostile 4-byte count could demand up to ~412 GB and abort the whole C++ host process on allocation failure — now capped before the reserve by the bytes that must back the elements (the same guard the function already applied to sibling counts), with a pinned refusal test; (2) `shekyl-ffi`'s crate-root `#![allow(clippy::cast_possible_truncation, cast_sign_loss)]` silently overrode the workspace deny on the one crate sitting on the C++ trust boundary — narrowed to site-level allows, each with a value-preserving rationale; (3) `shekyl-daemon-rpc`, `shekyl-wallet-rpc`, `shekyl-cli`, and `shekyl-relay` never inherited the workspace lint table (`-D warnings` cannot enable an allow-by-default lint) — wired in via `[lints] workspace = true`, with ~100 latent lint sites fixed (every surveyed lossy cast proved construction-time value-preserving; no behavior, wire, or public-API change). The rule is codified as `SIGNATURE_ALIGNMENT.md` §2.3 SA-R-7 — **ratified 2026-08-14** after a reviewer-driven completeness check over every `with_capacity` in the FFI crate (30 sites: the three fixed wire-embedded counts were the only ones; ptr+len ABI-pair counts are guarded, with one unchecked multiply brought up to the crate's own pattern; `.len()`-derived counts safe by construction). `legacy_util::bounded_capacity` now owns those reserves (including sibling counts); C1/C2 share one parser. Ratification's remaining residual — a mechanical source-scan gate on raw `with_capacity` / bare `from_raw_parts` — lands in the SA conventions tail. - **The cryptographic inventory is closed (SA-6).** `docs/CRYPTOGRAPHIC_INVENTORY.md` now carries every section the SA round owed it: §1 primitive pins + the RNG-source map (two-policy split: fail-safe hedged nonces, fail-loud key material) and §2 the six-surface signing inventory, both transcribed from `SIGNATURE_ALIGNMENT.md` with every pin and scheme-domain constant re-verified at source; §4, the curve-based ZK risk register §5 had cited before the section existed (perfectly-hiding commitments keep recorded amounts private retroactively; proof soundness is the quantum-exposed property, co-signed by hybrid PQC auths and owned by the V4 transition); and §6, the close — the honest audit-status column (exact-pinned FIPS crates, no external audit until Phase 9) and the infrastructure survey, which found **release assets are published with no maintainer-key signature step**: filed as a rule-21 reopen with a dated `RELEASE_CHECKLIST.md` enforcement row so the first release cannot ship unsigned by omission. Four stale `ARCHIVAL_FIREWALL_GATE6.md` sites still calling the C-1 vin-layer ML-DSA equality check "not yet landed" corrected to DISCHARGED (#277). - **The wallet has an operator alarm channel, and the tor supervisor is its first producer** (OA-1). `ARCHIVAL_BOND_2D2_SP_T0_TOR.md` §3c calls `TorPosture::Degraded` "the operator-alarm hook (`82`)" and specifies how an alarm layer must render it; no alarm layer existed, so the supervisor has been publishing alarm-shaped edges into nothing. The new `shekyl-operator-alarm` crate is that layer, re-exported from `shekyl-engine-core`'s root. Its primary surface is a `watch` snapshot rather than an event stream, because the requirement that decides the shape is that a check which is **not running** must read as *disarmed* and never as healthy — and a stream cannot say that, since a disarmed check and a clean one both emit nothing. Its `tor_posture` module maps posture to board: the §3c episode rule (a degraded tor that briefly recovers and degrades again is **one** `IncidentId`, ending only at `Ready { recovering: false }`), one cause tag per `ServiceFailure` with no collapsing, and vanguard integrity marked `Disarmed` off `Ready` because the `warning` field exists on that variant and no other. It takes the posture *receiver*, not a `TorService`, so it is testable without a tor binary. It is a leaf crate rather than an engine module because it names no engine type: rule 82 makes failure-mode UX first-class, and a GUI matching on `OperatorAlarm` should not have to link the wallet orchestrator to render a board. - **Alarms are classified by whether their observable outlives their condition, not by a severity ladder** (OA-1). `AlarmLifetime::Episode` leaves the board when its producer reports the condition gone; `LatchedRederived` is held for acknowledgment, because the evidence disappears while the damage stands — a vanguard set that was re-drawn stays re-drawn after the next write succeeds and the warning clears. The latch is also what makes a *coalescing* channel safe: a `watch` keeps only the latest value, so an unlatched raise-and-clear inside one tick would vanish. A severity ladder could express the split but nothing would enforce it; here the class comes from an exhaustive match, so a new alarm does not compile until someone classifies it. Acknowledgment names the `IncidentId` the operator actually read, so a fault that arrived while they were reading cannot be dismissed unseen. - **Live status and outstanding faults are separate shapes on the alarm board** (OA-1). `ConditionState` carries at most one *live* alarm — which is all a producer can report, since the tor supervisor's `warning` is one `Option` — and resolved latches go to `AlarmBoard::unacknowledged`, a queue ordered oldest incident first. A single per-condition slot was wrong: the vanguard set has three distinct integrity faults on one `AlarmCondition` and only one of them latches, so a later unrelated fault would have evicted the unacknowledged record of a guard-set re-draw — silently losing exactly what latching exists to retain. The split also turns two previously runtime-checked properties structural: a latch survives its condition being disarmed because it does not live on that condition's row, and `acknowledge` cannot silence a fault that is still happening because a live alarm is not in the queue to acknowledge. A recurrence *reclaims* its queued incident rather than duplicating beside it — `LatchedRederived` means the producer re-detects the same unfixed problem on every start, which is one fault, not two. - **The tor control `EventSink` is not, and was never going to be, the alarm channel's input** (OA-1, grounded correction). The plan of record had a translator consuming it. No production call site issues `SETEVENTS` — the DQ-T0.4 `STREAM` subscription is a harness, and §3c's own retraction settled that bootstrap readiness is a `GETINFO` poll, "a command, not a subscription" — so in production the sink receives nothing at all, and `ControlReply` is a deliberately forensic surface ("parse it, never log it") rather than an alarm vocabulary. The wallet's honest production value is the new `EventSink::unsubscribed()`, which states that at the construction site instead of leaving it as an anonymous dropped receiver that reads like an accident. The alarm input is the posture watch, where the supervisor's liveness policy actually publishes. - **A full rescan now reconstructs a lost staking history — the principal scan's bond watch** (SA-R-6 from-seed reconstruction, `feat/rescan-slot-reconstruction`). At open, while the seed is transiently in scope, the wallet derives the public persona canonical ids for the probe window `{bonded} ∪ {cursor..=cursor+32}` into the new `StakingBlock::persona_id_cache` — once per slot for the wallet's life (ids are pure functions of the seed), so every later open and the credential-less `rescan_blockchain` have candidates at zero keygen cost. The ordinary refresh/rescan matches on-chain `Input::BondPost` observations against the cache and the merge **adopts** each sighted slot back into `bonded_slots`, records its first-sighting height (`bond_sightings`), re-arms `staking_enabled`, and raises the monotone `p_slot` cursor — positive evidence only, unconditional for every wallet (a never-staked wallet's watch simply never fires). A restored-from-seed wallet therefore recovers both the SA-R-6 no-reuse mark and the reachability of its bonded personas (Model D: `bonded_slots` is the only derive-forward input) on any full rescan, with no staking-specific recovery flow. Soundness bridges: the phantom GC evaluates sighted slots with the height-gated verdict (a stale pscan seal cannot collect a probe-adopted real bond), and the first-stake resume guard treats a persisted sighting as `AlreadyStaked` (no duplicate bond post while the P-scan lags). `STAKING_BLOCK_VERSION` 1→2, `WALLET_LEDGER_FORMAT_VERSION` 16→17 (refuse-don't-migrate; pre-genesis recreate). Histories deeper than 32 slots converge across open+rescan cycles; the watch sees only what the principal scan covers (a `restore_from_height` above the bonds needs a lower-floor rescan — the same birthday semantics as funds). - **Levin white-noise emit routed through Rust; the fragment algorithm has one implementation.** `epee::levin::make_noise_notify` and `make_fragmented_notify` are now forwarding shims over two new `shekyl_levin_*` FFI exports backed by `shekyl-levin`'s KAT'd `noise_notify` / `fragmented_notify` — the C++ fragment-padding algorithm (the privacy-load-bearing emit logic: constant on-wire size is the property white-noise exists to provide) is deleted rather than kept as a parity twin. The byte-exact `make_noise.*` / `make_fragment.*` gtests now execute through the FFI, turning the hand-mirrored KAT oracle into a live cross-language identity check in CI. With compression already Rust-owned, the entire Levin emit-transform surface (`IMPLEMENTATION_INDEX.md` LV row) is single-sourced in Rust; still C++ until LV-3: the read-side state machine and the plain notification/request/response finalize path. Emit also refuses a bucket whose `m_cb` would exceed the Levin packet limit, checked in `shekyl-levin` before any allocation (the on-wire body, not the inner payload fragmentation exists to split); the FFI wrappers are marshaling. - **Curve-tree actor fail-stop now resumes over the held store, not a path-reopen** (SH-2a). `CurveTreeHandle` keeps a `WriterRecovery` for the wallet's life and `respawn` rebuilds the writer over the same open store. A live serving host already holds that `Arc`; reopening the file would be `DatabaseAlreadyOpen` (or, if it succeeded, a second store the host is not serving). The wallet-close/open reopen poll stays on `open_and_spawn` only. Recovery is its own capability rather than a method on `ServingReader`: the reader is `Clone`d out to the persona serving crates, and resuming from it would have made every one of those copies able to mint a second writer beside the actor. - **The serve-set staleness tripwire reads one clock twice** (SH-2a). The lag is the store's own `sync_tip_height` stamped when a `PinnedServeSet` is minted, subtracted from the same reading later — not the record's `as_of_height`, which is the *daemon's* tip over RPC. Differencing those measured how far the wallet trailed its daemon, and reported `Current { lag: 0 }` for the whole of a catch-up: exactly the window in which holdings move and a gained shard goes unpinned. A tip *below* the stamp is now `Staleness::RolledBack` rather than a saturated zero — the sync tip is not monotonic, and the rollbacks that move it backwards also delete pinned-segment rows. And `PersonaServingHost` counts consecutive failed refreshes (`Staleness::RefreshFailing`), because a curve-tree actor that fail-stops and cannot be resumed freezes ingest and pinning together — a common mode no store-derived reading can see. - **A terminally-pruned serve-set member no longer wedges every later refresh** (SH-2a). `PinnedServeSet::acquire` still refuses to start over one (nothing is serving; the remedy is a store rebuild by chain replay), but `refreshed` records it on `already_pruned()` and installs the pins that succeeded. Refusing the whole report on refresh froze the witness for the life of the host, leaving every shard connected afterwards unpinned and prunable — `ARCHIVAL_CHALLENGE_MECHANISM.md` §9.6 item 4's slash, re-entering through the error path of the refresh built to close it. `not_yet_frozen()` is renamed `not_yet_frozen_at_last_pin()`: it is a point-in-time diagnostic, and servability has one live authority (`open_frozen_segment_body`) that the serving loop already asks per request. - **`stake` gains a fourth refusal: `-29503 STAKE_RECORD_MOVED`** (PR-SA-5, `docs/api/wallet_rpc.yaml`). The RPC reads its persona slot from the open engine *before* the credentialed reopen, and that reopen runs the SP-R0 open-time reconcile — which can collect that slot as a phantom (arm #3) or burn the monotone cursor past it for a retired persona (arm #2), either of which makes the earlier read stale. A reconcile that *adopts* a chain-proven bond resolves to `-29502 AlreadyStaked` instead, and that precedence is correct: the wallet has just proved it holds a confirmed bond. `Engine::first_stake` already refused fail-closed; the refusal was merely **mis-surfaced as `-32603` InternalError**. It is a legitimate domain state (nothing durable written, a plain re-invoke reads the reconciled record) and now reads as one, per rule 82. The code carries **no payload**: persona slot numbering is wallet-internal and the operator's remedy does not depend on it (rule 81). Deleting the staleness outright — resolving the elect slot *after* reconciliation — reshapes the first-stake intent and the spawn gate, a distinct validation surface tracked in `FOLLOWUPS.md`. - **Daemon relay: the anonymity zone is chosen once, at origination** (Q12-U3). Relayed traffic inherits its arrival zone; coherence holds it. `p = 0.5` (`MIXED_ELIGIBILITY_PCT_HUNDREDTHS = 5000`) is the indifference point on a linear trade, not a measured optimum, and is not operator-configurable. Originated traffic that rolls clearnet does so by design and is distinguishable at the site from fail-closed (chose anonymity, zone unusable, send nothing). `/get_stem_tallies` rows now carry a `zone` label; the endpoint stays admin-only. `origin_zone` is still not a routing field. Named consumer: the Q12-D6a isolation arm. `on_relay_tx` and a missed submit nudge are residual absorption paths (zone re-decided after origination) and are recorded, not closed here. `send_txs` requires a `zone_route` token only `once_at_origin_route` can construct. - **Daemon relay: anonymity arrivals stem at arrival, and the txpool records which zone they arrived over** (Q12-U1 / Q12-U2 / Q12-U4, `feat/q12-u1-u2-arrival-zone-and-coherence`). `relay_method::forward` — "arrived over i2p/tor; hold, then broadcast to clearnet" — is deleted, along with the delay it timed. An arrival is now stemmed on the transport it arrived over, so R-1 coherence can keep it there for the rest of the stem. The pool still fluffs an expired stem to clearnet; that is the Dandelion++ exit, not a leak. The `origin_zone` field is written at first arrival (and not revised when a later peer upgrades the relay method). Q12-U3 later chose the zone at origination and labelled stem tallies by zone; this field remains a telemetry / isolation-arm instrument, not a routing input. - **Pinned crypto vectors: the SA-3a raw-public-key leaf-hash pins moved to a machine-readable fixture** (`docs/test_vectors/PQC_LEAF_HASH_RAW_PK_KAT.json`; PR-SA-3a, rule-30 vector-change record). The four byte pins — frozen captures of the pre-dedup `PqcLeafScalar::from_pqc_public_key` implementation, covering the degenerate lengths (empty, 1-byte) the derived-path `PQC_LEAF_HASH_KAT.json` cannot express — are **byte-identical relocations** of the inline hex that briefly lived in `rust/shekyl-fcmp/src/leaf.rs`; no vector was added, removed, or re-minted, so pin continuity is preserved. The consuming test (`pqc_leaf_hash_raw_pk_known_answer_vectors` in `shekyl-crypto-pq`) now asserts each vector through **both** consensus entry points (`hash_pqc_public_key` and the `shekyl-fcmp` wrapper), and the JSON is consumable by cross-language KAT tooling (C++/FFI differential harness). - **Pinned crypto vectors: the `bond_post` surface is retired from the hybrid-v2 KAT** (`docs/test_vectors/PQC_HYBRID_V2_KAT.json`; PR-SA-2b, rule-30 vector-change record). SA-2b resolved the bond-slot preimage question as "generic wins": the bond vin carries no on-vin signature, its authorization is the generic surface-A `pqc_auths` slot, and the dedicated `SCHEME_DOMAIN_BOND_POST` domain was deleted — so the fixture drops from seven vectors to six. The six surviving vectors and the pinned keypair are **byte-identical** to the previous fixture (the shared `KAT_MESSAGE` bytes are frozen with the pinned signatures; cross-build pin continuity is preserved — no construction change occurred and none is masked). - **Wallet internals: the in-flight-spend lock is now derived from the send journal instead of being stored on each output row** (`WALLET_SEND_RECORD.md` PR-SJ-1b, `feat/wallet-sj1b-field-retirement`). Runtime send/balance semantics are unchanged for a live wallet; balance computation got measurably faster (~14% fewer instructions on the hot path), and a wallet mid-rescan can no longer lose track of an in-flight send — the journal survives the wipe the old per-row lock did not. **Wallet-file break (pre-genesis):** files written by earlier builds need a re-create (`rm -rf` and restore from seed). Two user-facing consequences of the same change: - **A send that provably can never confirm now says so.** If a transaction's input turns up spent by a *different* transaction (another device restored from the same seed got there first), or a rescan from a higher block puts the input permanently out of the wallet's view, the send moves to presumed-dead in your history instead of sitting "pending" forever. Its input locks release with it. A late confirmation still flips the row back, loudly. - **Opening a wallet file from an older build gives the right error.** The version check now runs before the file body is parsed, so you get "unsupported wallet format version" — which tells you to re-create from your seed — rather than a parse error that reads like corruption on a wallet whose keys and seed are perfectly fine. ### Fixed - **`ci/gh-actions/rust` clippy compiles `ledger_iai` again after the client-request change.** `gungraun`'s `client_requests` feature pulls `valgrind-requests`, whose bindgen step needs `libclang.so`. The rust gate runs in a bare `ubuntu:24.04` container that had Valgrind headers but not libclang; `ci/benchmarks` stayed green because GitHub's hosted image already ships clang. Still gungraun — no harness swap. The container now installs `libclang-dev` and `clang`. - **`ci/benchmarks` capture no longer dies on `ledger_iai` `instructions=0`.** The six postcard cells were one-liners under gungraun's default `--toggle-collect=*::__gungraun_wrapper_mod::*`; when rustc inlined that wrapper, Callgrind recorded 0 and the producer guard rejected the run (PR #467 / `dev` from 2026-08-14). Those benches now bound the measured region with Callgrind client requests (`EntryPoint::None`, `--instr-atstart=no`) so collection does not depend on the wrapper symbol. See `docs/investigation/2026-05-09-bench-baseline-flake.md`. - **The RandomX v2 differential's daily cron is green again: the T8 RSS ceiling is re-derived for Arc reach-through residency, and the weekly cargo-mutants gate got its trigger back.** The T8 gate (`--mode=concurrent` RSS bound) had failed every daily cron since its first CI execution (2026-07-08): its 640 MiB ceiling modeled only `CacheStore`'s two slot holdings, but the workload's free-running workers legitimately keep displaced caches resident via their in-flight `Arc` holds — a flat ~1035 MiB drift plateau on the committed runner class, not a leak. The ceiling is now worker-count-derived (`(workers + 1) × 256 MiB + 128 MiB`, R1-D9 F4 Round-3 amendment in `RANDOMX_V2_PHASE2G_PLAN.md`); leak detection is preserved (a persistent per-rotation leak still trips the bound within four of the run's 32 rotations). Separately, a prior cleanup commit had accidentally severed the weekly `cargo-mutants` job's header, orphaning its steps into the runtime-modes job — leaving the T18 mutation gate with no trigger at all. The job is restored verbatim with its weekly cadence and 360-minute budget. - **`BlockchainLMDB::reset()` now wipes every table, so an in-place chain reset can no longer resurrect stale consensus state.** The old hand-written drop list omitted 25 Shekyl-added tables (the whole curve-tree and archival families plus block-burn and pending-additions); because a reset chain re-uses heights and hashes, a surviving row at a re-used key was read as the re-added block's state — an actively wrong `curve_tree_root`, silently. `reset()` now enumerates the environment's named tables and empties each one except the keep-list (`table_survives_chain_reset` — txpool only; mempool lifecycle is separate), then re-seeds the version row — the fresh-database state `open()` produces. A future table cannot be missed by construction. `reset_leaves_every_table_fresh` asserts every-table empty against the environment oracle and pins that populated txpool tables survive. No production flow currently reaches `reset()` (test harnesses only); this closes the hazard before any tool adopts in-place reset. ### Added - **LV-2b notifies — 2001–2004 / 2006–2010.** Typed Levin maps in `shekyl-levin` for `NOTIFY_NEW_BLOCK` through `NOTIFY_GET_TXPOOL_COMPLEMENT`, including `block_complete_entry` (pruned vs unpruned `txs`, `attestation_witness` transport cap `32 + 8 + 256 * 3385`). Empty STL containers omit the key (C++ match, including handshake peerlists). `dandelionpp_fluff` OPT default true; `CONTAINER_POD_AS_BLOB` for hash and `uint64` lists. In-crate consumer: encode → `notify()` → `BucketReader` (`tests/notify_kats.rs`). Does not wire the daemon (`handle_recv` / `net_node` remain LV-3). - **LV-2b dual-stack handshake against `shekyld`.** Ignored integration harness `rust/shekyl-levin/tests/dual_stack.rs` (`SHEKYLD_BIN`) speaks handshake / ping / timed-sync / support-flags with a live `--regtest` daemon over Levin. Default `cargo test -p shekyl-levin` still spawns no daemon (`LV2_PORTABLE_STORAGE.md` §12 step 4). - **LV-2b first drop — handshake / timed-sync / ping / support-flags.** Typed Levin maps in `shekyl-levin` on `shekyl-portable-storage` (1001 / 1002 / 1003 / 1007 plus the `network_address` union). OPT-omit, `cumulative_difficulty_top64` store-always / load-OPT, and encode → `invoke()` → `BucketReader` round-trips in `tests/payload_kats.rs`. Does not wire the daemon (`handle_recv` / `net_node` remain LV-3). - **LV-2 portable_storage decision** (`docs/design/LV2_PORTABLE_STORAGE.md`). First-party `shekyl-portable-storage` codec (LV-2a) then typed Levin command schemas in `shekyl-levin` (LV-2b). Cuprate `epee-encoding` is reference-not-dependency, closing the question `DAEMON_RELAY_PRIVACY.md` §8 deferred. Command inventory in `LEVIN_PROTOCOL.md` drops the non-existent 1004–1006 and adds live 2010 `NOTIFY_GET_TXPOOL_COMPLEMENT`. - **LV-2a `shekyl-portable-storage`.** First-party binary portable_storage codec, KAT'd against C++ `epee_serialization.cpp` (`two_keys`, `duplicate_key`) plus encode/decode limits as a parameter (Levin vs HTTP `.bin`). Completes `docs/PORTABLE_STORAGE.md`. Rewires `get_o_indexes.bin` (`shekyl-rpc-client`) and the test-only `get_blocks_by_height.bin` harness (`shekyl-engine-core`) onto the crate and deletes both homegrown parsers. LV-2b (typed Levin maps) is not in this change. - **You can now give up on a stuck send.** The new wallet-RPC method `abandon_tx` marks a dispatched-but-unconfirmed send `ABANDONED` in your history (`WALLET_SEND_RECORD.md` P3-4 / SJ-DQ-8, PR-SJ-3, `feat/wallet-abandon-sj3`). Closes the Phase 4c abandon FOLLOWUP. Abandoning is honest about what it does and does not do: - The record is kept, not hidden — and if the transaction confirms later anyway, the row flips to `CONFIRMED` loudly rather than staying wrong. - The retained per-transaction secret is kept, so a payment proof (`get_tx_proof`) still works for an abandoned send. - The spent-input locks are **not** released by intent: they release when the wallet's watchdog establishes the network no longer holds the send — the protection that stops two transactions provably spending the same input from linking your wallet. - A `CONFIRMED` or `FAILED` send refuses with `-29108` naming its state; there is deliberately no force override. Re-abandoning is a harmless no-op. - **Your sent transactions now appear in your transfer history, and they say what actually happened to them.** `get_transfers` / `get_transfer_by_id` project send-journal rows as `direction: OUTGOING` with the realized fee and txid-keyed ids (SJ-DQ-7, PR-SJ-2, `feat/wallet-rpc-outgoing-sj2`). Closes the Phase 4b W-D FOLLOWUP; Engine journal record landed in PR-SJ-1 (#414). Each outcome is its own state rather than being folded into a neighbouring one, because every such fold is a different lie to the person reading their history (rule 82): - `FAILED` — the daemon refused the send; it was never mined. Never reported as `CONFIRMED`. - `DROPPED` — the wallet's watchdog established that the network no longer holds the send and released the funds for re-spending. Never reported as `PENDING`: the wallet has stopped waiting, and saying otherwise would contradict the balance the same wallet reports. `Transfer.block_height` is now optional and means *inclusion height* only. A send that was never mined carries no height instead of the height it was dispatched at, so no client renders a plausible block number beside a payment that does not exist on chain. `since_height` bounds inclusion height and therefore never hides a row that has none — unsettled, failed and dropped sends survive any polling watermark, including across a reorg that returns a confirmed send to unsettled. `get_transfer_by_id` now distinguishes a malformed id (`-32602`) from a well-formed id naming no row (`-29400`): an uppercase txid pasted from a block explorer is answered as a formatting problem, not as "that transfer does not exist". `shekyl-cli wallet transfers` shows the fee alongside the amount, and prints `—` where a row has no inclusion height. - **WI-RPC-4 thin CLI/RPC surfaces** (`feat/wallet-rpc-wi-rpc-4-thin`): `get_wallet_info` aggregates wallet height, daemon height, balances, primary address, capability/network, restore floor, and staking summary in one round-trip (no new Engine API). `Transfer` gains FA-8 `attribution` — present on INCOMING rows only, since "which payment request did this arrive against" has no meaning for a row the wallet sent; `get_transfers` accepts an optional `attribution` filter. CLI un-stubs `engine_info` and `history incoming --unattributed`, and reports `?` rather than `0` for a height the server did not send (0 is a real height, so defaulting would show a synced wallet as unsynced). Closes the WI-RPC-2b deferrals for those commands; parity matrix rows 33/39 updated. Contract: `docs/api/wallet_rpc.yaml`. - **The wallet now keeps a durable record of its own sends.** A new send-journal ledger block (`docs/design/WALLET_SEND_RECORD.md`, PR-SJ-1) records, at dispatch, what a chain replay can never rebuild: the realized recipients and per-recipient amounts (cryptographically unrecoverable under CT), the realized wire fee, the selected input set, and the change amount. Rows follow the tx through the daemon verdict (accepted → lock baseline; terminal refusal → kept as failed-send history; retryable refusal → dispatch undone) and through refresh (confirmed at height; reorg returns the row to dispatched; watchdog release marks it presumed-dead — a display state, keys retained). The journal lives outside the scan-derived state, so it survives `rescan_blockchain` by construction, and a new merge post-pass re-derives the awaiting-confirmation locks from it during replay. ### Changed - **`rescan_blockchain` no longer refuses while a submitted transaction is unconfirmed.** The `-29202 RESCAN_BLOCKED` refusal previously covered two hazards; the unconfirmed-submitted-tx half is retired — the send journal carries the dispatched input set across the wipe and the locks re-derive during replay, so a never-mined transaction can no longer wedge rescan permanently. The refusal remains for outstanding pending-tx reservations (consumer-held or in-flight), whose in-memory output locks index rows the wipe would destroy. - **The relay `hop` parameter now carries measured provenance: the adopted verification-cost table lands.** `shekyl-relay-privacy` gains `verify_cost.rs` — the `DAEMON_RELAY_PRIVACY.md` §80 decision as code: a per-shape `f(n_in, tree_depth)` lookup over two closed axes, populated with the spec machine's (Raspberry Pi 4, rule 76) pinned measurements, refusing — never clamping — past its edge or at an unpopulated cell, with cell provenance as an asserted field so a spec value can never again be filled by scaling a fast machine's number. `DandelionParams::adopted()` derives `hop` through the table (124.5 ms modal verification + a 50 ms transit assumption recorded as such), and both FFI construction sites (embargo timer, relay zone) now build from it. **No behavior change:** the derived hop equals the inherited 175 ms by arithmetic — the old constant was Monero-era processing plus an ocean crossing; ours is FCMP++ verification plus the same crossing — so the embargo stays 190 s and the wallet propagation timeout 874 s, both test-pinned. What changed is that the number's justification is now a measurement with a named assumption instead of a 2019 laptop comment. Remaining, tracked in `FOLLOWUPS.md`: recovering the other 44 measured table cells from the bench-host artifacts, per-shape embargo consumption, and the clearnet hop-distribution measurement (§65.3). ### Removed - **The transitional `rust/shekyl-engine-rpc` crate is deleted** (C++-retirement roadmap B1). It was the wallet2-era Rust FFI library — a JSON-RPC server and embeddable library wrapping C++ `wallet2` through `src/wallet/wallet2_ffi.{h,cpp}` — and every consumer had already migrated off it: `shekyl-cli` became a native RPC client at WI-RPC-2a, the Tauri GUI wallet dropped it for in-process `shekyl-engine-core` path-deps, and Phase 4a/4b/4c built the Engine-native `shekyl-wallet-rpc` beside it rather than through it. Verified at deletion: no Cargo manifest in the workspace depended on it, no file under `rust/` outside the crate referenced `wallet2_ffi`, and `cmake/BuildRust.cmake` never built it (it builds `shekyl-ffi` and `shekyl-daemon-image` only), so no C++ extern could resolve against it. Retired with the crate: its `rust-scanner` feature and the `scanner_state::{LiveLedger, ScannerState}` read-side JSON-RPC cache (the FOLLOWUPS entry that scheduled this for a Phase 4b *cutover* is closed — it retired by deletion instead), its `native-sign` feature, and its `multisig` feature, which named no code inside the crate and only forwarded to `shekyl-engine-core` / `shekyl-fcmp`; dropping it from the `ci/multisig-feature` lane removes a no-op arm, not coverage. `.github/workflows/codeql.yml` loses a now-pointless `--exclude`, and `rust-audit-test.yml`'s `dalek-ff-group` isolation loop loses an entry whose every dependency is separately named in the same loop. **Not in scope:** the C++ `src/wallet/wallet2_ffi.{h,cpp}` facade this crate wrapped is untouched and still compiled. It now has no Rust consumer at all; deleting it is Phase 5's job (`docs/design/WALLET_REWRITE_PLAN.md` §Phase 5). `docs/WALLET_RPC_RUST.md` is re-anchored on that surviving C++ surface, and is no longer the wallet-RPC reference — that is `docs/api/wallet_rpc.yaml`. ## [3.1.0-alpha.7] - 2026-08-06 ### Changed - **Dandelion++ relay timing is now decided in Rust; the daemon C++ is a transport shim.** The relay-privacy arc (`DAEMON_RELAY_PRIVACY.md`, RP-1…RP-4; PRs #370, #372, #374, #377, #380) cut the stem/fluff epoch scheduler, stem map, fluff batching and delays, covert-channel scheduling, and the stem embargo out of `levin_notify.cpp` / `tx_pool.cpp` into `shekyl-relay` + `shekyl-relay-privacy` behind the `shekyl_relay_zone_*` / `shekyl_dandelionpp_*` FFI. `src/net/dandelionpp.{h,cpp}` is deleted; asio keeps only the sleep (one timer armed from Rust's `next_wake()`). The inherited mechanism was defective, not just relocated: the 39 s Poisson embargo did not follow from its own printed derivation and its distribution contradicted the derivation's survival assumption (F-1/F-2). The replacement is an exact-solve memoryless embargo, re-derived at **190 s** after `fluff_return_ms` was re-measured under the outbound-only flood the anonymity zones actually run (F-7: 2250 → 3250 ms); the wallet's propagation timeout moves 664 → **874 s**. Relayed clearnet arrivals now take a 2 %-per-hop anonymity-zone eligibility roll (R-1, #389), and per-successor stem outcomes are recorded (StemWatch) and readable via the new `/get_stem_tallies` RPC — recorded only; no reputation thresholds act on them yet. **Operator impact:** cover traffic ("noise") is **removed, not optional** — configuration B (Tor + noise) was an origin-routing oracle, so `--tx-proxy`'s `disable_noise` flag is a warn-and-ignore no-op and no shipped configuration sends covert padding (the wire-observer defense returns as Q-11 Unit 2). Anonymity-zone deployments now require an outbound-connection floor of **12**: `--tx-proxy` refuses below it and runtime `out_peers` changes clamp to it (F-8b). - **The relay minimum supported device is stated: Raspberry Pi 4.** The hop verification surface (#408, #409; `DAEMON_RELAY_PRIVACY.md` §§72–87) measured proof-verification cost across the full 48-cell shape domain on both x86 and Pi 4 arms and resolved the decisions: Pi 4 is the provisioning floor (promoted to `.cursor/rules/76-device-provisioning-floor.mdc`), per-shape cost is a lookup table rather than a scalar or a fitted curve (option D — decoupling `FCMP_MAX_INPUTS_PER_TX` from the embargo), and the table refuses below-spec entries rather than clamping. The adopted `hop` parameter set these decisions imply has **not yet shipped** in `shekyl-relay-privacy` (tracked in `IMPLEMENTATION_INDEX.md`, RP row). - **`blocks.dat` bootstrap files now carry the credit-wire attestation witness, and older files are refused.** `bootstrap::block_package` gained an `attestation_witness` field, written by `blockchain_export` and threaded through `blockchain_import`'s verifying connect. Without it a bootstrapped node held no witness for any imported block: it could serve none to its IBD peers and, after the credit-wire cutover, could not re-drive their admission. **Operator impact:** the bootstrap header minor version moves 1.0 → 1.1, and `blockchain_import` now refuses a file below 1.1 with an explicit "re-export it with this build" message rather than importing a permanently witness-less chain. Re-run `blockchain_export` to regenerate any existing `blocks.dat`. - **Genesis regenerated: the final five-founder allocation on every network, and the pins are now reproducible.** All three `GENESIS_TX` strings in `src/cryptonote_config.h` are re-pinned from freshly generated production wallets (fresh OS entropy via `geblock gen-wallets`; BIP-39 on mainnet/stagenet, raw-32 on testnet; every wallet restore-verified from its written seed before its address was committed). Mainnet and stagenet move from a single combined 100,000 SKL treasury output to the same **5 × 20,000 SKL** shape testnet already shipped; the total (100,000 SKL, the block-0 reward) is unchanged, as are every `NETWORK_ID` and `GENESIS_NONCE` (10000 / 10101 / 10002). Only public addresses are committed — no seed material is in any repository. Because the genesis tx key is derived from the recipients file, the pins are reproducible from committed inputs: `geblock verify` is green on all three networks and the byte-compare gate (`shekyl-genesis-tool/tests/config_pin_gate.rs::genesis_hex_matches_config_pin`) is **un-ignored and live in CI** — recipients-vs-pin drift now fails the build. The placeholder-era generator test is deleted with the era. Derived artifacts re-pinned in the same change: the `mining_parity` frozen genesis ids; the `shekyl-wire` regtest vectors (`regtest_coinbase_h{0,1,2}.block` + hashes; h0 **is** the mainnet genesis, so it now carries five outputs — `coinbase_roundtrip` asserts the count per height); and the `shekyl-rpc-types` §3.4 txid-oracle miner-tx blobs (`regtest_coinbase_h{0,1}.tx` + pins), which extract from those wire vectors and must move with them. Each network's genesis id is confirmed by three independent derivations: `geblock block-id` (pure Rust), the C++ `generate_genesis_block` path in `mining_parity`, and the live-daemon RPC capture (`coinbase_hash` also CI-cross-anchors h0 to the published mainnet id). `mining_parity` asserts the shipped genesis `tx_extra` is a fixed point of C++ `sort_tx_extra` **and** the explicit field order `0x01 → 0x06 → 0x07`, making geblock's canonical-emit-order claim fully executable against the real sorter and field parser. - **`docs/GENESIS_ALLOCATIONS.md` published**, fulfilling the `GENESIS_TRANSPARENCY.md` §5 commitment: per-network allocation tables, genesis identities, the tx-key derivation spec, key-custody statement, and the commands to rebuild and check the pinned bytes independently. ### Added - **Levin p2p compression routed through Rust; system libzstd dropped.** The C++ `epee::levin` compression path is now a marshaling shim over the new `shekyl_levin_*` FFI (`rust/shekyl-ffi/src/levin_ffi.rs`, backed by `shekyl-levin`), making the workspace-pinned vendored libzstd the binary's **single** zstd implementation: the root-CMake zstd detection, the `HAVE_ZSTD` gate (and its silently-degraded "compression disabled" fallback), the epee system-libzstd link, and the C++ compression constants are all deleted. Receive-side hardening rode the cut: the C++ handler now bounds decompression by `min(packet limit, per-command cap)` — the same bound the Rust `BucketReader` enforces — closing a ~34 MB inflation accept-window on `NOTIFY_NEW_TRANSACTIONS` and a 128-MiB-allocation-per-connection exhaustion surface — the bound is the *same* expression the bucket header is checked against, so a compressed message can deliver exactly what an uncompressed one could and no more. The emit seam is **whole-message** (`shekyl_levin_compress_message`), not payload-level: whether a buffer may be compressed at all is a property of its bucket header, so a payload-level seam left those questions stranded in C++ as a second, weaker copy of the policy — one that checked neither the signature, nor that the header accounted for every byte after it, nor the noise/fragment class whose constant on-wire size is the entire property the white-noise feature buys. `epee::levin::try_compress_message` is now a forwarding shim and `epee::levin::compress_payload` is gone; the C++ carries no compression policy at all. Five new `levin_compression` gtests cover the seam (the C++ tree previously had zero compression coverage), including `cover_traffic_is_returned_unchanged` — negative-controlled, a 4 KiB dummy compresses to 52 bytes without the guard. The Levin constant-parity CI gate drops its three compression rows (single-sourced in Rust now, nothing left to drift). `zstd` is promoted to `[workspace.dependencies]` (decision 2026-08-06) — not to hold a place for the anticipated shard-serving consumer, which the same day's Z-1 measurement ruled out, but because the feature set is load-bearing and must not be re-litigated per crate — pinned `default-features = false`: the default `zdict_builder` compiles dictionary-builder C that calls POSIX `qsort_r` and breaks the Win64 mingw link once zstd objects enter `libshekyl_ffi.a`, and the default `legacy` compiles decoders for frame formats v0.1–v0.7 we never emit, which would only widen the attacker-reachable C surface on the p2p receive path. Four hardening items rode the same cut: - **Padding is no longer compressed away.** `--pad-transactions` quantizes a relayed tx blob to a 1024-byte boundary so an observer cannot read transaction volume off the frame size; zstd erases that run almost perfectly. Since the `HAVE_ZSTD` gate is gone, compression is now unconditional, so `make_payload_send_txs` skips the compressor when `pad` is set — privacy over bandwidth, at the only layer that knows the size was deliberate. Pinned on the *wire* bytes by `levin_notify.padding_survives_the_emit_path` (the existing padding tests inspect the decoded message, which carries its padding either way and so cannot see this), with `levin_notify.unpadded_messages_still_compress` as the control that the guard did not simply switch compression off. The test uses pseudorandom bodies matching the Z-1 entropy measurement, because the hazard does not depend on compressible transactions: padding is a long run of one character, so a padded message compresses however random its transactions are. Measured — 9216 payload bytes padded, 8237 on the wire with the guard removed, quantization gone. - **Decompression writes into caller storage.** `shekyl_levin_ inflated_size` + `shekyl_levin_decompress_into` replace the buffer-returning decompress export, so the C++ shim inflates straight into its `std::string`: one allocation instead of two, no copy across the FFI, no wipe of public wire data, and no heap ownership crossing the boundary on the IBD receive path. - **Failure causes are distinguishable.** A frame that is malformed (`-3`) and one whose declared size exceeds the cap (`-7`) are separate codes with separate log lines. Both close the connection, but they call for opposite operator responses, and a single code cannot say which. - **`ShekylBuffer::from_vec` shrinks to fit.** A `ShekylBuffer` has no capacity field, so `shekyl_buffer_free` must free through `Vec::from_raw_parts(ptr, len, len)` — sound only if the allocation was exactly `len` wide, which `zstd::bulk::compress` (sized at `compress_bound`) is not. Now an invariant of the boundary rather than a per-caller obligation. - **`shekyl-levin` — Rust Levin framing crate (LV-1), inert until wired.** The byte-exact Rust skeleton of `docs/LEVIN_PROTOCOL.md`: the 33-byte bucket header, notification/request/response builders, noise and noise-shaped fragmentation, the zstd `COMPRESSED` path (feature-gated, `HAVE_ZSTD` parity), and an incremental `BucketReader` mirroring the C++ `handle_recv` state machine (signature early-reject, 256 KiB / 100 MB packet limits, fragment reassembly, message classification). Byte identity is pinned by KATs mirroring the `tests/unit_tests/levin.cpp` gtests assertion for assertion, and by a CI gate (`ci/levin-constant-parity`) that fails the build when any hand-copied wire constant stops matching its C++ definition — including `HEADER_SIZE`, anchored by a new `static_assert(sizeof(bucket_head2) == 33)` in `levin_base.h`. The reader is feed/pull (`feed` buffers, `next_message` parses one bucket), matching the oracle's dispatch-inside- the-parse-loop shape: at most one decoded payload is live at a time, and a delivered message survives a later bucket in the same read being malformed. Divergences from the C++ are enumerated **once**, in the crate docs (`rust/shekyl-levin/src/lib.rs`); every other mention points there rather than restating the list. Framing only — no portable_storage, no sockets, no daemon wiring; the C++ p2p path stays live until the scheduled cutover (`IMPLEMENTATION_INDEX.md` LV row; FOLLOWUPS "Levin p2p migration" carries LV-2/LV-3). New optional dependency: `zstd` 0.13 — compatibility with C++ peers comes from the zstd *frame format*, not from a shared library: `zstd-sys` compiles its own vendored libzstd unless its non-default `pkg-config` feature is enabled, while the C++ links the system one. Choosing between them is an LV-3 item (see the manifest comment and the FOLLOWUPS entry) — it needs a link graph that does not exist while the crate is in no staticlib. - **Phase 4c wallet RPC rescan** (`Engine::start_rescan` + `rescan_blockchain`). Named Engine API that rebuilds the wallet's transaction history from the chain, starting at the wallet's scan floor. Everything a replay cannot re-derive survives — retained `tx_keys`, notes, payment-request rows, restore height, and the durable staking bond record; everything scan-derived is rebuilt. The chain-global curve tree is left untouched (it holds no wallet data). Every refusal is raised *before* the destructive step: the single-flight slot it shares with `refresh` (`-29200`), a daemon preflight (`-29201`), and a new `-29202` `RESCAN_BLOCKED` for in-flight transactions whose spend record a chain replay cannot rebuild — checked atomically with the reset, so a concurrently-built transaction cannot slip through. Mid-scan failures after the reset is durable return `-29203 RESCAN_INCOMPLETE` for every join-path producer failure (not `-29201`, whose rescan contract is "wallet untouched", and not a catch-all `-32603`). `shekyl-wallet-rpc` dispatches `rescan_blockchain`; CLI `rescan` is un-stubbed and reports whether a failure left the wallet reset. Closes FOLLOWUPS Phase 4b rescan / WI-RPC-2b `rescan` deferrals; opens the `abandon_tx` follow-up that `-29202` needs as an escape hatch. Of the Phase 4b quality gaps only the OUTGOING filter remains Engine-gated — the build write-lock and submit verdict entries closed in this same release (below). - **`submit_pending_tx` reports the real daemon verdict.** The wallet-RPC send lifecycle no longer flattens every successful submit to `ACCEPTED`: `Engine::submit_pending_tx{,_async}` returns the new identity-bearing `SubmitOutcome`, and the OpenAPI `ALREADY_IN_POOL` / `ALREADY_IN_CHAIN` verdicts — with the verdict-scoped `confirmed_height` carrying the daemon-claimed confirming height (display metadata, never settlement truth) — are reachable for the first time. The CLI renders the three verdicts distinctly. Lock lifecycle is unchanged: every success verdict places the F14 awaiting-confirmation lock and refresh remains the settlement authority (`DAEMON_SUBMIT_VERDICT.md` §2.5 / §7.2). - **`geblock` — deterministic Rust genesis pipeline (`rust/shekyl-genesis-tool`).** Replaces the C++ `genesis_builder` end-to-end: `gen-wallets` (exactly five fresh-entropy founder/developer wallets — BIP-39 for mainnet/stagenet, raw-32 for testnet; 0600 custody files staged then promoted, refused inside any git tree before mkdir), `build` (genesis coinbase hex, same stdout contract as the old tool), `verify` (rebuilds all three networks and byte-compares against the `GENESIS_TX` pins in `src/cryptonote_config.h`, replacing the dead `verify_genesis.py`; pin-parser always gated on the real header), and `block-id` (v9-header genesis block id + tx hash). The genesis tx key is now **deterministic** — cSHAKE256 with customization `shekyl/genesis-txkey-v1` over the recipients file — so the pinned genesis is byte-reproducible from committed inputs for the first time; genesis is CT-Null/cleartext/published by design, so the public derivation discloses nothing (`GENESIS_TRANSPARENCY.md` §5). Recipients move in-repo to `config/genesis_recipients.{mainnet,testnet,stagenet}.json` in the final 5 × 20,000 SKL shape on **all** networks (mainnet/stagenet placeholder-keyed until the founder ceremony; the combined single treasury output is retired). Golden KATs pin the derivation → output construction → wire encoding → v9 block hash chain; the config-pin **byte-compare** gate lands `#[ignore]`d until the post-regen re-pin un-ignores it (parse coverage is always on). ### Removed - **C++ genesis-builder path retired** (rules 15/16/20 — superseded by `geblock`): `cryptonote::build_genesis_coinbase_from_destinations` (`cryptonote_tx_utils.{h,cpp}`; its fresh-txkey draw made every build irreproducible), the `GENESIS_TOOL_SRC_DIR` CMake seam + `genesis-builder` Makefile target (the shekyl-dev `tools/genesis_builder` C++ tool they built is deleted in shekyl-dev), and the out-of-build-graph `shekyl-crypto-pq/examples/gen_genesis_addrs.rs` placeholder generator (its label-derivation continues, documented and testable, as the `placeholder_recipients` test in `shekyl-genesis-tool`). ### Fixed - **`build_pending_tx` / `submit_pending_tx` no longer stall read RPCs.** Slow send work (FCMP++ membership assembly, daemon submit RPC) ran under the wallet-RPC process's exclusive Engine lock, so `get_balance` / `get_height` / `get_transfers` blocked for the whole operation. `Engine::build_pending_tx{,_async}` and `Engine::submit_pending_tx{,_async}` (and `discard_pending_tx`) now take `&self`; wallet-RPC holds a read lock; builds serialize on an engine-owned permit of one inside the engine — taken by build's membership assembly and by submit's stale-reference re-anchor alike, so the network observable is unchanged (one membership-assembly round-trip at a time; raising the permit is an explicitly gated future decision, not a tuning knob). - **A build no longer commits inputs a concurrent refresh invalidated.** `refresh` merges scan results under a *shared* engine borrow, so it was never serialized against the send lifecycle by the wallet-RPC lock — under the write lock a merge simply landed just after the build instead of during it, and either way the build committed against the state it selected from. Build now re-validates its selected inputs at the commit boundary — the same lock₂ discipline the CT-5d re-anchor applies to its reference — and refuses if the transfer vector shifted under a reorg or an input was observed spent elsewhere, rather than returning a `PendingTx` the daemon is certain to reject as a double spend. - **A daemon's `AlreadyInChain` height can no longer strand the spent inputs.** The claimed confirming height is untrusted metadata (`DAEMON_SUBMIT_VERDICT.md` §7.2) but was persisted verbatim as the submit watchdog's horizon baseline. A claim above the wallet's tip — a lying remote node, a reorg-confused daemon, or `u64::MAX` — held the horizon at zero forever, so the escape ladder never ran and the persisted awaiting-confirmation lock never released: the inputs stayed unspendable across restarts with no alarm. The baseline is now clamped to a height the wallet has actually reached, making §7.2's "bounded liveness cost" damage cap true by construction. The raw claim still routes the release path and still reaches the client. ## [3.1.0-alpha.7] - 2026-08-03 ### Changed - **8C retention-proof Status REVERSED in part — the challenge payload is superseded by the test≡job sequencing ruling** (`ARCHIVAL_RETENTION_PROOF_8C_FEASIBILITY.md` ↔ `ARCHIVAL_TEST_EQUALS_JOB_SEQUENCING.md`, 2026-07-29). The Round-1 BUILD statement (~3 KB Merkle opening as the serve challenge) is reversed on its payload: the test must demand the shard (~3.3 MB), because pricing the test three orders of magnitude below the service manufactured the A5 free-rider margin, and the doc's own §3 (public CT tuples — any opening-shaped test is reacquirable by construction) and §4 ("`m` is the wrong lever until §7.5 closes") already carried the root cause. §7.5's reacquisition *hinge* is reinstated while Round 1's fetch-on-demand *doctrine* survives — payload corrected, doctrine preserved. The `R_k`-anchored verify machinery survives as TJ-A's substrate. Do not build the challenge from the Round-1 statement. ### Added - **Shard-indexed staker-share escalation — shape frozen and wired live, genesis-neutral (D2 Stage 3; `feat/stage3-escalation-shape`).** The staker pool share is no longer a frozen constant: it is a pure map of `n = frozen_segment_count` (the archival burden operand), with the §6.1 shape — monotone, floored at today's 25%, asymptote < 100%, banded-PL integer fixed-point, no controller — implemented in `shekyl-units`/`shekyl-economics` with the five constraints as tests and `EscalationParams` load-time validation (persisted-params digest `0x01`→`0x02`). The wiring is live consensus C++: every burn split routes `shekyl_compute_burn_split_escalated`, the operand is read at parent-block state through the asserting `Blockchain::parent_frozen_segment_count` (throws on a template/connect divergence — that mispricing is a chain halt), `n` is computed once per template build (both `construct_miner_tx` passes) and once per connect (money check and staker-inflow accrual share it), and the `prev_block`/`from_block` template path — the one path that could not read its parent's `n` — is deleted with the RPC field reserved and loudly refused. `SHEKYL_STAKER_POOL_SHARE` is deleted from the generated C++ header: no share constant crosses the FFI. Shipped **genesis-neutral by construction** (`asymptote_share == floor_share`, bit-identity to the flat constant pinned by test); the asymptote numeric stays provisional-until-testnet under the §11.4 capture-point ceremony (`ARCHIVAL_WORK_PRECISION_AND_ESCALATION.md` §12.12). - **Daemon chain-store design (`DRS-*`) — gap-close (docs only).** SoT [`docs/design/DAEMON_REDB_STORE.md`](design/DAEMON_REDB_STORE.md) (§0.1 Tier A/B, surface map, P0 envelope, findings A-1…A-6). Implementation PRs (pure-virtual hooks, etc.) go on short-lived branches off `dev`, not direct-to-`dev` design-session edits. - **Transaction proofs and reserve proofs across the wallet stack** (WI-RPC-3; Phase 2c, `feat/wallet-rpc-proofs`). The `shekyl-proofs` DLEQ primitives are projected through the Engine (`engine/proofs.rs` workflow with explicit `ProofsCtx` per `ENGINE_COMPOSITION_DECOMPOSITION.md`; KeyActor generates inbound and reserve proofs so secrets never leave the actor), the `wallet_rpc.yaml` contract, `shekyl-wallet-rpc` (`get_tx_proof`, `check_tx_proof`, `get_reserve_proof`, `check_reserve_proof`; the `check_*` pair is wallet-less — a verifier needs only a daemon), and the CLI (RESERVED stubs un-stubbed, with disclosure warnings and proof strings kept out of readline history). Proof strings are Bech32m (`shekyltxproof…`/`shekylreserveproof…`). Prerequisite landed in the same tranche: the send path now retains per-tx secrets in `TxMetaBlock.tx_keys` at dispatch, with the spend-quadruple (`TransferDetails.spending_tx_hash`, `KeyImageObserved.containing_tx_hash`) and reconciliation keeping the I-2 no-orphans invariant through confirmation, reorg, and rescan. Review hardening (F-13): `check_reserve_proof` rejects any proof whose entries repeat a key image (`-29300` malformed) — entries verify independently, so a duplicated unspent output would otherwise inflate the proven reserve N-fold. Review hardening (2026-07-24 high review, retention lifecycle): the retention write moved from the submit-ACCEPTED verdict to dispatch (persisted atomically with the in-flight flip, before the bytes can reach the daemon), closing the crash/restart window that silently lost the secret of an already-exposed tx; terminal and retryable submit verdicts (provably unrelayed) retire the record so refused builds still leave no residue. Retention death is now reconciliation-only: a reorg rewind re-pends unreferenced entries instead of collecting them (a rewound tx commonly re-confirms, and a deleted secret can never serve that proof), and the watchdog's confirmed-absent release clears F14 locks only — its trigger is a local relay verdict, not a proof of network-wide absence. Lifecycle pins amended in `wallet_rpc.yaml` §OUTBOUND PREREQUISITE. Review hardening (2026-07-24 merge review, F1–F6): `get_reserve_proof` refuses `amount = "0"` at the RPC params surface (-32602 "amount must be positive"; the engine's selection independently refuses an empty selection as `-29302`, so a zero bound can never reach the key actor and surface as an internal error); `check_reserve_proof` resolves locator-named txs in chunked batched `get_transactions` calls (100 hashes per call, the daemon's restricted-RPC cap) instead of one round trip per unique txid; OUTBOUND generation reuses the shared secrets derived during recipient-output discovery (`generate_outbound_proof_with_secrets` — the hybrid KEM runs once per output instead of twice; wire format unchanged, entries stay `Zeroizing` in transit); the per-output hybrid-KEM ciphertext layout constants have one home (`shekyl_crypto_pq::kem::{X25519_KEM_CT_LEN, HYBRID_KEM_CT_LEN}` — the scanner/engine/fee-model copies are gone); actor-side proof failures keep their typed `ProofError` through the workflow error mapping (`KeyEngineError::Proof` → `ProofsError::Generate`, honoring the variant's documented discrimination); and the wallet-rpc `Hex32` rule has a single shared parser (`params::parse_hex32`) behind both the txid and transfer-id surfaces. - **cli: receiving, staking, and fee commands over the WI-RPC-1 surface** (WI-RPC-2b). `request new` / `requests list` (payment requests — the Shekyl-native receive-attribution primitive, un-deadening the FA-8 stubs), `make_uri` / `parse_uri`, `stake` / `staked_balance` / `staked_outputs` / `staking_info` (never-conflated staked-balance breakdown as the RPC reports it), and `fee` (tier quotes + size/weight estimate; principal lane only — P-lane fees are canonical and never user-facing). ### Fixed - **consensus: archival slash single-struck on one missed baseline — sliding-window m-of-n failure confirmation now gates it** (`feat/archival-failure-window`; builds the pinned-but-unimplemented policy of `ARCHIVAL_FAILURE_CONFIRMATION_PIN.md` §1, pinned 2026-06-08; pre-genesis V3.0 correctness gap). The m-of-n existed only in the Round-1 sim (`failure_confirmation::run_sliding`), so consensus went straight from gate-2 §6's `challenge_failed(P, s, E)` to the gate-4 §4.2 slash and forfeited a `FLOOR` on a *single* missed challenge — punishing exactly the isolated connectivity flukes the pin exists to absorb, and leaving the Round-2 stressnet with nothing to stress. The slash now fires only when `m` misses fall within the last `n` baseline **observations** for a `(P_id, shard)` (`shekyl-archival-retention::failure_window`, reached through `shekyl_archival_failure_window_params` / `_slashable`). The interval-append is untouched — `slash_open_interval_to_append` still produces exactly what the writer appends; only *whether* it fires moved. The window counts epochs at which a challenge was actually **posed**, so a bonded-but-untested epoch can never become a miss, and look-back stops at the boundary of the pair's current continuous challengeable run — which makes a `Rebond`-reinstated record start clean rather than sit one or two misses from the next slash. Nothing is persisted: the window is recomputed from the `serve_credit_bit` ledger and the bond record, both already reverted by `pop_block`, so there is no schema change (no rule-42 bump) and reorg safety is structural. `m = 11` / `n = 13` are the Round-1 **provisional** values in `config/consensus_constants.json` (shape genesis-frozen, numerics re-pinned at the Round-2 stressnet — the `bond_duration` precedent); C++ reads them through the FFI, so there is no cross-language drift pair, and a re-pin is a deliberate two-site edit (the JSON value plus the Round-1 sentinel const-assert that pins the shipped pair — the `bond_floor` idiom). The consensus decision is held equal to the sim policy that measured the pin by an exhaustive equivalence sweep, and `archival_substrate_lmdb` covers absorb / sustained-slash / full-window-slide / pop-recompute at the scheduler's production site. **Consequence — the window working as designed at end-of-life:** the window widens the exit-forgiven tail, so a persona that goes dark can `Unbond` with full collateral inside the grace, where single-strike would have slashed it at the next deadline. Grounded in the numbers this is not a new escape: the release cooldown is 2 epochs, far below `m = 11`, so a departing record only rides the **same** `m − 1` forgiveness envelope a still-bonded record already gets, earns nothing while dark, and cannot exceed the window's tolerance without crossing `m`-of-`n` and being slashed. An exit-time "clear the window" gate would punish the honest crash-then-leave record identically to the adversarial squeeze — they are on-chain indistinguishable — so it is a mis-fix (rule 82); the exit tail is **subsumed by the `m`/`n` sizing** the Round-2 stressnet already owns (`FOLLOWUPS.md` V3.1), not a separate decision (rule 21). Two `db_lmdb.cpp` comments that justified the exit-forgiven tail with "'stop serving and just hold' keeps being slashed every epoch" were single-strike arguments and are corrected in place. - **scanner: KEM-ciphertext extra packing — vout ≥ 1 of multi-output txs was silently unscannable** (`fix/kem-extra-packing`; closes the FOLLOWUPS V3.0 genesis gate "KEM-ciphertext extra packing mismatch", WI-RPC-3 review F-14; pre-genesis, no compat shim per rule 60). `Extra::for_hybrid_transfer` emitted one `0x06` `PqcKemCiphertext` extra field per output, but every reader — the scanner's scan path, the proof-check path, `shekyl-wire::pqc_kem_per_output`, and the C++ `wallet2` reader — consumes a single field and slices output `o`'s ciphertext at `o × HYBRID_KEM_CT_LEN` within it (the packing the C++ writers already emitted). Every output at vout ≥ 1 of a production-built multi-output tx — including all change — was therefore undetectable by the wallet and unverifiable by inbound proof checking. The writer now concatenates all per-output ciphertexts into one `0x06` field (readers unchanged); `tx_fee_model::extra_kem_field_weight(n_out)` accounts the single field so `predict_weight` stays byte-exact; bench fixtures serialize through the production writer; regression test asserts both vouts of a production-packed 2-output tx recover through `Scanner::scan`. On-chain data was intact throughout — a rescan recovers. ### Changed - **docs: verified-complete archive policy + Batch 1 relocate.** Wrote the durable policy (previously commit-message-only) into [`docs/design/IMPLEMENTATION_INDEX.md`](design/IMPLEMENTATION_INDEX.md) §8, with eligibility triad, living-SoT hold rule, move checklist, and Batches 2–3 / hold ledger. Moved four closed historical records to `docs/completed/` after self-check + code verify: `ARCHIVAL_REWARD_GATE_M1.md` (RETIRED #346), `ARCHIVAL_COVER_DRAW.md` (RETIRED), `REWARD_EMISSION_E3_GATING_ROUND.md` (round CLOSED), `ARCHIVAL_EXIT_STANDOFF_FD4_WINDOW.md` (mechanism deleted; X-1…X-3 CLOSED). Inbound/outbound relative links and path citations repaired (`FOLLOWUPS`, `RELEASE_CHECKLIST`, living design SoTs, `shekyl-standoff` module docs). Stage 0 / Stage 1 PR 4–7 and closed-audit pins remain follow-up batches per the ledger. - **docs: `CONFIDENTIAL_STAKING` supersession hardening (agent-misread).** The retired confidential-claim / 3C / entitlement staking body moved to `docs/completed/CONFIDENTIAL_STAKING.md` (deleted 2026-08-26) with a loud non-SoT banner. A short redirect stub remains at `docs/design/CONFIDENTIAL_STAKING.md` (deleted 2026-08-26) naming the living authoritative surfaces (`PHASE_2B_STAKE_LIFECYCLE`, `PRINCIPAL_STAKE_LIFECYCLE`, `REWARD_EMISSION_LEG`, bond/firewall docs) so basename greps under `docs/design/` hit the tombstone instead of ~1300 lines of obsolete design that read as current. Index §8 records the superseded-with-stub pattern. - **proofs: tx-proof wire format carries per-entry vout indices** (WI-RPC-3; pre-genesis wire change, no compatibility shim per rule 60). Outbound/inbound per-output entries grew a `vout_index[u32 LE]` prefix (128 → 132 bytes) so verification re-derives KEM secrets at the *actual* output index; previously non-prefix output subsets verified incorrectly against positional indices. FFI (`shekyl_generate_tx_proof_inbound`) and the wallet2 caller pass explicit indices. ### Removed - **cli: `get_tx_key` / `check_tx_key`** (WI-RPC-3; rule 60/21). Exporting the raw per-transaction secret is the wallet2-era mechanism; the DLEQ-based `get_tx_proof`/`check_tx_proof` is the product (proves the payment without an interactive secret handoff, and INBOUND proofs never reveal wallet key material at all). The commands refuse at parse time with guidance naming the replacement. - **engine-state: `TxSecretKeys.additional`** (WI-RPC-3 adjacent, rule 15). No producer existed — the send path signs every output including change with one tx key; `TX_META_BLOCK_VERSION` bumped to 2. - **cli: wallet2-era commands with no Shekyl-native equivalent** (WI-RPC-2b; rule 60, WI-RPC-1 pin 1). `account`* / `address new` (no account or subaddress model — payment requests replace receive attribution), `seed` / `viewkey` / `spendkey` (no secret-egress RPC by design; the mnemonic shows once at create/restore), `export`/ `import_key_images` (Phase 2d bundles replace the workflow — locked), and `sweep_all` (no engine surface; FOLLOWUPS row carries the reopening criterion). Removed commands and `--account`/`--subaddr-*` flags refuse at parse time with guidance naming the replacement. RESERVED stubs (proofs, `sign`/`verify`, offline signing, `rescan`, wallet info, unattributed receives) name their gates; carriers in `docs/FOLLOWUPS.md` §"WI-RPC-2b deferrals". `CLI_PARITY_MATRIX.md` reconciled against the new command set. ### Security - **wallet-rpc: in-process RPC serves over a private Unix domain socket, not auth-less loopback TCP** (WI-RPC-2a; `WALLET_REWRITE_PLAN.md` Shape B). `spawn_in_process` previously bound an ephemeral `127.0.0.1` TCP port with authentication disabled — reachable by any local process for the lifetime of a CLI session. It now serves over a UDS created `0600` inside a private per-spawn `0700` directory under `$XDG_RUNTIME_DIR` (private tempdir fallback), removed on shutdown. Priority-1 fix under `00-mission.mdc`: the wallet's unauthenticated command surface is no longer visible to other local users or processes outside the owning uid. - **cli/wallet-rpc: seed and secret hygiene across the RPC surface** (WI-RPC-2a). The interactive `create` now preflights terminal safety *before* creating the wallet and has no non-TTY fallback: it refuses to print the one-time seed to a pipe, redirect, or log (the deliberate, auditable file path is `create --seed-out`). The JSON-RPC client zeroizes the serialized request/response byte buffers that carry the password and seed (rule 35), not just the caller's own copies. Internal wallet-RPC error messages no longer echo raw `io`/persistence strings that can carry a local filesystem path or schema detail — they return a category-only message and log the cause server-side (honoring the `message()` never-carries-secrets contract, rule 30). ### Added - **wallet-rpc: `restore_wallet`** (WI-RPC-2a; contract in `docs/api/wallet_rpc.yaml` same PR). Restores a wallet from its seed backup + password + optional `restore_height`, mirroring `create_wallet`'s lifecycle shape. The seed format is network-governed, matching create: mainnet/stagenet take a BIP-39 mnemonic, testnet a 32-byte raw seed as hex (so a testnet wallet made by this stack is recoverable). Error mapping keeps seed material out of every message (stable `InvalidParams` strings). - **cli: native wallet-RPC client** (WI-RPC-2a; ratified Shape B, decision log 2026-04-25). `shekyl-cli` is now always a JSON-RPC client: the REPL and one-shot commands self-host `shekyl-wallet-rpc` in-process over the private UDS above; `--rpc-url` targets an external daemon. Core commands (lifecycle incl. `restore`, balance, address, status, refresh, the build→confirm→submit/discard transfer flow, transfers) run over the OpenAPI contract; the CLI is presentation-only. - **cli: non-interactive `create` / `restore` subcommands** (WI-RPC-2a) for scripting/automation. `shekyl-cli create --seed-out [--password-file | --password-stdin]` writes the one-time seed backup to an explicit `0600` file (O_EXCL, fsync) instead of a terminal; `restore --seed-file …` reads it back. This is the sanctioned way for a seed to reach a file — the interactive path refuses non-TTY output rather than leak it (see Security). ### Removed - **cli: the wallet2 FFI path** (WI-RPC-2a). `EngineContext` and the `shekyl-engine-rpc` dependency are deleted from `shekyl-cli`; a test gate greps the crate for `wallet2`/`shekyl-engine-rpc`/`EngineContext` identifier residue. Commands whose native RPC surface has not landed answer with a uniform transitional stub (migrated or deleted in WI-RPC-2b). ### Security - **consensus: output points must be canonical prime-order at admission** (`GENESIS_TX_WIRE_FORMAT.md` §2.3 output-point rule, ratified 2026-07-22; prompted by advisory GHSA-r675-h3pj-wj2f). Every output public key `O` (all tx shapes, including coinbase) and every coinbase `outPk` mask must be a canonical, prime-order (torsion-free) encoding — output keys additionally non-identity — matching the strictness the FCMP++ leaf builder already applies. Previously a torsioned/identity output point passed the inherited `crypto::check_key` (on-curve only) or, for coinbase masks under `CTTypeNull`, no point check at all, and was then *silently skipped* by curve-tree leaf construction: deterministic on every node and self-inflicted (the output was unspendable and unreceivable — not a theft or inflation vector), but a permanent hole in the every-on-chain-output-is-a-leaf invariant. Admission now rejects loudly. Single home `shekyl-ct-balance::{check_output_keys, check_commitment_masks}` (the crate owning §2.3 point canonicality), exported as `shekyl_check_output_keys` / `shekyl_check_commitment_masks`; the C++ `check_outs_valid` / `check_commitment_mask_valid` are now thin marshaling shims (rule 20 boundary advancement), the trivial-mask fingerprints (identity, `G`, coinbase `zeroCommit(amount)`) migrated to Rust in the same cut, and `prevalidate_miner_transaction` gains the previously-missing output-key gate for the miner tx. Pre-genesis freeze: no installed base ever ran the lax rule, so no fork surface exists. ### Removed - **the GF-7 order coin, the entry-seam "second event," and the `r = 1.86` verdict** (the GF-7 retirement: PR #354 §14.4 withdrawal, PR #356 deletion, PR-C retraction; `ARCHIVAL_BOND_WI4_MEASUREMENT.md` §13.1 + §14.4 banners). Gone: the `bond_first` order coin (`draw_entry_gap` now returns the bare `spread`), the whole second-event abstraction (`shekyl-standoff::plan` — `EntrySeamPlan`/`plan_entry_seam`/ `is_inverted`), the phantom `FundingSendDispatched` timeline event, the conformance fair-order grade, `FORCED_CAUSAL_ORDER`, and the sim's inversion axis. Ground (unconditional, no posture qualifier): at entry only the bond post is chain-attributable — the funding transfer is the unnamed, CT-hidden FCMP++ input, so there was never a second event to order, and the channel `r = 1.86` measured was never on the chain (the correlator held its anchors by construction — the strawman). The transport observer is the named residual, defended structurally by `shekyl-p-transport` circuit isolation (open front: the principal-side default flip, WI-4 §18.13). The spread survives only against an off-chain-anchored adversary (execution site `due_height`, `pscan/dispatch.rs`); `DEFAULT_ENTRY_GAP_WINDOW = 600` converts to an F-W3-pattern provisional sentinel; GF4b-2's funding-input count is the surviving entry-seam channel (structural, gated, not yet closed). The §14.4 founder-cover PARTITION-PASS was withdrawn in full alongside (false premise, retired §14 posture, 70%-phantom feature dictionary) — distinct from and not touching the Foundation's five `CompleteTree` reliability anchors. Golden vector re-frozen `[u64; 16]` with a consumption-identity proof (exactly one word removed per draw); new `bounded_uniform` cover-scale KAT. No wire change (the persisted record already stored the bare scalar). **Deliberately retained fail-closed as reference**: both sim arms emit named withdrawals (`GF7_FAIL_CLOSED`, the §14.4 `WITHDRAWN` status) — never a verdict — a doc that says "not citable" loses to a binary that prints a pass on demand. - **the M1 `K_COVER` gate machinery** (the implementation half of the retirement below; `ARCHIVAL_REWARD_GATE_M1.md` §13.3 enumerates the surface). Gone from production code: the `epoch_close_compute` early return and the `frozen_shard_count`/`k_cover` operands, the `k_cover.rs` sentinel module + `KCover` newtype, the `build.rs` generation + JSON keys, the `provisional-k-cover` feature and its seven enablement sites, the FFI/C++ operand pass (`shekyl_archival_epoch_close_compute` loses a parameter; the segment-freeze O(1) frozen-count substrate stays — it is freeze bookkeeping with future consumers), the G-row gate KATs + fixture, and the predicate-sites tripwire's gate invariant — its five segment-freeze substrate invariants survive as `check_segment_freeze_sites.sh`, still wired as consensus-invariants \[5/5\] (cursor accounting, boundary operator, writer/division/ counter-mutation one-site). **Deliberately retained as reference**: `shekyl-staking-sim` (the partition-adversary and GF-7 instruments), every sim finding and measurement record in the design docs, and the M1 spec itself — the record that this was considered thoroughly and rejected after the surviving-justification audit. No behavior change at any tip (the gate only ever ran as the `k_cover = 0` identity). ### Changed - **wallet design: the §15 remote-daemon structural refusal is REJECTED at review** (WI-4 §15 head banner, 2026-07-23; the GF-7 retirement arc's fourth surface). The proposed dispatch-driver refusal does not ship: the endpoint check measures socket locality, not control, and errs in both directions (tunnel false-negative — §15.3's own concession — and own-LAN/VPS false-positive); refusal removes user choice on a proxy signal; and the pricing's `r ≈ 5` basis is from the withdrawn instrument. The ratified replacement: the **asymmetric warn-only rule** (non-loopback draws a warning true regardless of daemon control; loopback draws silence; *no configuration ever draws an assurance* — the anti-autodetect rule) and the structural defense of **circuit isolation** (persona side already built — `shekyl-p-transport`, unrepresentable-otherwise), with the principal-side Tor **default flip** as the open front. §15.4's transport-isolation reopening criterion is dispositioned absorbed — the path it named was built. **The warn-only disclosure is BUILT** (`shekyl-cli::network_posture`: warns on a non-loopback endpoint that no configured proxy actually reaches, silent on loopback / unix socket / a genuinely proxied connection. `--proxy` is wired into the `--rpc-url` client (ureq) and the REPL's direct daemon client, so those are proxied and stay silent; the self-hosted server's block-scanning connection to the daemon has no proxy support in its transport, so a non-loopback daemon warns regardless of `--proxy` — the warning reflects what is actually proxied, never a global `--proxy is set` guess. A test pins that the only emissible message is a warning — never an assurance; it does not grade the operator's own proxy). **The principal-side Tor default flip is not yet built** — Tor as default and a first-class, never-ambient opt-out are settled, but the embedded-Arti-vs-external-Tor source fork and the transport-selection knob (whose axis is *who provides the transport*, not "Tor or not") remain open and are deliberately not being decided by implementation. The broadcast-③ type-level refusal (SP-T4a `BroadcastPosture`, no ThirdParty variant) is untouched — that one is structure, not detection. - **consensus design: the M1 `K_COVER` reward gate is RETIRED** (`ARCHIVAL_REWARD_GATE_M1.md` §13 — the record; spec preserved, code removal tracked in FOLLOWUPS as the implementation half). Ruling: reward withholding is legitimate only as the consequence of an action the individual controls — slash/bad-intervals is the stick a staker accepts, payment is the carrot that makes it acceptable; a collective gate that zeroes every persona's rewards on an uncontrollable condition is retired as a concept. The surviving-justification audit (M1 §13.1): anti-farming is structural in the serve-credit acceptance gate (unfrozen shards uncreditable, data possession proven, per-`(P,shard,E)` dedup), inflation is bounded by construction (`Σ rewards ≤ budget(E)` — the denominator sums the numerators), and the privacy floor inverted on the lineup argument: every relevant anonymity denominator (ambient traffic, unmarked joins, active personas) is built by paid participation, which the gate suppressed — it confiscated the `g(age)/r_market` pioneer premium that funds the organic cover bootstrap and zeroed the §14 founder personas' own market rewards. The WI-4 §13.2 cold-start measurement stands, re-dispositioned to the (now market-funded) founder-entry schedule plus a wallet-side thin-market entry disclosure (new FOLLOWUPS build item). No behavior changes at any tip: the gate has only ever run as the `k_cover = 0` gate-identity degenerate. ### Added - **wallet (engine): F-D2 aggregate drain-balance read — DS-PR-3 PR-A (`ARCHIVAL_DRAIN_SEND_FD2.md` §1 layer 1).** The core-side "how much is drainable?" accessor the GUI polls: `Engine::drain_balance_aggregate` (`engine/drain_read.rs`), a self-arc method that loads the sealed `PScanState`, anchors the canonical send-path reference via `bond_orchestrator::anchored_reference_block` (the same helper the drain path anchors through — so the displayed drainable figure is measured at the block a spend proves against, never raw tip), and returns `drain_balance`'s aggregate scalar. "Spendable" is **mature ∧ unreserved**: a funding output a live bond post / claim / drain already commits (the sealed `reserved_gindexes` union, read via `load_pending_posts_for_engine`) is netted out — the same "spendable = unreserved mature" definition the bond sweep uses. The F-D1 planner (`plan_drain` / `project_drain_operands`) applies the identical `reserved` carve, so the read total, the drain plan's affordability check, and the selectable candidate set are one consistent net set: the numbers reconcile, and the selector can never pick an output an in-flight tx already holds (a double-spend of a committed input). Non-stakers / unscanned wallets report an honest `Ok(0)` (short-circuited before anchoring). The new `DrainBalanceReadError` is two-armed by design (rule 82; DS-PR-3 locked decision) — `Unanchorable` (transient; the UI renders "syncing", never a zero) vs. `State` (non-transient fault) — so a balance read never renders a misleading zero and never conflates "still syncing" with a real fault; the anchor-error mapping fails closed with a static message on any unexpected arm, so no amount or gindex crosses the read surface. Aggregate-only by construction: no reward decomposition crosses the surface (F-D1 trust boundary). The GUI wiring (`EngineSession`/command/ render) follows in DS-PR-3 PR-B (`shekyl-gui-wallet`). - **wallet (engine): F-D2 drain orchestration + dispatch landed — DS-PR-2 (`ARCHIVAL_DRAIN_SEND_FD2.md` §DS-PR-2, the core-side dispatch half).** The `P`→principal drain **request path**, the emission-claim sibling: - **Orchestrator.** `orchestrate_drain` — a free function over an explicit `DrainCtx` (borrowed handles/records/reserved-set/destination/amounts/ `chain_tip`, **never `&Engine`**; the `orchestrate_emission_claim` shape): anchor the reference → scope + plan the persona's records → assemble membership paths → delegate to the actor's `assemble_drain` (`engine/drain_orchestrator.rs`). No new `Engine` generic or inherent multi-step method (composition discipline §4). - **Exit-fee reserve (DS-4 / T-DS-4).** `EXIT_FEE_RESERVE_ATOMIC` lands as a real constant (`shekyl-standoff::reserve`, `50_000_000` atomic = 0.05 SKL, `0 < reserve < COVER_RUNG_ATOMIC` compile-time asserted — re-grounded post-#350 as a corner-fraction bound vs the tiling cover draw) and is **enforced** in the orchestrator: a **partial** drain from a **live** persona may not spend the pool below the reserve (`DrainOrchestrationError::ReserveBreached`); a **retired** persona sweeps to zero (reserve-moot). The `retired` flag is resolved engine-side from `PScanState::pending_unbonds` (confirmed terminal `Unbond` → no future `Unbond` owed) — not `retired_records`, which is funded-gated and would deadlock the reserve gate against a drain-all sweep. - **Dispatch seam (T-DS-2).** `Engine::submit_drain` (`engine/drain_dispatch.rs`, the `claim_dispatch` sibling): resolve the wallet's own principal destination engine-side (T-DS-3, never caller-supplied), assemble through the pipeline, **seal the `PendingDrain` before any send** (persist-before-dispatch; the `reserved_gindexes` fold dedups against bond/claim/second-drain), one live drain per persona, and dispatch **only** through the audited persona-transport choke point (`BroadcastSubmitter::local` → `submit_bound`), never a bare submitter or default `DaemonClient`. A comment-stripped self-grep tripwire pins the three invariants (pipeline entry, choke-point, seal-before-send order). - **Composite wire-shape arm — final leg (T-DS-6 ∧ T-DS-7).** `regtest_e2e::e2e_drain_wire_shape_matches_a_real_transfer` builds a **real** 1-in/2-out transfer (`sign_tx`) and a **real** 1-in/2-out drain (`submit_drain`), both daemon-accepted (consensus verify), and asserts their normalized wire skeletons byte-identical while raw bytes differ (non-vacuous); `flatten_tx_shape` = the in-slice `normalize_shape` plus the public `fee` varint (transfer weight-priced, drain caller-priced). This discharges the "a real drain must exist to diff against a real transfer" condition DS-PR-1 could only stage. - **No new schema** — `dev`'s `PENDING_POST_VERSION` (v6, post- `entry_offset_blocks` removal) already carries `PendingDrain` from DS-PR-1. - **Deferred (named, WI-3 sibling slice):** drain retirement wiring (confirmation-observe / terminal-reject prune, byte-identical resubmit) — the sealed `PendingDrain` shape already serves it. Gates: fmt + clippy `-D warnings` clean. - **wallet (engine): F-D2 drain assembly landed — DS-PR-1 (`ARCHIVAL_DRAIN_SEND_FD2.md` §DS-PR-1).** The actor-side `P`→principal drain-send assembly, mirroring the emission-claim shape: `AssembleDrain`/`AssembledDrain` messages + a thin validate-and-delegate handler on the `StakeEngine` actor (`stake_engine.rs`) over the new free-function `assemble_drain_tx` (`engine/drain_assembly.rs`); a durable `PendingDrain` record with its `reserved_gindexes` fold in `engine-state::pending_post_block` (schema `PENDING_POST_VERSION` v4 → v5, snapshot updated, `v4_seal_fails_closed_under_v5_binary` fails-closed). The drain is **transfer-shaped by construction**, now compile-time-enforced: the whole `WireEncodeInput` is built by a **single shared constructor** (`sign_bridge::assemble_transfer_wire`, extracted in this slice) that `sign_bridge::sign_tx` also calls, so transfer-parity is one-constructor-two- callers rather than a property re-established per test (outputs still via `build_output`, inputs via `prepare_funding_inputs`, prefix/proving/PQC-auth via the plain `sign_transaction` calls with empty `extra_inputs` + spend-only `pqc_auths`) — realizing the T-DS-6 ∧ T-DS-7 composite wire-shape arm (full serialization byte-identical to a modal 2-out confidential transfer modulo hidden values). **Drain-all splits the payment into two nonzero principal outputs** (a sweep to self) rather than emitting a zero-value change output: the shared prover rejects `ZeroOutputAmount`, so the ratified zero-value-change mechanism was unrealizable — the split preserves the same two-nonzero-output wire shape and *strengthens* T-DS-6 (uniform across partial drain / drain-all / ordinary transfer). Partial-drain change returns to `P`'s own base spend key (T-DS-3, change-to-principal unrepresentable — no destination arg on the surface); a net-payment `< 2` drain-all is refused loudly (`DrainAssemblyError::PaymentUnsplittable`), and a zero payment is refused up front (`DrainAssemblyError::PaymentZero`) before any output construction. The drain owns a dedicated `DrainAssemblyError` taxonomy wrapped by `StakeEngineError::DrainAssembly` (a value-out failure never mis-reports as "bond assembly"). Threat-arm dispositions (T-DS-4 crossing-class maps onto an existing F-D4 §16 class — F-W10: the drain is not an identifiable transaction on-chain; DS-4 exit-reserve: partial drain is a mid-life constructor reserving `EXIT_FEE_RESERVE_ATOMIC`, post-retirement sweep moot — enforcement rides DS-PR-2 selection) recorded in the design doc round log (2026-07-20). Composition-discipline clean (`assemble_drain_tx` never names `Engine`; god-files gain only the thin handler). A whole-tx **normalized byte-diff** lands in-slice (`drain_partial_and_drain_all_are_wire_identical`: partial and drain-all serialize identically modulo hidden/committed leaves — a non-vacuous diff, raw bytes asserted to differ), guarding the one freedom the shared constructor does not pin (split-on-drain-all reshaping amounts without perturbing the skeleton). Gates: fmt + clippy `-D warnings` clean; `drain_assembly_shape::{drain_is_transfer_shaped, drain_all_still_emits_two_outputs, drain_all_net_below_two_is_refused, drain_partial_and_drain_all_are_wire_identical}` + `sign_bridge` + the `engine-state` schema snapshot green. Remaining carries (drain orchestrator/dispatch over `DrainCtx`, persona-transport self-grep, the **full transfer-vs-drain** end-to-end byte-diff) ride DS-PR-2. The engine decomposition ratchet ceiling for `stake_engine.rs` is raised `4749 → 5253` under the conf's reviewed-raise clause (~82 lines genuine in-actor handler growth per rule 36; ~422 lines inline `drain_assembly_shape` tests pending extraction); the coherent decomposition that ratchets it back down is carried by the `stake_engine.rs decomposition` item in `docs/FOLLOWUPS.md`. - **docs: F-D2 drain-send subsystem design round opened (`docs/design/ARCHIVAL_DRAIN_SEND_FD2.md`, scoping — review rounds 1–4 applied: rounds 1–2 review + Round 3 threat-model addenda + Round 4 structural-distinguishability pass, threat-model axis closed).** Scopes Gate-6 R4's last open item — the unbuilt `P`-value-out subsystem in `shekyl-gui-wallet` — against the verified substrate: gui-wallet `dev` embeds `engine-core::Engine` in-process (`EngineSession`, engine backend default-on, `start_pscan_if_staker` at every engine open, `activate_staker` → `Engine::first_stake`), the core-side planner (`plan_drain`) is fee-agnostic with no data source and no consumer, and the claim pipeline is the assembly analog. Design questions DS-1…DS-7 drafted (assembly locus, staker-mode engine adoption, destination pin, assembly-side fee carve + first-class sweep entry, round-number / random-split default with an F-D1-sibling arm, the accepted F-D4 §16.4 funding default, shape-era doc sweep) with cross-repo sub-PR slicing (rule 26 cited; pre-flight owed before code). DS-2 RATIFIED engine-first (dual-open rejected); the `shekyl-engine-rpc`-is-a-wallet2-FFI-shim crate-role correction is recorded, and DS-PR-3 is re-scoped to its remainder (the aggregate `P`-balance read; doc §2.4/DS-2/round log). Carries the FOLLOWUPS "P-drain mechanism re-walk" items (a)/(b)/(c). Rule-94 registrations: the `DS-` family + the drain-send current-front row. Closing F-D2 closes Gate-6 R4; per PR #337's scope review (merged into this round before its first review) the former GF-4/F-D1+F-D2 `K_COVER` seal seat is REMOVED (M1 §4, F-W10) — F-D2 stays a pre-genesis Gate-6 build item, not seal evidence. (Round 1's initial "GUI holds no `Engine`" substrate assumption was corrected in review round 2; the full correction is in the doc's round log.) DS-4 records the weight-uniformity cross-reference: the `P`-lane fee is `f(weight, daemon-rate)` with no capital/cover term (impl-verified against the landed WI-RPC-1 fee surface — `tx_fee_model` / `fee_query` / wallet-RPC `fees.rs`; contract pin 3d22d1e), so the only fee-adjacent distinguisher left is weight/input-count itself — GF-4b's funding-input-count territory, not the fee's. Round 3 runs the rule-26 A3 threat-model addenda (§5): five attacker objectives (T-DS-1…T-DS-5 — amount-channel, transport linkage, mis-send, crossing-class inflation, funding-input leak) graded; T-DS-1/2/3 in-scope (defended by construction), T-DS-4/5 forward-actioned into DS-PR-1 (F-D4 §16 crossing-class mapping + GF-4b funding-input discipline), T-DS-2 arms DS-PR-2 (persona-transport self-grep). The core-side impl-time pre-flight (rule 26 Part B) was run early 2026-07-19 (`ARCHIVAL_DRAIN_SEND_FD2_AUDIT.md`, Round 0): every DS-PR-1/DS-PR-2 `shekyl-core` substrate cite re-verified at pin (one B6 errata — §2.3 `"stake"` route `handlers.rs:48` → `:51`, chain terminus unaffected) and all 24 core artifacts (F-D1/F-D2 planner + weight-only fee + P-scan persistence) green; the GUI-side pre-flight (DS-PR-3/4/5) is carried to each GUI sub-PR's open. Composition discipline pinned against `ENGINE_COMPOSITION_DECOMPOSITION.md` (doc §4): DS-PR-1/DS-PR-2 mirror the claim path's already-target shape (free-function orchestrator over an explicit `DrainCtx`, actor-held signing, separate dispatch driver — never `&Engine`, no new `Engine` generic/inherent method), drain code stays in its own `drain_*` module set (not the flagged `stake_engine.rs`/`local_pending_tx.rs` god-files), and DS-PR-4's entry point is a thin façade delegate — armed as an F-D1-sibling self-grep acceptance property. Round 4 (maintainer adversarial pass) adds the structural-distinguishability axis, settled at source: **T-DS-6** (drain-*tx* output shape) — **absorbed**, a spend with `n_out < 2` is consensus-invalid (`shekyl-wire/transaction.rs:1868`) so a change-free drain-all is not a 1-out tx but 2-out like the modal confidential send (DS-4 "change-free" is economic, not a wire property); **T-DS-3 change-output arm** (change → persona `P`-space, not the transfer assembly's default principal `subaddress_index: 0` at `signing_assembly.rs:126`) and **T-DS-7** (Monero-era field re-walk — `payment_id` absent + single `ct_type` closed, `unlock_time` builder-zeroed *not* removed, `tx_extra` structural) forward-actioned to DS-PR-1; plus a GF-7 disposition line (drain is the F-W7/F-W10 phantom, not a GF-7 event) and a local-adversary scope-boundary paragraph (rule-36 jurisdiction). **Round 4 closed maintainer-accepted:** T-DS-6 and T-DS-7 fold into a single **composite wire-shape arm** (the distinguisher can live in the join, since the drain is claim-plumbed but transfer-shaped) — the drain's full wire serialization must be byte-identical to a modal 2-out confidential transfer modulo hidden/committed values, enforced by routing through the shared `sign_bridge` path and verified by diffing a real drain tx against a real transfer tx (not separate count/`tx_extra`/`unlock_time` checks); the flagged `0x07` PQC leaf-hash is confirmed appended unconditionally by that shared path (`sign_bridge.rs:283`), uniform across transfer and drain. Threat-model axis closes clean; the three impl-time arms gate DS-PR-1, not the round. No prior disposition reopened. - **rpc: WI-RPC-1 — receiving, fee, and staking-read wallet-RPC surfaces.** `shekyl-wallet-rpc` gains nine JSON-RPC methods, each a pure projection of an existing Engine surface (contract updated in `docs/api/wallet_rpc.yaml`; no tx/consensus/secret surface): *receiving* — `create_payment_request` (the one mutating method; local bookkeeping persisted via the wallet's normal crash-atomic save path, URI composed from the stored request so `rid` cannot drift), `list_payment_requests`, `make_uri`, `parse_uri`. The merchant reference is the non-zero u48 `rid` on the `shekyl:` URI — no subaddress or account type exists or was added. *Fees* — `estimate_tx_size_and_weight` and `get_default_fee_priority` over new read-only Engine helpers (`estimate_tx_size_and_weight`, `quote_fee_tiers`) that project the single `predict_weight` byte model and the build path's fee-converge fixpoint; a failed daemon snapshot is the same `-29102` the build path uses. *Staking reads* — `get_staked_balance` (three never-conflated fields: `bonded_principal_confirmed`, `bonded_principal_pending`, `rewards_received_unspent`), `get_staked_outputs`, `staking_info`, via a new authoritative `Engine::staking_read_view` aggregating reconcile evidence + sealed pending posts + funding outputs — never the `bonded_slots` hint (hint-divergence KAT pins this). Staking actions (`unstake`, `claim`) remain engine-gated and RESERVED. - **tests: the emission-claim end-to-end (PR-4c) — the E4 merge gate closes.** `e2e_emission_claim_accepted_and_applied` (`regtest_e2e.rs`) drives the whole reward loop live on a regtest chain: a market (`ShardSetCompact`) bond posted through the production assembly, serve credit injected for the first claimable epoch (onset stagger: membership starts at `join + 1`) under the `SHEKYL_SETTLEMENT_EPOCH_BLOCKS` lever, the epoch mined closed, the claim built by the production CB-3 path, accepted by the PR-4b daemon battery, and **applied** — the daemon claimed-set row lands, the loud reward vout equals `budget_atomic(E)` **byte-exactly** (the staker-inflow conservation identity over real RPC), and the production P-scan re-discovers the reward as rung-1 `EmissionReward` funding. Three pop legs bound the identity under reorgs: depth-1 idempotency, the epoch-straddling pop (the close is undone and **re-closes byte-identically from the pop-surviving credit bits**, floored above the non-pop-symmetric injection), and the deep pop through the claim's reference block (the chain re-converges and the stranded claim stays inert; the replacement-claim half is the named retire/resubmit follow-up). The shared confirmed-bond fixture (`stake_persona_to_confirmed_bond`) now serves both PR-4 e2e legs. ### Fixed - **daemon: the settlement-epoch lever armed before the genesis add.** On a fresh datadir, `Blockchain::init` added the genesis block — which runs epoch arithmetic and latches the process-wide schedule — before the `SHEKYL_SETTLEMENT_EPOCH_BLOCKS` arm gate, so every levered daemon refused to start (`ArmedTooLate`). The lever had no live consumer until the PR-4c e2e's first spawn caught it; the gate now runs at the top of `init`, before anything touches the DB. The fail-loud `ArmedTooLate` invariant in `shekyl-archival-retention` is unchanged — it did its job. ### Known gaps (named, tracked in FOLLOWUPS) - **The `first_stake` genesis posture cannot earn an emission**: it posts JoinMarket **CompleteTree** holdings, the daemon's close gather flags every complete-tree bond foundation, and E-2 excludes foundation complete-tree bonds from `market_R`/`Σwork` — so no wallet-postable bond can claim today. Everything below the entry handles market bonds first-class; the missing piece is solely the wallet entry's holdings policy (automatic shard selection — coupled to the gate-6 serving stack). The e2e claims through the composed production steps as the executable pin of what the entry must produce. - **daemon: emission-claim Phase-C submit battery (PR-4b completion).** The Rust submit engine accepts the second and last transaction kind the E4 e2e needs: `SubmitTxKind::Emission` classifies at Phase A (the opaque vin is decoded + validated once under the canonical `emission_wire` codec; loud reward sum must be non-zero; the FCMP++ proof presence couples to the fee-input count), Phase B marshals the §8.7.2 E6/E7 fact bundle (claimant bond record + one frozen as-of-E gather snapshot per claimed epoch, through a C++-owned handle inside the single snapshot lock scope), and Phase C runs the battery natively: O6, the mint-side CT balance (`Σ pseudoOuts + Σ loud vouts = Σ out_masks + fee` via the shared archival balance's debit slot), Bp+, the E2 auth-slot/claim-key binding, the F-C1c vin-less signable hash, and the `emission_vin_verify_claims`/`_backing`/`_auth` minters — the same `shekyl-archival-retention` functions the C++ oracle reaches over FFI — plus the shared fee-input FCMP++ subset and whole-tx PQC legs. Phase D re-probes the claim slot from the reparsed blob (record gone or epoch consumed → `DoubleSpendConflict`). The `DAEMON_SUBMIT_VERDICT.md` §8.7.2 E-row matrix pins every leg at source; the FOLLOWUPS "daemon Rust submit engine" item closes, and PR-4c (the E4-gate e2e) has no remaining daemon-side gap. - **wallet: SP-R0 arm #2 — the done-side retirement ledger + atomic retire-time prune (`feat/sp-r0-arm2-retire-gc`).** `RetiredPersonaRecord` rows land in `PScanState` (**`PSCAN_STATE_VERSION` 6 → 7**, schema snapshot regenerated), written by `PScanAccrual::retire_persona`: the persona's `bond_post_matches` rows and the `pending_unbonds` trigger leave in the same mutation that appends the record — one atomic seal (the bound on `bond_post_matches` growth, the most privacy-sensitive structure in the state). Funding rows are already gone by then: the **funded-gate** defers retire until the slot is drained (arm #1 pruned each spend as it was observed), with a defense-in-depth retain + `debug_assert` behind the gate. The durable prune fires only under the DQ-D **sweep-corroborated tip clamp** `min(claimed_tip, verified_frontier + reorg_depth)` (the WI-3 R2-1 reserve, consumed as corroboration; a low-claiming source defers the prune — fail-safe; the actor key-wipe keeps its frontier basis). At open, retired slots are cleaned from the live `bonded_slots` hint before derive ("stop deriving slot N"; an emptied hint reverts the wallet to a non-staker), with the derive-forward subtraction following from the monotone cursor. Guard-2: logic-discharged at the task/accrual/lifecycle test level (disclosed cfg(test)-evidence deviation — claim-window reachability); production-discharge rides the PR-4b-gated regtest lane. - **daemon: bond-post Phase-C submit battery (PR-4b, bond-post half).** The Rust submit engine's `SubmitTxKind::BondPost` arm is no longer an unimplemented refusal: `DaemonTxVerifier` runs the §8.7.1 BP battery for JoinMarket bond-posts — O6 mask checks, the bond CT balance (`Σ pseudoOuts + bond_debit = Σ out_masks + fee + bond_credit`), BP2 canonical-id recomputation, the BP5 identity-key auth pin, BP3 record claim-slot + BP4 economic battery (native `shekyl-archival-retention::verify_join_market_bond_post` — the same function the C++ oracle reaches over FFI), Bp+, FCMP++ over the `ToKey` funding subset, and PQC hybrid auth over every input. `SubmitFacts` gains the BP3 `bond_record_exists` fact (Phase-B probe keyed on the vin's claimed id; Phase-D re-probe from the reparsed blob races a record-appearing-during-verify to `DoubleSpendConflict`). The stale `verifier.rs` reachability docs ("§13 wire reshape" / "bond posts cannot clear Phase A") are corrected — no wire contradiction existed. Non-JoinMarket post kinds (`Unbond`/`HoldingsUpdate`/`Rebond`) refuse loudly under a named rule-21 reopening criterion. The PR-4a staker-harness tripwire is promoted to accepted-and-applied — staking through the production `Engine::first_stake` entry (bond mined, bond-post match + `BondPostChange` change funding re-discovered by the production P-scan, swept funding records pruned from the sealed state: the SP-R0 arm-#1 **production discharge**, DQ-F fire live). The emission submit leg remains open on its FOLLOWUPS item and still gates PR-4c. Driving the battery live surfaced and fixed **two latent production bugs** (each invisible until a bond post could actually reach FCMP++ verification and a block): - **wallet: ordinary transfers omitted the `tx_extra 0x07` PQC leaf-hash blob — every transfer output was unspendable.** The curve-tree leaf's 4th component `H(pqc_pk)` can only come from the sender (the PQC key is KEM-derived), and an output ingested without the field carries a zero leaf hash no spend can ever satisfy. The bond/emission assembly paths always appended it; `sign_bridge.rs`'s transfer assembly did not, so transfer-created outputs (e.g. persona funding) failed FCMP++ verification at first spend (`BatchVerificationFailed`). Fixed: the transfer path now appends the per-output `h_pqc` blob exactly as the bond path does. - **daemon: `Blockchain::prepare_handle_incoming_blocks` crashed on any block carrying an archival vin.** Three scan-table prefill loops did an unguarded `std::get` over every vin; the first mined bond-post threw `std::bad_variant_access` out of `generateblocks` (and would have crashed block relay on any archival-vin block). Fixed with the file's own `holds_alternative` skip idiom — archival vins carry no key image and no amount. - **wallet: the staker-activation entry — first-stake goes production (`ARCHIVAL_STAKE_ACTIVATION_PLAN.md` §5.8).** New wallet-rpc `stake` method (`Capability::Full`-gated; refusals `-29500 StakeNotReady` / `-29501 StakeInFlight` / `-29502 AlreadyStaked`; password crosses only the local transport into `Zeroizing`). SA-R1-a: `Engine:: open_full_with_first_stake_intent` spawns the StakeEngine for a non-staker under a **transient** intent (never persisted; an aborted first-stake leaves nothing durable — pin test included), with the on-demand P-scan started under intent so `stake_in` funding can scan-discover. `Engine::first_stake` composes the ratified order: idempotency/W2 split (new `persona_canonical_id` actor projection) → preflight sweep (W1-clean, production witness) → re-entrant `persist_bond_record` → sign/assemble → the `.wallet.pending` seal; no broadcast on the path (GF-7 preserved structurally — the bond dispatch driver sends at its offset). SA-R1-c: multi-input bond posts are now a consciously-logged exception (`tracing::warn!` at assemble). Retires the GF4b rule-21 dead-code half (b) on all five witness consumers. Same PR: **SP-R0 arm #3** (open-time phantom `bonded_slots` GC at the derive-time locus, phantom ⟺ no-pending ∧ absent-within-covered with the pending-record bridge as the wrongful-GC argument; logic-discharged by the SA-DQ-3 co-designed crash-fixture lane). Guard-2: arms #1/#3 production-discharge is **blocked on the PR-4b daemon submit-legs battery** (named; the regtest tripwire's promotion note carries the conversion instruction). **Post-review hardening (same branch, `ARCHIVAL_STAKE_ACTIVATION_PLAN.md` §5.9):** wallet-level idempotency + typed `WrongSlot` guards on `first_stake` (a call naming another slot can no longer mint a second first-stake); verify-then-close on the intent reopen (new `WalletFile::verify_password` — a wrong password or daemon fault refuses with the wallet still open; name-bound close + best-effort restore for the residue); dark-scan self-heal (a retry reopens when no P-scan handle is parked); rule-82 taxonomy split (new `State` / `FeeEstimate` arms — internal/daemon faults no longer misdiagnosed as the `-29500` funding refusal; fee faults map to `-29102`); the amount/gindex-free sanitizer now covers the post-persist assemble/sign path; one shared `sweep_bond_funding` body for preflight + assemble (`BondFloorZero` now refuses W1-clean); the arm-#3 GC degrades (keep-all + warn) instead of bricking open on a corrupt auxiliary seal; the GF-7 pending-release ordering contract is pinned on `reconcile_phantom_bonded_slots` + `PendingPostState`; `stake`'s password is `Zeroizing` on every path; and `docs/api/wallet_rpc.yaml` now specifies `stake` + `-29500..-29502`. - **wallet: SP-R0 arm #1 — spent-funding-record prune via key-image watch (logic-discharged).** The `P`-scan dual extractor gains arm (c): each scanned tx's FCMP++ `ToKey` key images are matched against an in-actor watch-set of the key images of `P`'s held funding outputs (derived from the vault via the single shared `KI = x·Hp(O)` definition assemble also uses; DQ-A structural containment — redacting `Debug`, no `Serialize`, zeroize-on-drop, tripwire-enforced). Derivation is offloaded off the actor's async executor (DQ5/DQ6) with per-record quarantine: a deterministically underivable record is skipped loudly, never a scan halt (assemble re-derives the same bundle and fails identically, so a quarantined record can never be swept into a bond post). A hit prunes the matching `PScanState::funding_outputs` record at ingest (extend-then-retain — a discover-then-spend within one scan step nets to no record; the in-step blind spot is closed by the handler's trailing pass), no `PSCAN_STATE_VERSION` bump (D-2: element removal is wire-identical + pre-genesis fresh-sync). Mints the **sole production `SpentRecordsDurablyPruned` constructor** (`arm1_watch_pruning_live`), discharging the GF4b-5 structural go-live gate on `sweep_funding_outputs` and its five consumers. DQ-F fire lane: `shekyl-engine-core/tests/sp_r0_arm1_fire.rs` (harness compiled `test-helpers` + `not(test)`, so Guard 1 — no `for_test()` on the path under test — is a compile-time fact) drives the production scan/watch/prune/witness path; CI asserts both prune paths fire. **Guard 2: logic-discharged only — production-firing gated on the staker-activation round** (`ARCHIVAL_BOND_SP_R0_PLAN.md` §4 build record). ### Changed - **docs: economic-model docs corrected to the live archival pay-for-service staking model.** `DESIGN_CONCEPTS.md` still described the retired passive lock-tier PoS model (`staked_amount × duration_multiplier`, duration tiers, claim/unstake, "Staking is implemented end-to-end") as if live; Components 3–4, the §2 config note, the §5 lifecycle/value-flow, §6 anti-gaming, §9 test coverage, §10 dashboard, §11 simulation params, and the §13 core-parameter table are rewritten archival-native (transfer-shaped admission, on-chain bonds, reward budget divided by capped verified serve-work, no lock/tier/claim wire, no consensus-minimum bond). `STAKER_REWARD_DISBURSEMENT.md` (the claim/lock-tier disbursement spec) gains a **SUPERSEDED — do not implement** banner pointing at the live specs. `V3_STAKER_ARCHIVAL.md` gains a dated ship-timing correction: archival is the **genesis (V3.0)** model, not a V3.x dot-release. User-facing guides are corrected too: `USER_GUIDE.md`'s `## Staking` section is rewritten archival-native and no longer documents the retired `stake ` / `claim_rewards` / `unstake` `shekyl-cli` commands (which do not exist — the CLI staking UX is roadmap; staking today is via the GUI or wallet-RPC `stake` + read methods), the RPC method reference is corrected to the live surface, and the multisig "staking security" use case is de-tiered; `STAKER_OPERATOR_GUIDE.md` and `GENESIS_TRANSPARENCY.md` drop residual lock-tier phrasing. Canonical live references are `design/REWARD_EMISSION_LEG.md` (consensus reward leg) and `V3_STAKER_ARCHIVAL.md` (mechanism); retirement history is in `design/LEGACY_CLAIM_ERA_RETIREMENT.md`. Documentation-only; no code or consensus change. - **ci: `Rust: audit, test, determinism` split into its own workflow (`ci/gh-actions/rust`).** The Rust audit/test/determinism gate moved out of `ci/gh-actions/cli` (`build.yml`) into `rust-audit-test.yml` so a flaked C/C++ `compile_by_OS` runner can be re-run without waiting for this ~35–48m job to conclude (GitHub only allows re-running failed jobs once the whole workflow run finishes). The job never depended on the OS builds — no `needs:`, no artifact download, its own `Swatinem/rust-cache` — so the job body is byte-identical and only the containing workflow (and its concurrency group) changed. **No branch-protection change is needed:** required status checks are keyed by the bare job name (`Rust: audit, test, determinism`) plus the GitHub Actions app, not by the workflow name, so the existing required-check context keeps matching this job in its new workflow. ### Removed - **multisig: `group_id` deleted — group identity is the address fingerprint (MS-5 PR-B).** `multisig_group_id` was a per-output `cn_fast_hash` over the leaf `MultisigKeyContainer` (whose keys are derived per-output from KEM shared secrets), so it produced a *different* value for every output while being named for the group; it had **no consumer** under Option E′; and its one long-term caller (`wallet2::create_pqc_multisig_group`) threw on every input. Deleted, both languages: the Rust `multisig_group_id{,_with_versions,_from_address}`, `DOMAIN_SEP_V31`, `rotating_prover_index`, `verify_fcmp_multisig_partials`, and the Option-D receive-construction unit (`construct_multisig_output_for_sender` / `validate_multisig_output_at_receive` / `MultisigOutputConstruction` / `PersistedMultisigOutput` / `GriefingTracker`); `verify_multisig`'s `expected_group_id` parameter and **check 9** (`GroupIdMismatch`, FFI error code 9 — now a **9-check** pipeline, codes 10/11 unchanged, discriminant 9 retired not renumbered); the FFI `shekyl_pqc_multisig_group_id` / `shekyl_pqc_verify_with_group_id` and their C headers; the C++ `wallet2` PQC-multisig surface (`create_pqc_multisig_group`, `get_pqc_multisig_info`, the four `m_pqc_multisig_*` fields + accessors + boost-serialize lines — the on-disk epee cache never emitted them, so this is a clean drop with no version gate); and the `fuzz_group_id` harness (+ its inventory smoke-gate row). Group identity is now the **address fingerprint** (`cSHAKE256(canonical(MultisigAddressPayload), "shekyl/multisig-address-v1")`, PR #323): per-group, independently derivable, version-binding via the canonical payload. **Chosen consequence:** with the Option-D receive constructor deleted, there is **no multisig output constructor at all** until S2/S4 writes E′'s two-component `O = ho·G + B_group + y_out·T`. The `MultisigKeyContainer` per-output-vs-long-term lifetime ambiguity is filed as its own S2-blocking slice. ### Added - **wallet/sim: GF-7 leg-(b) sealing-run harness + grader landed; first session's PASS withdrawn in-PR after review; hardened re-run graded PASS 2026-07-19; seal-input status then WITHDRAWN on scope review and the `K_COVER` seal re-dispositioned as designed (`ARCHIVAL_BOND_WI4_MEASUREMENT.md` §19.8 design / §19.9.1 withdrawal / §19.9.2 re-grade / §19.10 scope correction; `ARCHIVAL_REWARD_GATE_M1.md` §4 2026-07-19 update).** The receipt-timestamped live-driver form: a `#[cfg(test)]` + `gf7-hooks` harness in `shekyl-engine-core` (`gf7_sealing_run.rs`, `#[ignore]`d, `SHEKYLD_BIN`-gated) drives the **production** P-scan task + dispatch driver against a live `shekyld --regtest` through a new feature-gated seam (`start_pscan_sealing_run` — production config/cadence pinned, observer + positive-control-only `DispatchConfig` override injected; byte-identical control flow with the feature off), writing a receipts artifact that `shekyl-staking-sim --gf7-seal` grades with the same circular statistic and committed bound (`r = P(link)·(N−1) < 2`). A first 4 h 42 m session graded PASS (gate row `r = 1.275`), then the PR #337 review found the harness could not detect background-miner death (chain-advancement geometry unverifiable from the artifact), the grader enforced neither `R = 24 + 8` nor run-ordinal contiguity, the recording period was three untied literals across two crates, and the documented space-separated `--gf7-seal` form silently ran the default simulation — the grade is withdrawn (WI-4 §19.9.1) and the hardening landed in this same PR: fail-loud miner watch, grader `R`-floor/contiguity/`cadence_ms == TICK_MS` enforcement (artifact rows now self-describe their period), an `Arm` type binding label/seed-domain/config so a mislabeled row is unconstructible, both CLI forms with a loud missing-path error, sim verdict strings made status-neutral (they point at §19.9 instead of asserting its outcome), and a CI clippy lane compiling the `gf7-hooks` combination so the reopening-criterion re-run harness cannot rot silently. The hardened re-run then executed (both sessions disclosed, §19.9.2): session 1 graded INVALID — the §19.8.3 negative control biting on shared-host cross-wallet coupling, exactly its job — and session 2 graded **PASS** (gate row `r = 0.412`, clustered SE 0.121, ≈ 13 SEs under the committed bound 2; positive control 0.875 ≥ 0.80, negative 0.087 at chance, receipt-noise floor 413.9 ms of `T = 60 s`). **Scope review (same PR, §19.10):** the form's `K = 2` concurrently-posting-persona premise is design-foreclosed — the grounded persona-lifecycle enumeration (§19.10.1) shows `2 + E + H (+R)` wallet-timed `P`-attributed txs per persona lifetime and `stake_engine` has no rotation scheme — so **leg (b)'s seal-input status is withdrawn** (the TM-1 "does the design do this?" correction class, applied to WI-4's own arm); measurement records stand, and the hardened instrument + CI lane are retained as a dispersal-regression tripwire. `K_COVER`'s seal predicate is now stated once in M1 §4 and dispositioned as designed: partition-proof operand DISCHARGED (§14.4), value paired to the **measured** cover floor (`N ≈ 10` min / `20` with margin; thin-cover failure `r = 3.54` is the enforced fact) via the §14 founder schedule; the GF-4/F-D2 seal seat is removed (F-W10 — not an on-chain observable; stays a Gate-6 build item); the §17.9 calibration reduces to a one-page recorded derivation. Companion corrections landed with the sweep: TM-1/FIREWALL_THREATS "sequential rotation" wording (claims an unenforced code property), `V3_STAKER_ARCHIVAL.md` registration-tx line and Gate-6 refund-output line (both superseded by gate-4), and a FOLLOWUPS wallet-UX item for thin-cover disclosure at bond/claim time (warn-don't-prohibit on user acts; the enforced invariant stays consensus-side). Rule-94 registrations in the same slice: the §17.9 Gate-7 calibration and the terminal `K_COVER` seal act each gained an `IMPLEMENTATION_INDEX.md` front row. - **fcmp: F-D1 drain-amount taint-carve BUILT (`ARCHIVAL_FIREWALL_GATE6.md` §12.3) + F-D2 core-side aggregate-only surface (§12.4).** The `P`→principal value-out (drain) path lands in `shekyl-engine-core/src/engine/` as three modules mirroring the three stages of the (a)+strip encoding: `drain_orchestrator.rs` is the **single trust boundary** — the only drain-path site that names/holds `PFundingOutputRecord` (which carries `MintLineageOutput`); it projects the funding records into an aggregate spendable scalar (`DrainBalance`, the F-D2 core-side surface) and a stripped `{output_id, amount, spendable_height}` candidate vector, dropping `{lineage, epoch, height}` so no reward-sequence decomposition survives. `drain_amount.rs` consults the scalar for an **affordability check only** (`target ≤ affordable`), never as a computation input, so core code cannot *derive* a reward-shaped amount from the per-output decomposition — the vector never reaches the stage. (Steering the returned value away from any subsum is the F-D2 round-number / random-split UI default, not built here.) `drain_select.rs` does lineage-blind largest-first coin selection over the stripped vector. `plan_drain` composes the three; it takes a single scalar `target`, so a UI on this surface structurally cannot pre-fill a reward-shaped decomposition. The **M1 import-check arm** was landed on a skeleton first, a deliberately-wrong `MintLineageOutput` import was proven to fail it (exit 101) and reverted, then the real logic went in against the armed gate — the arm now runs as `fd1_arm_*` unit tests (CI-enforced) that grep each guarded module's own source and refuse if it names a forbidden type. `plan_drain` lands as a correctly-carved planner with **no data source and no consumer yet** — reachable API, not yet load-bearing. **F-D2's remaining half is NOT a pending UI default and NOT gated on RPC**: the GUI (`shekyl-gui-wallet`) is a Tauri app that links the engine crates in-process and would call `plan_drain` directly from a `#[tauri::command]` handler; `shekyl-engine-rpc` is an FFI library, not a wallet-RPC server. What is unbuilt is a whole `P`-value-out (drain-send) subsystem — a `P`-scan data source (`PScanState.funding_outputs`) feeding the planner, a drain tx assemble→sign→broadcast path, and the round-number/random-split default on top. F-D2 is not recorded as landed until that subsystem exists; Gate-6 R4 stays open on it. ### Changed - **wallet-rpc: WI-1 — wire the 2d-1 `P`-scan task into the wallet lifecycle (embedder-held handle).** The P-scan driving task (`Engine::start_pscan`) had no production caller: `start_pscan` / `start_pscan_if_staker` were `pub`, which *disarmed* the `dead_code` lint rather than satisfying it, so the "unwired" gap read as closed from a source-first check. `shekyl-wallet-rpc`'s `Tenant` now holds the `PScanHandle` — `create`/`open_wallet` auto-start it for a staker (a non-staker parks `None`), and `close_wallet` `PScanHandle::shutdown().await`s the task **before** `Arc::try_unwrap`: the task holds its own clone of the engine arc, so a live handle would fail the close "still in use" (the prior Drop-based "close-stops-task by ownership" framing was insufficient — `shutdown` is the step that makes the unwrap possible). A staker whose sealed P-scan state cannot load fails the open **closed** (`00-mission` priority 2 — privacy is not a degraded mode); a close that cannot complete re-arms the scan on restore (logged, never silent). Closure is a behavioral test (`staker_open_parks_a_pscan_handle_and_close_shuts_it_down`), not the lint a `pub` item cannot trip. Enabler: engine-core `__test_helpers::make_staker_for_test` (feature-gated) — the first `test-helpers` downstream consumer, the rule-21 public-re-export reopen the feature reserved for its first named caller. - **docs: F-D5 disposition round RAN — the structural-derivation attempt failed at source; NO GRID ships at genesis; Gate-6 R4 close condition (iv) discharged** (outcome record at Gate-6 §12.7 OUTCOME). Executed in the ratified order and the halves resolved oppositely — the inverse of X-3 (correct geometry, empty harm): the harm survives F-D1 in one sentence (the grid protects §18.12's lifetime-aggregate drain-all floor, not the subsum match F-D1 closes — a real `P`→user bridge), but the width derivation fails at three ineliminable, source-anchored population entry points: (1) no population-free lattice exists below the `Curve`'s shape constants (`scarcity_milli` divides by `r_market`, the shard's live replication count); (2) the atomic-value spacing is `budget(E)/Σwork(E)` — fees + release-scaled emission over the live cohort's work, a quotient X-3's block-height-space derivation never had in its path; (3) the strongest variant (a rate-adaptive relative grid, `K` curve-derivable work buckets, consensus-deterministic and zero-tolerance-compatible) delivers reachable-collision, not cover — epoch sets are published per claim and every `reward_P(E)` is a loud `P`-attributed mint vout, so collisions among *published sums* are population occupancy (the GF-7 nominal-vs-effective finding again). Sharpest form: the grid's cost side is derivable today (dust + the pass-4-priced contribution→reward distortion), its benefit side is derivable by nobody pre-genesis — an inequality with one computable side cannot be optimized structurally. All three charter preconditions discharged: the genesis-frozen consequence is spent knowingly (any future grid is a hard fork), pass-4 is reconciled (different target linkage; its cost half transfers intact), and the attempt's failure is derived, not defaulted. The lifetime-aggregate band registers as the first R5 S-2 ledger row; R4 remains open on F-D1 + F-D2 only. - **docs: Gate-6 R4 close condition (iv) re-worded — F-D5's "→ §14.4 economics" routing was a dangling pointer** (dated amendment at Gate-6 §12.9; disposition-round charter at §12.7). WI-4's §14.4 is the partition-adversary arm (RATIFIED PR #291, closed — no quantization or economics agenda); the pointer was a summary-layer conflation of WI-4's discharged strata/lifetime riders with its quantization GF-4-candidate routing (WI-4 §18.10 R-3), propagated through Gate-6 and the index as though it named a real round. F-D5 itself survives the substance filters (real off-chain observer, no spend graph in the path — method note 5 does not dissolve it). A fresh measurement round was refused (the grid's privacy benefit is behavioral, testnet-unproducible — method note 3). The chartered round: (1) names the genesis-frozen consequence (deferral to a post-genesis S-2 read means no grid at genesis; a hard fork if ever wanted); (2) reconciles the §10.12 pass-4 banding rejection explicitly; (3) runs the **structural-derivation attempt first** (grid width from reward-curve structure, rate-independent, X-3-shaped) — width derives ⇒ grid ships at genesis with S-2 grading the residual; width doesn't ⇒ no grid, recorded plainly. The lifetime-aggregate band registers as an R5 S-2 ledger row either way. Every propagation site carries a dated annotation (Gate-6 header/§11.7/§12/§12.7/§12.8/§12.9, index rows). The failure shape got its own record — **Gate-6 §11.8 method note 6**: the referent was minted by summarization (WI-4 never wrote it; the index row and Gate-6 prose joined two true routings into a false one that survives verification-by-grep because both halves check out separately). Distinct from method note 5's five instances; the rule: summary-cited routing targets verify against the source document's own words, and a summary that joins two source claims is a new claim. - **docs: ring-signature/decoy legacy sweep across the user-facing corpus (Gate-6 §11.7 method note 5 applied to documentation — inherited privacy prose re-walked against what FCMP++ actually emits).** Contrastive mentions ("unlike ring signatures…") were left alone; five stale claims fixed: (1) `LMDB_SCHEMA.md` — `txs_prunable` described as containing "ring signatures"; corrected to the actual `rctSigPrunable` contents (Bulletproof+ range proofs, pseudo-outs, FCMP++ proof blob). (2) `V3_ENGINE_TRAIT_BOUNDARIES.md` — `PendingTxEngine::build` "picks fresh decoys"; corrected to fresh FCMP++ membership-witness assembly. (3) `UPGRADE_POLICY.md` — HF2 planned "lattice-based ring signatures", contradicting `POST_QUANTUM_CRYPTOGRAPHY.md`'s retirement of that survey; re-worded to the lattice-only membership-proof successor. (4) `USER_GUIDE.md` + `EXECUTABLES.md` — documented `shekyl-blockchain-mark-spent-outputs`, a binary with no source and no CMake target (rows and section deleted); `--trusted-daemon` described with Monero decoy-fanout prose ("privacy-preserving request splitting"), corrected to the code's actual meaning; the surviving ancestry/depth/usage/known-spent tools annotated as having no spend-graph substrate under FCMP++ and queued for a V3.2 deletion audit (new `FOLLOWUPS.md` entry). (5) `DESIGN_CONCEPTS.md` §14 research appendix — dated re-walk note added: the "clean coins as mixing layer" hypothesis is ring-decoy-era framing, and Mechanism A's harm model ("when a coinbase output is spent") presupposes an observable FCMP++ does not emit; Mechanism B (claim batching) is the only one with a live substrate (claims are P-attributed with loud amounts). ### Removed - **archival-retention: `release_cooldown_anchor_height` deleted (rule 15 — the F-D4 mechanism deletion orphaned it).** The decision-5 PR below removed `draw_exit_gap`, the anchor helper's only production consumer; post-landing every call site was a unit test, the `lib.rs` re-export, or the staking-sim `standoff.rs` doc-comment — an armed primitive with no trigger. Its F-D6 anti-drift job was also discharged: the `20_000` doc fossil it was minted to purge is gone, and keeping a production function alive so a sim comment can cite it is speculative generality. Decided as its own rule-15 disposition (the decision-5 out-of-scope call was consumer-relative, and the deletion is what falsified it), with the keep-as-derivation-home branch declined: a future caller needing an earliest-spend height re-derives `(last_served + RELEASE_COOLDOWN_EPOCHS) × SEB` from the named consts at its own site and boundary-tests against `release_cooldown_elapsed` (reopen note at `release_cooldown.rs`; git history holds the reference implementation and boundary tests). Removed: the function, its four unit tests, the re-export; the `standoff.rs` exit-seam paragraph rewritten to cite the consensus predicate and the F-D4 deletion instead of the deleted symbol. F-D6's named-const half stands (config-generated `RELEASE_COOLDOWN_EPOCHS`, no restated integer anywhere); `release_cooldown_elapsed` — enforced consensus with two live verify consumers — is untouched. Disposition at Gate-6 §12.7. - **standoff: F-D4 exit-standoff mechanism deleted at Gate-6 §12.9 decision 5's scope** (`ARCHIVAL_EXIT_STANDOFF_FD4_WINDOW.md` §15.4 item 1 reviewer-map). The round-4 premise audit found the channel the mechanism decorrelates against unpopulated (F-W7: no on-chain principal-side re-appearance observable), and the R4 decision round re-homed the exit seam to the off-chain principal↔user crossing, whose instrument is the S-2 ledger — so the apparatus guards nothing and is audit surface (rule 15). Removed: `shekyl-standoff/src/exit.rs` wholesale (`draw_exit_gap`, `ExitGap`, `ExitGapWindow`, the `DEFAULT_EXIT_GAP_WINDOW` sentinel + compile-refusal + seal tripwire), the `provisional-exit-gap-window` and `exit-window-kat` features with every consumer acknowledgment line (`shekyl-engine-core`, `shekyl-staking-sim`, the crate's own dev-graph line), the exit arms in `conformance.rs` (`grade_exit_sample`, `certify_exit_draw`, `exit_release_population`, `ExitCertifyReport`), and the exit tests (`tests/exit_golden_vector.rs`; five arms in `conformance_grading.rs`). Out of scope, untouched: `release_cooldown_anchor_height` (F-D6 — anti-drift/slashability, predates the audit; its doc comment re-anchored on its own grounds) and `release_cooldown_elapsed` (enforced consensus). The F-D4 §15.4 tripwire and archived re-run template are unchanged; the sentinel was deleted having never shipped a value (the `K_COVER` pattern's vindication, F-D4 §15.7, now complete through the deletion arm). R4's re-formed close condition (iii) is satisfied. ### Added - **docs: Gate-6 method note 5 extended — the generative pattern named (fifth instance of one failure) and first-applied to §2.4's remaining two CryptoNote-lineage carries.** F-W10 was recognized at ratification review as the fifth instance of a single failure — phantom `T` (F-W7), the cross-persona k-collapse near-miss (F-D4 §16.3), X-3's harm model (F-W8), `σ_L`, and output-count (F-W10) — each a privacy intuition imported from a visible-spend-graph chain into FCMP++, which has none. §11.8 method note 5 now records the pattern (an inherited pin is a claim about the chain it was written for; re-walk every CryptoNote-lineage carry against what FCMP++ actually emits) and its first application: Gate-6 §2.4's other two PHASE_2B-§2.4 pin bullets, re-walked in the same sitting rather than left to surface as instances six and seven. Verdicts (recorded at §2.4): **rewards→stealth-outputs holds, re-anchored** — its real mechanism is §2.1 key/scan-boundary independence (creation-side, substrate-independent); the graph-side half of its old justification is by-construction under FCMP++. **Unbond-refund is half-phantom** — "release creates a P-attributed output at public `bond_floor` amount" named an output no observer can identify (the refund enters as ordinary hidden vouts against the public `bond_debit` source term, gate-4 §2.4; F-D4 T-2 had already graded the watch-the-refund channel structurally unrepresentable), and the "same decorrelated-drain discipline" tail retires with F-W10. §2.1's transfer-leg pin scoped to entry-leg-only ("timing/output still leak" holds only for the funding spend's existence and timing — the GF-7 seam); the §2.1 crypto-layer carry checked in passing and stands (key derivation and scan separation carry no graph presupposition). Source doc corrected in place: `PHASE_2B_STAKE_LIFECYCLE.md` §2.4 tx-leg rows (decorrelated-drain cells struck with dated notes) and the §7 Unbond-refund threat row (re-graded structurally unrepresentable with the source-term/vout distinction). No new F-W tokens minted: both verdicts are dispositions of existing pin prose under an adopted method note, not blocking findings. - **docs: Gate-6 R4 decision round (§12.9) — the F-D4 §15.5 hand-forward answered and RATIFIED: the exit seam re-homes, the joint grade dissolves by four-axis attrition (F-W10 added at ratification).** Four decisions recorded with the F-D4 §2.1/§15 audit and the F-W9 premise correction as inputs. (1) **Seam re-homed** to the principal↔user crossing (WI-4 §18.13): the timing half is phantom (F-W7/F-W9 — zero observable exits on either branch), the amount half was closed on-chain by construction and its surviving off-chain subsum match is that seam's boundary class 3, which F-D1/F-D2 close — they re-label, build and arms unchanged. After this round the on-chain `P`↔principal surface is the entry seam alone (GF-7). (2) **Joint grade dissolved by axis attrition.** Ratification rejected the draft's walk (three of §12.8's four named axes; output-count silently absent, mis-disposed as an F-D1-sibling arm — F-D1 governs the amount computation's *inputs*, output-count shapes the drain's *outputs*) and added the fourth row: **F-W10 — output-count is phantom.** Under FCMP++ the drain is not an identifiable transaction (no spend graph; spend set unenumerable; reward-output spends carry no `P`-typing on the wire), so its output count is not an observable; the Gate-6 §2.4 lump-sweep discipline is retired as a CryptoNote/ring-signature-lineage carry never re-walked across the crypto change — §2.4's own goal ("beyond what a disciplined user already avoids on ordinary transfers") is met by construction. With all four axes attrited (exit-timing phantom; amount structural-armed pass/fail; holdings a public posture fact; output-count phantom), CB-3's joint-grading obligation is discharged — nothing left to multiply — so **GF-10's width grades standalone** against its §11.5 pre-committed advantage claim and R3's close re-forms accordingly; the correlated-trigger obligation relocates to the crossing as an S-2 row. Method note 5 adopted (*a pin carried across a substrate change re-walks at carry time; "carry from X" is a re-walk trigger, not an exemption*). (3) **F-D4 §16.4 funding default ACCEPTED** as F-D2-class: self-funding via the GF-4b lineage machinery is the honest-wallet default, external growth-funding is a loud override routed through the entry-standoff draw; growth-gating cost accepted, consensus enforcement stays rejected. (4) **Harness deleted, not re-parameterized** (a rate model over counterparty arrivals fails method note 3; archived-unlanded is final) and the mechanism-deletion PR is scoped (`shekyl-standoff/src/exit.rs` wholesale; F-D6 anchor + `release_cooldown_elapsed` out of scope). Re-formed R4 close: F-D1 + F-D2 (incl. the funding default) + the deletion PR + F-D5's §14.4 disposition; S-2 opens R5 with F-W9's finite domain. Swept: Gate-6 header/round-table/§2.4/§11.8/§12.1/§12.3/§12.8/§14.4/revision history, F-D4 status banners + §15.4/§15.5/§16.4, WI-4 §18.13, FOLLOWUPS L17-wargame rider, index rows 95/97/98; the retired output-count discipline's downstream consumers updated in place (`PRINCIPAL_STAKE_LIFECYCLE.md` — DQ3 closes without a count rule, `drain()` shape constraint lifted; `F1_TA3_TA7_LIFETIME_WINDOW.md` T-A5 residual; `V3_STAKER_ARCHIVAL.md` output-layer bullet; `PHASE_2B_STAKE_LIFECYCLE.md` carry-over row). - **docs: F-W9 — F-D4 §13.3's repetition premise corrected by walking the lifecycle; `m` is a bounded count, not a growth variable; Gate-6 method note 4 adopted (*enumerate the events that compose*).** §13.3's intersection argument quantified over a growing observation count `m` that was never counted against the state machine — four review rounds sharpened the base (`q`) while the exponent was inherited as "grows." F-D4 §16 counts it against the landed FSM (`bond_post.rs`, all four `post_kind`s): **one** mandatory observable principal↔`P` crossing per bonded life (`JoinMarket`; one-per-life enforced at `:540`), **two** optional wallet-default-closeable crossing classes (`HoldingsUpdate`-add; credit-bearing `Rebond` — the common standing-only `Rebond` moves no value by Pin 2), **zero** observable exits (voluntary refund CT-hidden in-tx; terminal slash emits no transaction at all), rejoin unmodeled (self-harm class; slot-indexed persona derivation). The amount channel carries no grouping signal on any row (`bond_credit` is a pure function of the public holdings descriptor). Consequences: the geometric-collapse clause falls (at the mandatory `m = 1`, GF-7's per-event grade *is* the on-chain lifetime grade per persona per observer class, at nominal cover); the `E[m]`-is-economics objection dissolves (the mandatory count is mechanism-class — readable from the state machine); the residual's dead `σ_L` routing loses its last claim; the S-2 routing survives **narrowed and finite**. A near-miss is recorded in the same section (§16.3): cross-persona intersection (`q^k` over a principal's `k` personas) was raised and **withdrawn for want of a linking key** — funding sources FCMP++-hidden, amounts publicly determined, entry timing per-event decorrelated, network isolated; clustering over an unknown partition is not intersection over a known key (F-W7's phantom-observable shape, caught at draft). Surviving composition surfaces: the T-4 off-chain counterparty ledger (its own books are the key) and shared-trigger funding clusters (the entry standoff's existing graded case). The §16.4 proposal rides the §15.5 hand-forward to Gate-6 R4: aim the GF-4b lineage filter at the optional-crossing funding paths (honest-wallet default, not consensus; cost stated — growth gated on accumulated rewards ≥ FLOOR). Swept: F-D4 §13.3 banner + new §16, Gate-6 §12.8 hand-forward + §11.8 method note 4 + revision history, WI-4 §13.5 and FOLLOWUPS carry-4 geometric-collapse citations (conclusion unchanged, ground narrowed), index rows 97/98 (family extended F-W1…F-W9). - **F-7 (R1-F-7): first `EngineSignerKind` associated item — `SigningCeremony`.** The sealed signer-kind marker was a zero-associated-item trait; adding the first item was a Round-1 closure prerequisite (associated items must be defined by every implementor, and associated-type defaults are unstable, so a compiler would have forced it mid-MS-1). Landed the trait-surface amendment now: `EngineSignerKind::SigningCeremony`, with `SoloSigner::SigningCeremony = core::convert::Infallible`. A solo wallet runs no multisig ceremony, so its ceremony type is uninhabited — "a solo wallet ran a multisig ceremony" is unrepresentable, guarded by a compile-time `match ceremony {}` pin (same compile-forced tier as the `!Clone` archival keys). `MultisigSigner` (V3.1) will set its own FROST ceremony type (MS-5). `rust/shekyl-engine-core/src/engine/signer.rs`. - **sim + docs: GF-7 effective-cover sensitivity sweep (`shekyl-staking-sim --gf7-breakeven`, `gf7_breakeven.rs`); fifth WI-4 §13.5 conditional's kind sharpened; `r < 2` found structurally blind to cover; aspiration-vs-gate framing pinned.** Follow-through on the reclassification entry below, per review; reading corrected same day, per review. *Kind:* "the isolation-conditioning kind" got permanence right and shape wrong — isolation conditions a **channel** (fails ⇒ new channel, qualitative change); effective cover conditions **the number itself**: `1.86` was computed at nominal cover, an upper bound, and the honest characterization is **stacked marginals of opposite sign** (pessimistic oracle-union adversary panel × optimistic nominal cover; F-W4's hygiene applied on this side). Scope narrowed: thin-cover-as-regime is already L12/design-away (M1 reward gate refuses the regime); the conditional carries effective-vs-nominal **at steady state**. *Sweep (mechanism-class under method note 3 — a property of the model, no testnet, no economics input):* the new arm sweeps the gate-relevant posture over `N ∈ [2, 16]` with per-`N` validity controls, worst arm per row, 1 000 trials/point. Finding: worst-arm `r` **clears the bound at every row of the range, never trending toward the bar as cover thins** — **`r < 2` is structurally blind to cover**. The LR scorer's hit probability and the `1/N` blind baseline shrink together, so the ratio renormalizes thin-cover harm away by construction (at `N = 2` the worst arm links 72.9% and still "clears"). Consequences: **cover was never gated** — not on an optimistic assumption, not at all; the gate's ~7% margin is margin on the mechanism's fixed relative leak, never margin against cover (the structural fact that made the §12.8 misfiling possible); the pre-sweep "true `r` ≥ 1.86, unknown" framing is retracted; and **no per-event monitoring threshold was discovered** — the candidate `P(link) ≤ 0.2` is the ratio bar evaluated at nominal cover, back-derived arithmetic (under flat `r` the parity point sits at `N ≈ nominal · r/bound` *by identity*, the run's parity row landing at `P = 0.199`/`N = 9` vs `0.179`/`N = 10`), so it restates the nominal-cover assumption with no independent content, and pinning it as an absolute bar is refused as commitment-backwards-from-the-number. Per-event `P(link)` also does not bound the aspiration's quantity (the `P`↔principal binding is observed repeatedly; intersection collapses geometrically, F-D4 §13.3/F-W5): the conditional's instrument is the **S-2 fused exposure ledger** (Gate-6 R5, "build first," unbuilt). *Framing pinned at WI-4 §13.5:* the conditional register is **the aspiration's ledger** — the standing statement of what stands between "every gate passed" and "the aspiration holds" (gates are falsifiable and ours; the aspiration — only-KYC-remains — cannot be gated on); and S-2's permanent deferral while every track terminates at "S-2 is the instrument" is now itself recorded as a finding on that ledger. The "state actor has KYC anyway" scope argument is explicitly rejected as a reading of the measurement (KYC yields principal→user; GF-7 protects `P`→principal; both broken yields `P`→user — which shards, what earnings, what patterns). Surfaces: `gf7_breakeven.rs` + `--gf7-breakeven` (scale-invariance report; parity line labeled as identity), WI-4 §13.5 (fifth conditional rewritten; verdict clause (3) reworded to name S-2 as the instrument; aspiration-ledger pin), Gate-6 §12.8 + §11.8 method note 3 (constructive-half demonstration corrected to its honest output) + revision history, FOLLOWUPS carry 4, `IMPLEMENTATION_INDEX.md` row "Round N / R N". - **docs: F-D4 review round 4 — the premise audit (`ARCHIVAL_EXIT_STANDOFF_FD4_WINDOW.md` §2.1 + §15; F-W7/F-W8 minted; disposition RATIFIED 2026-07-16: deletion-with-tripwire).** The correlated observable `T` ("principal-side re-appearance") that every exit-seam channel quantifies over was never named by rounds 0–3; the §2.1 population audit finds it empty within the seam: **T-2 structural** (the refund is the `bond_debit` source term of the `Unbond` tx itself — hidden outputs in the posting transaction, CT-enforced to the public floor, no later release to watch; `bond_connect.rs`), **T-1 policy + economics** (rotation dead by S-5 scope + T-A1 portfolio identity + in-place `HoldingsUpdate`; population ≈ 0 by design), **T-3 conditioning** (§10.9 default-on isolation cannot be assumed and harvested), **T-4 re-homed** (the off-chain counterparty crossing is Gate-6 §18.13's principal↔user seam — widen-not-close, S-2 ledger). F-W8 retracts X-3's harm model (cohort membership is `last_served_epoch`, already public; the partition only fed assignment, which is a function of `T`). §5–§14 outputs stamped conditional-then-closed; disposition **RATIFIED: deletion-with-tripwire** (§15.4 — reopen criteria named: rotation in scope / refund moved out of the posting tx / new public principal-keyed term / isolation weakened; re-run shape specified; the mechanism-deletion PR's reviewer-map is §15.4 item 1, with the `--exit-standoff` harness sequenced behind the §15.5 answer — a harness grading a phantom channel is a trigger with no gate, but deleting-then-resurrecting would fight a re-homing PR; landing disposition: the harness never landed on `dev`, preserved at `archive/feat/fd4-exit-sweep-2026-07-16`). §15.5 — whether GF-4's exit seam re-homes to §18.13 wholesale, and what the four-axis joint grade becomes without its on-chain timing axis — is **handed forward to Gate-6 R4 with the audit attached** (received at Gate-6 §12.8). Ratification sweeps landed: Gate-6 §12.5/§12.6/§12.8 + revision history, the `RELEASE_CHECKLIST.md` `DEFAULT_EXIT_GAP_WINDOW` seal entry **removed**, FOLLOWUPS swan-2/W8 wargame closed-as-posed (reopen rides the §15.4 tripwire), index rows 95–98. Methodological finding adopted (§15.6 + Gate-6 §11.8 method note 2): **observer-as-code, pre-registered before any row runs** — the harness's declared variable is what exposed the phantom four prose rounds missed; *a channel's meaning is its observable*. The `K_COVER` sentinel pattern is vindicated (§15.7): the unsealable value already refused to compile, so the premise collapse required no un-shipping. F-W7/F-W8 registered at birth per rule 94 (index row 97, family extended F-W1…F-W8). - **docs: mechanism-vs-economics measurement filter adopted (F-D4 §15.8 + Gate-6 §11.8 method note 3 + `RELEASE_CHECKLIST.md` seal-queueing rule).** Second finding extracted at the F-D4 round-4 ratification, true *before* the phantom-`T` finding: two of the three Phase 7.7 seal inputs were unmeasurable on a testnet in principle — `σ_L` is the latency to an event that doesn't occur (empty, not noisy), `N_x` is a market-panic cohort size (no real value at stake ⇒ no panic; a scripted mass-exit measures the script), `ρ_x` is profit-taking cadence (economic behavior). The read would have returned the test plan with the authority of "measured," and a genesis constant would have been sealed on it. Standing posture: before queuing any constant for a stressnet seal, classify the measurement — **mechanism** (partition firing, draw bias, determinism KATs, timing under load; a testnet reproduces these because the software doesn't know the money is fake) vs. **economics** (exit rate, churn, panic cohorts, mobility; functions of real value at risk, which a testnet cannot produce). Economics ⇒ the stressnet is the wrong instrument; the honest exits are derive-structurally / design-the-constant-away / don't-ship-the-genesis-frozen-value. Filter run across the PF-9 / Phase 7.7 queue at adoption: `K_COVER` is the only queued seal and passes (the §14.4 partition run is mechanism); the remaining Phase 7.7 entries are mechanism exercises; one caveat placed at Gate-6 §12.8 — GF-7's effective-vs-nominal-cover residual is testnet-dischargeable only on its observer machinery, never on the cover level (post-isolation network-event rate is economics). - **docs: GF-7 effective-cover reclassified as the fifth WI-4 §13.5 standing conditional; method-note-3 sweep extended across Gate-6's residuals.** The §12.8 caveat's "stays open on the level" was the wrong filing — "open" implies it closes when someone does the work, and the cover level is unfalsifiable before real value is at risk: a standing conditional of the isolation-conditioning kind (carried as a stated assumption with a post-genesis monitoring plan, never a task). Verified at source that the `1.86` was computed on **nominal** cover (`gf7_timeline.rs` seeds `N = TARGET_ANON_SET = 10` at full honest strength; Gate-6 §11 (iv) already names the measured set an upper bound), so the conditional is load-bearing: effective cover below nominal moves realized per-event exposure `P(link)` up (the later sweep entry above shows `r` itself is cover-blind, so the harm never registers gate-side), and per WI-4's no-cross-subsidy pin it cannot be paid out of the gate's margin. WI-4 §13.5 ledger extended four → five conditionals; the seal-time verdict line now carries three permanent clauses (isolation, GF-4 promotion, effective cover). FOLLOWUPS funding-seam carry 4 split (observer machinery = testnet obligation, retained; cover level = register). Sweep across Gate-6's remaining named residuals: one proviso (GF-6 dummy/fragmentation ratio — the ambient large-v3 distribution must be observed from the live Tor network, never synthesized by the replay, or the ratio is tuned against the load generator); leg-(b), the §14.4 partition run, G-10.D, and the heavy-path L-curve classify as mechanism; L12 cold-start passes because its §14 disposition is already the design-away exit; isolation conditioning was already correctly filed. - **docs: the F-D4 §8 exit-seam sweep — X-1/X-2/X-3 arms built and run; findings recorded; the instrument itself **never landed on `dev`** (`ARCHIVAL_EXIT_STANDOFF_FD4_WINDOW.md` §8/§9 step 4; new §14). Per the round-4 landing disposition the harness is preserved at `archive/feat/fd4-exit-sweep-2026-07-16` and its fate rides the §15.5 hand-forward (re-parameterized on the crossing observer if Gate-6 R4 re-homes the seam; otherwise never resurrected).** The instrument as built and run: `shekyl-staking-sim --exit-standoff` (`src/exit_standoff.rs`), built to the bar before any row ran: X-1 steady-state arm over the §5.1 planning-box corners in the WI-4 N-sweep form (`r < 2` at every swept `N`, worst row reported); X-2 crisis-cohort arm (`N_x ∈ {3, 5, 8}`, one/two-anchor per the §3 quantization lemma, swept `σ_L`) whose two-anchor rows certify the X-3 coverage boundary (measured cross-anchor overlap = predicted `(W − SEB)/W` at all three windows — mixes-at-all, never "merged"); the §8.3 shared-trigger negative control (caught via clustering-detection; the assignment observer is provably blind to the skip, which is why the control keys on clustering); the §8.4 thin-regime bias arm (unbiased); and the §5.3 lever-vs-queue costing on the shared capital-idle axis. The §5.4 frozen rule is encoded once (`frozen_rule_window`, planning-box corners pinned as its KAT) and `EXIT_TARGET_ANON_SET` is minted at this consumer (F-W5 §13.4 carrier). Observer pre-registered: support-gated exact Bayes under `L ~ U[0, σ_L]`. Two structural findings recorded at §14, stated not buried: delivered X-1 cover is **latency-gated** (`ρ_x · σ_L`, W-independent — the window buys de-quantization and the X-3 merge; confusability is bought by `σ_L`, priced by the exact inversion `required_sigma_l`), and marginal confusability is not `r < 2` (the X-2 trough needs `σ_L` on the `W/2` scale; 71/72 swept rows reported failing per GF7 §5.1 — the pre-committed §5.3 costing surface, never a bar move). On the archived branch the sim is a named approved consumer of `exit-window-kat` (comments pinned in both `Cargo.toml`s; the binary sits outside every production graph). The seal this instrument was to serve is removed by the round-4 entry above; the numbers survive as the §15.4 tripwire's re-run calibration template. - **docs: F-W5 resolved — the exit-seam `N_t`, derived a-priori (`ARCHIVAL_EXIT_STANDOFF_FD4_WINDOW.md` §13).** Result: **`N_t(exit) = 10`** — numerically equal to the entry posture anchor by derivation, not inheritance. The committed bar is WI-4's N-invariant ratio bound (`r < 2` per regime row); `N_t` is only the posture anchor the window is sized to deliver. Each seam-variant structural fact was examined for whether it moves the anchor: cover class moves `ρ_x` (already spent on `W`), one-sidedness moves `W` and a graded §8 arm, the trough cohort is its own X-2 regime row (WI-4 regime splitting — never averaged into steady state); the anchor's own determinants (protected secret, bar, advantage semantics) are seam-invariant. The one real asymmetry — the persona↔principal binding observed repeatedly, keyed to the public `P_id` (recurring drops + terminal drain + entry∩exit intersection) — provably cannot be priced into a per-event anchor (intersection collapses geometrically; inflating `N_t` re-imports F-W3 via the pre-testnet `E[m]`) and routes to the `σ_L` re-appearance discipline and the S-2 exposure ledger as a named residual with reversion criteria. The §5.4 rule's X-1 term reads `9/ρ_x`; the §8.1 arm grades at `10` and carries the WI-4 N-sweep form; `EXIT_TARGET_ANON_SET` is minted at the sweep-harness PR (numerically equal to the sim's entry constant, independently movable). The `N_t = 9 ⇒ 20_000 exact` re-cut trap did not bind — `9` has no model support in the derivation. Gate-6 §12.5/§12.6/§12.8, index F-W/Round-N rows, and the release-checklist seal entry swept. - **standoff: `draw_exit_gap` against the F-D4 provisional sentinel (Gate-6 §12.5 F-D3; `ARCHIVAL_EXIT_STANDOFF_FD4_WINDOW.md` §9 step 2).** New `shekyl-standoff/src/exit.rs`: the one-sided exit-seam draw through the shared unbiased `bounded_uniform` — no order coin, no inversion (forced by the cooldown) — with typed `ExitGap` and an `ExitGapWindow` capability newtype whose `wallet_default()` reads `DEFAULT_EXIT_GAP_WINDOW`, a `K_COVER`-pattern provisional sentinel (`0`; provisional ⇔ 0 const-asserted; `compile_error!` unless the consumer enables the grep-able `provisional-exit-gap-window` acknowledgment feature, deleted at the Phase 7.7 seal; `kat_inject()` behind the permanent dev-only `exit-window-kat` feature — named per the `kat_forge` precedent so the PF-6a tripwire's `for_kat` grep stays scoped to `KCover`). Golden vector at a synthetic KAT window (10 007) with a **seal tripwire** that fails at seal time to force the re-freeze; the §12.5 pinned shared-trigger negative control landed (`conformance.rs::exit_release_population`, clustering-detection) beside a two-property exit grade (uniformity + serial independence — no order axis). Consumer acks: `shekyl-engine-core` and `shekyl-staking-sim` enable the arming feature explicitly. - **archival: F-D6 cooldown-anchor derivation — `release_cooldown_anchor_height` (Gate-6 §12.7, DONE).** The exit-standoff scheduling anchor `H_cd = (last_served + RELEASE_COOLDOWN_EPOCHS) × SETTLEMENT_EPOCH_BLOCKS` now derives from named consts in one home (`shekyl-archival-retention` `release_cooldown.rs`), mirroring `release_cooldown_elapsed` exactly (vacuous never-served arm; fail-closed overflow arm) with a boundary test proving the two agree at `H_cd` and disagree one block before it. The last `20_000` doc-comment fossil (staking-sim `standoff.rs` module docs — the drift vector F-D6 was opened against) is purged, pointing at the derivation instead. - **docs: F-D4 review round 3 — F-W6: X-3 anchor-merge bound folded into the frozen rule (`ARCHIVAL_EXIT_STANDOFF_FD4_WINDOW.md` §5.2a/§12).** Round 2 had parked the `2 × SEB` structural argument as a tiebreak narrative; round 3 made it precise — it is a **derived bound**, from the §3 quantization lemma with no rate input: adjacent anchors sit `SEB` apart, cohort windows overlap iff `W > SEB` (measure-zero at equality), so at `1 × SEB` the adversary partitions a straddling crisis cohort by exit height — invisible to X-1, which grades background cover, not cohort integrity. Minted X-3 (`W ≥ 2 × SETTLEMENT_EPOCH_BLOCKS`); the §5.4 rule is now `W := smallest SEB multiple ≥ max((N_t − 1)/ρ_x, 2 × SEB)`. Carried with it: the coverage boundary (2 × SEB *opens* the split — 50 % mixed fraction at the cliff, continuous improvement, no second cliff; the §8 two-anchor arm grades the actual fraction; nothing may claim "merged") and the cost stated beside the benefit (the §5.3 queue predicate gains a hard `20_000` LHS floor in every measured world, so the F-W2 lever-vs-queue costing no longer evaporates on a dense `ρ_x` read). A preference-shaped `max()` floor was refused; the derived bound is what earned rule entry. Gate-6 §12.6, `RELEASE_CHECKLIST.md` seal entry, index X-/F-W/Round-N rows, and FOLLOWUPS swan-2/W8 swept to the two-term form. - **docs: F-D4 review round 2 — F-W3/F-W4/F-W5: the window value is not derivable pre-measurement; sentinel + frozen decision rule (`ARCHIVAL_EXIT_STANDOFF_FD4_WINDOW.md` §5.4/§11).** F-W3 (blocking): the X-1 bound spans a ~19× planning box over the doc's own ranges (`N_P ∈ 79–154`, `c ∈ [0.02, 0.2]`; corners `2_922`–`56_962`), so both draft candidates (`2 × SEB`, `3 × SEB`) were points picked from a box, not derivations — `ρ_x` is a pre-testnet unknown and the value cannot precede the rate. Resolution mirrors the ratified `K_COVER` pattern (M1 §9.3): `DEFAULT_EXIT_GAP_WINDOW` ships as a provisional sentinel `0` with compile-time refusal absent explicit acknowledgment, sealed by the Phase 7.7 stressnet read of `(ρ_x, N_x, σ_L)` (`RELEASE_CHECKLIST.md` entry added beside `K_COVER`/PF-9). A wallet default sizing an anonymity set is soft-frozen (post-ship change = §16.1 partition trap as a flag day), hence consensus-constant treatment. Frozen now: the decision rule (smallest `SEB` multiple ≥ the X-1 bound), the F-W4 conservatism commitment (10th percentile of the joint `(N_P, c)` read — no stacked marginal worst cases), and the F-W5 pin (exit-seam `N_t` re-derived a-priori before the seal; the entry-inherited `10` is not the exit constant). `2 × SEB`'s structural argument survives independent of X-1. **`draw_exit_gap` unblocked against the sentinel** — compiles for testnet under explicit arming, refuses to ship unsealed. Gate-6 §12.6, index F-W/X-/Round-N rows, and the FOLLOWUPS swan-2/W8 entry swept to match. - **docs: F-D4 rate-model review round 1 — F-W1/F-W2 resolved by amendment (`ARCHIVAL_EXIT_STANDOFF_FD4_WINDOW.md` §10).** F-W1 (blocking): the draft candidate `2 × SEB = 20_000` failed its own X-1 planning-instance bound (`22_500`, itself the conservative end — the pessimistic `c = 0.05` pushes the bound *up*) while §5.4 said "clears" — the GF7 §5.1 bar-moving violation; re-derived to `3 × SEB = 30_000` (33 % margin, clears down to `c ≥ 0.0375`), failure recorded in-place, not erased. F-W2 (load-bearing): `σ_L` is wallet-drawn — a design lever, not a fact of nature — so the §5.3 predicate is a joint constraint over `(W, σ_L)`; the wallet-discipline lever (`σ_L ≥ W/N_x = 6_000` blocks, cost borne by exiters only) is now costed against the release-queue (domain widens for everyone, always) on the shared capital-idle axis, neither pre-selected. Lemma, X-1 shape, X-2 form, and the one-window pin survived review. `N_t` citation disambiguated to `shekyl-staking-sim/src/standoff.rs:119`. F-W family registered. (The `3 × SEB` re-derivation was itself superseded same-day by round 2's F-W3 sentinel reshape — see the entry above.) - **MSW-6: relax the tx-wide `scheme_id` agreement (staking unblock).** A v3 transaction may now carry per-input PQC schemes from `{1,2}` freely — e.g. scheme-2 (multisig) funding sharing a tx with a scheme-1 (single) bond vin. The former tx-wide agreement (every input matching `pqc_auths[0].scheme_id`) is dropped in lockstep across the C++ verify batteries (`tx_pqc_verify.{h,cpp}`, `blockchain.cpp`) and the Rust submit verifier (`shekyl-daemon-rpc/.../verifier.rs`); per-input scheme validity, key-blob length, and signature remain enforced. The agreement's stated scheme-downgrade purpose was vacuous (self-referential; per-output binding is the `h_pqc` leaf hash), and its actual effect — foreclosing a solo/multisig cross-model linkage — belongs in the wallet on **no externality** (one-time keys, FCMP++ proof over the whole tree — no other anonymity set shrinks) plus Shekyl's own opt-in `scheme_id=2` self-marking precedent, **not** TM-1; the compensating coin-selection invariant (never cross key models, with a test) + disclosure line are a **blocking E′/MS-5 ship gate**, not consensus. Cross-seam KAT: `tests/unit_tests/fcmp.cpp::msw6_mixed_scheme_transaction_verifies`. See `PQC_MULTISIG.md` §16.3. - **MSW-1/2/3/4/5/8: V3.1 multisig wire-format bundle @ MAX=5.** One validation surface; the cross-language KAT is the gate whose absence produced F-1. - **MSW-1 (bound family + MAX=5).** `MAX_MULTISIG_PARTICIPANTS` 7→5 (MSW-G, 2f+1 at f=2). The canonical wire length now lives on the type that owns the bytes (`HybridPublicKey`/`HybridSignature::CANONICAL_LEN`); `SINGLE_KEY_CANONICAL_LEN` / `SINGLE_SIG_CANONICAL_LEN` alias it. The DoS ceilings (`PQC_MAX_PUBLIC_KEY_BLOB = 16384`, `PQC_MAX_SIGNATURE_BLOB = 32768`) are **decoupled** from MAX and guarded by compile-time `const _` asserts against the largest legal container — so a ceiling *below* a legal max-threshold blob is now a build error. The F-1 fossil `2 + N·LEN` (which omitted the 3-byte container header and the 32-byte-per-participant spend-auth keys) is deleted from `cryptonote_config.h`, `tx_pqc_verify.cpp`, and `shekyl-wire`. The same stale `n > 7` cap survived in `multisig_receiving.rs` and `shekyl-address` (×3): both are deduped to `MAX_MULTISIG_PARTICIPANTS`, and the crypto-pq↔address caps are pinned equal at compile time. **F-1 catcher:** the new `shekyl_pqc_canonical_lens()` FFI accessor + `fcmp.cpp::msw1_pqc_constants_match_rust` prove the C++ config constants equal the Rust ones across the FFI — the one mechanism that would have caught F-1, where each side was internally consistent and disagreed only across the boundary. - **MSW-2 (disjointness KAT).** The scheme-1 leaf length (1996) is disjoint from every scheme-2 container length, `expected_blob_len` is injective (so a length-relabelled blob is rejected on the length cross-check), and `m_required ≥ 1` is the secondary separator. Leaf encoding untouched. - **MSW-3 (misattribution / MS-8 record).** The misattributed `"output committed="` string was already removed by MSW-6; MS-8 stays retired (group_id verify no-op → folded into MSW-2). - **MSW-4 (group_id version plumbing).** `multisig_group_id_from_address` sources `group_version` / `spend_auth_version` from the group's `MultisigAddressPayload` rather than crate constants, so a version change produces a distinct group_id (§5.3); the bare `multisig_group_id` wrapper is now documented tests/fuzz-only. - **MSW-5 (reserved-version carrier).** The address payload transports a reserved `spend_auth_version = 0x02` (Option E′ marker) opaquely — it round-trips unchanged for forward-compatible readers (§8.2). - **MSW-8 (address shrink).** The vestigial `MultisigAddressPayload.hybrid_sign_pubkeys` (Solution C fossil, zero consumers — leaf sign keys are derived per-output from KEM shared secrets) is deleted; `PER_PARTICIPANT_LEN` collapses 3200→1216 (~2.6× smaller address). Clean removal at payload version `0x01` — no version bump. `PQC_TEST_VECTOR_002_MULTISIG.json` regenerated off the F-1 fossil model. See `docs/design/V3_1_MULTISIG_RUST_ENGINE.md` (wire-work table) and `PQC_MULTISIG.md` §5.3 / §6.2. - **docs: F-D4 a-priori exit-window derivation (`ARCHIVAL_EXIT_STANDOFF_FD4_WINDOW.md`, Gate-6 §12.6).** Committed before any `draw_exit_gap` code or sweep (GF7_HOOKS §5.1 ordering): anchor `(last_served + RELEASE_COOLDOWN_EPOCHS) × SEB` from named consts (F-D6); inherited `r < 2` threshold, regime-split (steady-state X-1 vs L17 crisis-cohort X-2); the anchor-quantization lemma (the cooldown delays and quantizes a synchronized cohort — no smear; the a-priori answer to swan-2/W8's mechanism question, wargame-confirmed arm pre-registered); the §5.3 queue predicate making "is a release queue needed" decidable; `DEFAULT_EXIT_GAP_WINDOW` (the draft's committed candidate was retracted by review rounds 1–2 — sealed by measurement per the F-W3 entry above). Exit window does not borrow the entry 600 — the derivation shows SEB-scale. Code gated on the rate-model adversarial review. - **docs: F-D3/F-D4 activation fired — fossil sweep (Gate-6 §12.5).** The rule-21 activation criterion ("a verify path reads the release cooldown as a spendability gate") is met at source: `bond_post.rs:369` (`HoldingsUpdate`-drop) / `:627` (`Unbond`), landed with the bond FSM (PR #303/#307). Swept every row that still claimed the gap: Gate-6 header/§6 R4 cell/§12 statuses, `IMPLEMENTATION_INDEX.md` Round-N row, `PHASE_2B_FSM_RETOOL.md` P2B-7 residuals row (said "open" against its own exit checklist's "closed" — Pin-4/Pin-5 closed at PR #303, sim reconciliation DONE at §L18), and the FOLLOWUPS `HoldingsUpdate` "Still open" clause. New §12.5 pin: `draw_exit_gap` per-event independence gets its own shared-trigger negative control. Build order: F-D4 a-priori window derivation before any code or sweep. - **ci: F-6 `--features multisig` lane + P0-n doc gate (follow-on to #308).** Lands the F-6 lane (`.github/workflows/multisig-feature.yml`) and the P0-n doc-literal gate (`scripts/ci/check_multisig_doc_literals.sh`), split across two workflows (`multisig-feature.yml`, `multisig-doc-literals.yml`) so a docs-only change runs only the cheap ripgrep gate, not the full `cargo check/clippy/test`. Clippy posture matches `build.yml` (toolchain 1.94.0; `-D warnings` on clippy argv only). The gate catches the `pqc_auth` fee fossil in bare / comma / approximation forms (not only tilde-prefixed) and any `k-of-n` config over `MAX_MULTISIG_PARTICIPANTS = 5` (not only the two 7-literals), and now scans the design carrier / tracking index / FOLLOWUPS instead of excluding them wholesale — genuinely-historical lines opt out with an inline `doc-literal-gate-allow` marker. - **fix: stale `pqc_auth` size in `POST_QUANTUM_CRYPTOGRAPHY.md`.** The hardened P0-n gate surfaced a comma-grouped fossil (`5,385 bytes`) that contradicted the same doc's oracle figure — corrected to `5,389` (`pqc_auth_weight()`). - **docs: PR #308 Copilot round 2 — E′ consistency (principles, roles, ops, VERSIONING).** Principle 5 no longer claims FROST SAL is out of V3.1 scope (E′ *is* 15.4a). Roles / MULTISIG_OPERATIONS banner Option D prover playbooks as historical. §15.1 / §7.2 / §7.4 `spend_auth_version` = `0x02`. USER_GUIDE ~5.4 KB. VERSIONING / §15.4a cite `derive_output_secrets` for `y ≠ 0`, not `output.rs:304`. - **docs: defer F-6 / P0-n CI to follow-on PR (PR #308).** Remove `ci/multisig-feature` workflow + `check_multisig_doc_literals.sh` from this docs carrier so #308 can land alone. Fossil *sweeps* remain here; gate script + workflow ship in the follow-on CI PR. Carrier / INDEX status: R1-F-6 open again until that PR greens. - **ci: align F-6 multisig lane with build.yml clippy posture (PR #308).** Dropped job-level `RUSTFLAGS=-D warnings` (it denied rustc `deprecated` in path-dep `helioselene`, which main CI only warns on). Pin toolchain to `1.94.0` (not `@stable`); keep `-- -D warnings` on the clippy argv only; add `--keep-going`. *(CI files moved to follow-on PR — see deferral bullet above.)* - **docs: PR #308 Copilot review — decide/enforce labels + F-11 consistency.** MAX=5 is the ratified MSW-G target; docs no longer imply the live constant (still `= 7` until MSW-1). Changelog drops the superseded same-day MSW-G=8 bullet (trail stays in the carrier history). Finding range unified to R1-F-1…F-11. §16.7 feature-flag block labeled planned sketch (not a copy-pasteable `Cargo.toml`). - **ci/docs: P0-n doc gate + F-6 `--features multisig` lane (PR #308).** `scripts/ci/check_multisig_doc_literals.sh` (cap / fee-oracle fossils) and `.github/workflows/multisig-feature.yml` (`cargo check` / `clippy -D warnings` / `test` on engine-core + engine-rpc + ffi with `--features multisig`). Operator docs swept off `5-of-7` / `5385` fossils. Pause before MSW-6 — first enable may clippy-fail on `frost_sal` (Option A debt); that is the signal. *(CI files moved to follow-on PR — fossil sweeps stay in #308.)* - **docs: D-7…D-10 — VERSIONING + MS-4/MS-5 under E′ (PR #308).** VERSIONING: two-component gate discharged (`output.rs:304`); remaining blockers = threshold-share `y_group` + wire `0x02`; protocol-3 wire is `scheme_id=2` M-of-N list (not "FROST-style"); 15.4b gets IR 8214C → MPTC ~2027 → later-process scale. MS-4/MS-5 restated: receive = local KI via `b` (no I7); spend = FROST-on-`y` + nonce types (not prover handoff). Round 1 closure checklist + §10.3.1 premise date. - **docs: Pin #1 reversion + VERSIONING/§10.3.1 for E′ (PR #308).** Named reversion: comment-only production-source amendments allowed when recording a semantic this doc-only PR pins (`bond_wire.rs` MSW-7 pseudonym-uniformity — was a silent Pin #1 break). `VERSIONING.md`: 15.4a blocker open (`y≠0` landed); `0x02` = E′ product path. `V3_ENGINE_TRAIT_BOUNDARIES` §10.3.1 status: MS-8 retired, E′, MSW-*. Sequencing note: MSW-G cold before MSW-1 code. - **docs: Phase 0 sweep D-1…D-6 + P0-n grep-gate (PR #308).** Fix normative `n_total > 7` → `MAX_MULTISIG_PARTICIPANTS` (D-1). Correct ROLLOUT vs-N-solos ratios (0.57× / 0.48×), cap fossils (`=7` / `5-of-7` / ~37 KB → 5 / 5-of-5 / ~27 KB), `pqc_auth` 5385→5389 from `pqc_auth_weight()`, Last-updated date (D-2…D-5). ANALYSIS: historical banner, E′ row in §2.1, §6/key-escrow superseded, §7.6 E′ tweak Phase 6 target; rotation naming collision (§11.1 ≠ §15.2). P0-n: doc grep-gate against bare-cap / fee-oracle drift. - **docs: MSW-8 — delete vestigial address hybrid_sign_pubkeys (PR #308).** Confirmed zero consumers: `multisig_receiving.rs` derives leaf sign keys from KEM; address field is construct/parse-only (Solution C fossil). ~2.6× address shrink is MSW-8, not E′ (attribution correction). N=2 ≈ QR-able. §15.3 registry demoted off critical path. Defect class named: shape-from-prose without a read site. E′ three closes complete (dealer-only, y-tweak→Phase 6, MSW-8). - **docs: Option E′ — two-component trust split is the product path (PR #308).** `O = ho·G + B + y·T`: `b` group-plaintext (view+link / local KI); `y` FROST M-of-N with `y_out = y_group + y_kem` (E′, not fixed-`y` E). Dealer-mode, no DKG, no `export_multisig_info`. Deletes mandatory-prover Option D scaffold (~prover/heartbeat/ counter_proof/I7). `spend_auth_version=0x02`; `0x01` never issued. `frost-sal-v4` = E′ gate (keep primitives; delete Option A wrapper). Does not shrink auth blobs or fix anonymity partition. Address size win deferred to MSW-8. Carrier §0.5; `PQC_MULTISIG` §15.4a / §16.7 aligned. - **docs: MSW-6 + MSW-G=5 overhaul; MSW-7 retract (PR #308).** Track A staking unblock is **MSW-6** — relax tx-wide `scheme_id` agreement (per-input ∈ `{1,2}`); archival core never sees funding inputs (`verify_*_bond_post(vin, record_state)`). **MSW-G = 5** (2f+1 at f=2; withdraws same-day MAX=8). MSW-1 narrowed to `SINGLE_KEY_CANONICAL_LEN` single-source + DoS ceiling. **MSW-7 retracted** — `bond_spend_pk` stays 1996 for pseudonym uniformity (`bond_wire.rs` comment amended). Pin #4 named reversion for MSW-6 only. Sequencing: Phase 0 docs → MSW-6 → MSW-1…5 → F-6 → Option E′. Phase 0: `V3_ROLLOUT` whole-tx weights; `PQC_MULTISIG` §5 derivation, §11.1 rotation≠hostile-sender, §14.2 stream attribution, §15.4a two-component O open, §16.3/16.4/16.7 retire DiD/wallet2. *(Same-day provisional MSW-G=8 withdrawn into this entry — decision trail lives in `V3_1_MULTISIG_RUST_ENGINE.md` history, not as a separate changelog bullet.)* - **docs: corrected picture — design done; Track A is the urgent half (PR #308).** Banner in `V3_1_MULTISIG_RUST_ENGINE.md`: April decisions stand; gap is unbuilt scaffold + consensus surfaces the feature gate never covered. Three buckets, differential rigor, reviewer scorecard. §11.1 / ANALYSIS grinding lead → availability ~`k·n`, not scare-`n^k`. - **docs: R1-F-3 → DELETE Option A FROST fossil (PR #308).** Not the Apr 15 rotating-vs-fixed *prover* fork (both Option C/D; rotating chosen with named 1/N loss). `MultisigGroup.pqc_public_key` is Option A (rejected 2026-04-04 for `pqc_auth` fingerprint). Do not quarantine behind `frost-sal-v4` — artifact fuses SAL keys with rejected fixed group PQC key. F-4/F-5/F-9 evaporate on delete. `V3_ROLLOUT.md` coordinator-held + staking-recommendation drift fixed (P0-h partial). Retract "Phase 6 cryptographer" claim on per-output privacy. - **docs: §0.4 coexistence — V4 rewrites beside V3.1 (PR #308).** §15.4–15.5 imply permanent dual stack (version byte + HRP), not in-place evolution. Discriminability > evolvability; MSW-4/5 version bytes are the entire V3.1↔V4 interface. Track A permanent / Track B ~4y then legacy — process inversion named. MS-1(a)/(c) preferred, (b) rejected under coexist. MS-7: versioned record with working discriminator, not evolvable enum. - **docs: R1-F-11 / MSW-4+5 — version hooks for 2030+ (PR #308).** `group_version` fused with `MULTISIG_CONTAINER_VERSION` (constant, not `container.version`); `spend_auth_version` hardcoded / not on container wire by accident; reserved-namespace KATs incomplete (`group_version` never varied). Same shape as F-1 — feature gate does not protect genesis-frozen bytes. MSW-4 unfuse + known-version set; MSW-5 pin carrier decision; §15.1 honesty (P0-k). - **docs: §15.4 posture pin — auth already PQ; SAL = liveness (PR #308).** Scheme_id=2 is M×ML-DSA today; solo and multisig share classical FCMP++ membership/SAL. Multisig adds no classical exposure. Classical SAL is 1/N permanent-*loss* (availability), not compromise. Grinding / griefing → MS-4/5 engineering, not Phase 6 crypto. Phrase pin: `spend_auth_version=0x02` = 15.4a threshold classical SAL; 15.4b = composite/lattice-only *auth* size — not lattice SAL (impossible under FCMP++). Ship decision = economics (~3×), not crypto-maturity. - **docs: split PQC_MULTISIG §15.4 — FROST SAL ≠ lattice auth (PR #308).** §15.4a FROST SAL (mandatory prover / 1/N) blocked on two-component address / `y`, not NIST. §15.4b pure-PQC composite auth blocked on lattice threshold maturity; NIST IR 8214C is reference-material only (MPTC ~2027 → later process). TRacoon wrong N regime + trusted KeyGen + not FIPS; watch dPN25 / TALUS for size. MSW-G size lens has 15.4b expiry; genesis still freezes on today's signature-list economics. `VERSIONING.md` V4 note aligned. P0-j landed. - **docs: MSW-G — MAX=7 has no derivation; F-1 fix is the 6/7/8 pick (PR #308).** Recorded in `V3_1_MULTISIG_RUST_ENGINE.md` §0.3. Fossil bound accidentally caps spendable N at 6; reward zone independently caps worst-case at 6. Raising the bound to admit 7 is a behavior change. Candidates 6/7/8 held open. MSW-2 reopen names **either** separator (length primary; byte[2] secondary). V3_ROLLOUT fossil ~7× table + coordinator/staking drift → P0-h. TRacoon / `lib-q-threshold-raccoon` noted as V4 research, not Track A. - **docs: F-2 retraction — Track A revised to constant fix (PR #308).** Leaf preimage **left alone** (not free: wallet2 / emission Auth-B / FFI ABI / vectors; not needed: byte[2] prefix disjointness already separates scheme 1 vs 2). Track A = MSW-1 bounds reconcile + MSW-2 disjointness KAT + MSW-3 misattribution; **MSW-G** on whether `MAX=7` survives reward-zone. Fee model / `MAX_INPUTS=128` → Track B. `wallet2` PQC multisig group surface acknowledged under MS-2. - **docs: Round 1 adversarial on V3.1 multisig engine design — Track A/B split (PR #308).** Records R1-F-1…F-11 in [`V3_1_MULTISIG_RUST_ENGINE.md`](design/V3_1_MULTISIG_RUST_ENGINE.md). **Track A (MSW-1…MSW-3):** V3.0 pre-genesis wire — N=7 blob-bound fossil (fund-loss), leaf `scheme_id` genesis decision, dual verify sites; **not** held by engine rule-26 halt. **Track B:** MS-1…MS-7 remain gated; **MS-8 retired** (group_id verify is a no-op → MSW-2). Round 1 cannot close until F-3…F-7 + F-6 CI. INDEX `MSW-` registry; FOLLOWUPS group_id item superseded. - **docs: V3.1 multisig Rust engine integration design — Round 1 OPEN (`docs/v31-multisig-rust-engine-plan`).** Fires `V3_ENGINE_TRAIT_BOUNDARIES.md` §10.3.1. New carrier [`docs/design/V3_1_MULTISIG_RUST_ENGINE.md`](design/V3_1_MULTISIG_RUST_ENGINE.md) poses **MS-1…MS-8** (trait identity, Rust-owns-logic C++ boundary, feature-gate flip criteria, receive/spend/transport placement, Persistence R6 confirm, daemon `group_id` verify as separate PR). Protocol remains `PQC_MULTISIG.md`; §16.4 C++ wallet bullets marked superseded pending Phase 0. INDEX registry + FOLLOWUPS pointer updated. **No implementation** until design rounds + rule-26 pre-flight and explicit go-ahead. - **archival: `ShardSet` newtype — parse-don't-validate holdings shard list, and the shard list is now a set on the wire (gate-4 §3.4.1; `feat/shardset-newtype`).** Converts `HoldingsDescriptor.shard_ids` from a bare `Vec` to a validated `ShardSet`: the structural invariants that were carried by convention — the `MAX_HOLDINGS_SHARDS` bound (each verify re-guarding; `bond_floor` signalling an invalid set with an in-band `0`, the same value the empty exit shape returns) and duplicate-freeness — are enforced **once, at construction**, so an invalid set is unrepresentable past any decoder. - **Consensus tightening (ratified 2026-07-15): holdings are a set.** A shard is a distinct retention obligation, so a `ShardSetCompact` list carries no duplicate id — previously rejected only inside the `HoldingsUpdate`/`Rebond` diffs and **silently tolerated by `JoinMarket`** (`[7, 7]` bonded `2·FLOOR` for one shard). Duplicate-carrying byte strings now reject at decode. - **Byte-identical for accepted txs.** Insertion order is preserved (not canonicalized), so a valid `ShardSet` encodes exactly as the old `Vec` did — no accepted tx re-encodes; only the validity predicate tightens. Pinned by the unchanged golden-vector codec test. - Construction routes through `ShardSet::new` at every decoder boundary: the wire decoder (new `WireError::HoldingsDuplicateShard`), the FFI vin marshal (a second decoder — new code 48 `HOLDINGS_DUPLICATE_SHARD` beside the count cap), the emission claim-source decoder, and every builder/fixture. `ShardSet` derefs to `[u64]`, so read sites are untouched. The independent `shekyl-wire` oracle enforces the same duplicate-reject in its own `Holdings::read`. - **Retires the per-verify oversize guards** the FOLLOWUP named: the `RebondPostOversize` verify belt is dead (verify receives a bounded `ShardSet`) — removed; FFI/C++ code 45 reserved, not renumbered. The raw-slice connect path keeps its `RebondConnectError::PostOversize` belt (C++ marshals the post array to the connect fold without re-constructing a `ShardSet`); `bond_floor_of` keeps its count-path oversize guard (the Unbond fold's C++ count crosses the FFI as a primitive). - Tests: `ShardSet` bound/duplicate rejection, insertion-order-preserved byte-identity, decode-rejects-duplicate-wire-bytes, FFI-marshal and `shekyl-wire` duplicate-reject twins. Behavior-preserving migration across 6 crates (retention, ffi, engine-core, bond-builder, staking-sim, shekyl-wire). - **archival: `Rebond` — post-slash reinstatement, end-to-end + the same-epoch slash-interval consensus-halt fix (gate-4 §3.4; P2B-9, ratified 2026-07-14; `feat/bond-fsm-rebond`).** The final bond-FSM kind: the full genesis lifecycle (`JoinMarket / HoldingsUpdate / Rebond / Unbond`) now verifies, connects, and reorg-reverts through the real block path. **Reinstatement, not re-entry:** a slash is a bounded penalty (one shard + one burned `FLOOR` + a zero-earning gap) and `Rebond` resumes the persona in place — same `P_canonical_id`, carried tenure, backlog; it is the only exit from a terminal slash (nothing to unbond at `bonded_total == 0`). - **Consensus-halt fix (commit 1, P2B-9 Pin 5):** every held shard is challenged every epoch against a stale eligibility copy, so an offline N-shard record appended N identical open bad intervals in one block — at N > the 256-interval codec cap the encode threw **inside the block-connect slash hook**, a deterministic consensus freeze reachable by accident. Same-epoch slashes now **coalesce** (append only when no open interval exists); the pop revert was already idempotent-compatible, so the fix is apply-side only. Establishes the **≤ 1 open interval** invariant the Rebond close targets. KAT drives a 300-shard same-epoch sweep (above the cap) with byte-exact revert. - **Verify (Rust-native):** precondition = exactly one open bad interval (zero → not slashed; more → the coalescing invariant's loud corruption reject); `post ⊇ current` duplicate-free non-empty superset (Pin 1 — shedding of carried shards forbidden, stays `HU`-drop's gated job; the slashed shard's abandonment is **priced** — burn + portfolio gap — not prevented, rule-21 reopen if slash economics ever return collateral); `bond_credit == bond_floor(post) − bonded_total` with **zero legal and common** (Pin 2 — the landed slash preserves floor-equality, no deficit exists); interval-log headroom ≤ 254 (Pin 6 — one slot for the next slash + one for `Unbond`'s clean close, exit always reachable); ShardSetCompact both sides. Auth = identity key on the landed GF-1 selector (credit path, Pin 4). - **Connect/pop:** the connect closes the open interval **in place** at `end_exclusive = E_rebond + 1` (Pin 3 — the partial rebond epoch forfeited in both directions, the `E_add+1` mirror; carried and added shards resume together) and rebuilds the coupled add-epochs (carried keep theirs, added take `E_rebond` — Pin 7); the pop re-opens the journaled interval to `MAX` (identity-belted) and reverts the `|added|·FLOOR` credit, zero delta included. New `archival_bond_rebond_log` pre-image journal (maxdbs 45→46) carrying the closed interval's index + start alongside the holdings pre-image — `pre_bonded_total == 0` legal (terminal reinstatement). - **FFI + dispatch:** `shekyl_archival_verify_rebond_bond_post` / `shekyl_archival_rebond_connect` / `shekyl_archival_rebond_pop`; shared `BOND_POST` error space codes 37–44 + the `REBOND_APPLY` family; the C++ verify arm marshals the record + full interval log and pins the identity key; per-post live-counter threading throughout. `rebuild_shard_add_epochs` generalized to a K-shard override set (shared with `HU`). - **KATs:** 17 Rust (standing-only/growth/terminal accepts; swap-shed, headroom-boundary, multi-open, and every term/shape reject) + 3 C++ real-block-path (growth connect/pop with interval close/re-open and add-epoch rebuild; standing-only zero-credit round-trip; journal codec). - **Docs:** P2B-9 pins in `PHASE_2B_FSM_RETOOL.md` (incl. the corrected Pin 1 coverage boundary + the R-3-routed retention-valve forward question); gate-4 §3.2/§3.4/§4.1 swept to the credit equation and the `E_rebond + 1` close. - **Review round (9 findings + 2 Copilot):** two new verify belts — `RebondPostOversize` (an oversize post on a terminal-slashed record collapsed `bond_floor` to 0, verified at zero credit, then aborted block connect at the record encode) and `RebondRecordFloorBroken` (a floor-drifted record now rejects at verify instead of riding to the connect fold's FATAL belt — tx rejection, not a chain halt); the FFI vin marshal now enforces the wire codec's `MAX_HOLDINGS_SHARDS` bound for every post kind (the marshal is a second decoder), and the Rebond connect fold belts oversize posts. The Pin-5 coalescing *decision* moved Rust-side (`slash_open_interval_to_append`, KAT-covered) — the C++ slash writer appends exactly what the fold returns. Duplication paid down: the HU marshal prologue is shared with the Rebond FFI entry (`bond_post_record_marshal_prologue`), the LMDB connect-writer scaffold is generalized to all POST-holdings kinds (`apply_archival_bond_record_update`, with the in-place interval close as a fold output), the verify arms share one record-fact marshal (`archival_marshal_record_facts`), and the Rebond round-trip KATs share one seed/tx builder. - **archival: `HoldingsUpdate` add + drop — the voluntary one-shard bond balance path, end-to-end (gate-4 §4.4; P2B-7; `feat/bond-fsm-holdings-update`).** Building on slice A's `bond_duration` freeze: the fourth and final bond-FSM kind (`JoinMarket / Rebond / Unbond / HoldingsUpdate`) now verifies, connects, and reorg-reverts through the real block path. A record stays `Bonded` throughout — one shard is added (`+FLOOR` credit) or dropped (`−FLOOR` debit) under the `bonded_total == bond_floor(holdings)` balance. - **Record (v6):** `ArchivalBondValue` gains a per-shard `shard_add_epochs` array, index-parallel to `held_shard_ids` under one holdings count (a length desync cannot round-trip). Join-time shards take `E_join`; a HoldingsUpdate-add appends `E_add`; a drop removes the dropped shard's entry. The drop path reads it for the retention horizon and (slice A) the age-at-add. The reorg-log schemas ripple to match: `ArchivalSlashRevertValue` v3 and `ArchivalBondUnbondRevertValue` v2 carry the coupled add-epochs. - **Verify (Rust-native, `shekyl-archival-retention::bond_post`):** `verify_holdings_update_add` gates on ShardSetCompact record + post, exactly `+FLOOR` credit, good-standing (`good_through` the current epoch), a single-shard set-diff add, and post-floor equality; `verify_holdings_update_drop` gates on exactly `−FLOOR` debit, a single-shard removal (drop-last rejected — a full exit is `Unbond`), post-floor equality, the **retention horizon** (`current_epoch − add_epoch ≥ bond_duration`), and the **grace-tail** precondition (the dropped shard's release cooldown elapsed and slashes settled through its last-served anchor). 22 KATs. - **Grace-tail DROP model (ratified 2026-07-15):** the release cooldown is a verify *precondition*; at connect the shard leaves `holdings` and the FLOOR returns immediately via the `bond_debit` source term — no cooldown sub-state, no interval, no clean-close marker (superseding the earlier drop-then-cool fossil, P2B-7 Pin 2/3, swept in the design docs). - **Connect/pop folds + FFI:** `holdings_update_{add,drop}_connect` produce the counter movement (absolute post-values, threaded per post) and the single-shard delta; `holdings_update_pop` reverts by the connect's `±FLOOR`. Exposed across the single `shekyl-ffi` boundary (`shekyl_archival_verify_holdings_update_{add,drop}`, `shekyl_archival_holdings_update_{add_connect,drop_connect,pop}`). - **C++ dispatch (thin glue):** the bond-post verify branch routes add/drop on the debit direction and applies the GF-1 auth selector (credit → identity key; drop's debit → the record's committed `bond_spend_pk`, the Unbond arm's twin); the connect arm journals the record pre-image to a dedicated `archival_bond_holdings_update_log` (the record stays `Bonded`, so it cannot share Unbond's Exited/clean-close pop path) and applies the fold's write set with per-post live-counter threading; `pop_block` reverts it. Real-block-path add/drop connect/pop KATs assert the add-epoch rebuild, the counter movement, and the byte-exact reorg restore. - **Review round (2026-07-14):** seven correctness findings fixed. The checkpoint fast-path GF-1 belt is re-keyed on the GF-1 selector itself (`bond_debit > 0`, not `post_kind != JoinMarket`) — as written it rejected every valid HoldingsUpdate-add block under fast sync (the add is a credit arm, identity-key authorized): a network split. The HU verify arms gain the **Bonded-state gate** (shared prologue; P2B-7 Pin 1 `Bonded → Bonded`, `HoldingsUpdateRecordNotBonded` + connect-fold `RecordNotBonded` belt) — without it an Exited record passed every add gate (the zero-length clean close excludes nothing from `good_through`; `bond_floor(∅)` vacuously satisfies the floor pins) and became a JoinMarket-bypassing resurrection whose connect threw on the empty-pre-image journal row: verify-valid but unconnectable on every node, a chain-stall vector. **Pin-4 + Pin-5 are CLOSED, not follow-on:** `archival_bond_holds_shard` — the one accessor both serve-credit acceptance and slash eligibility bottom out in — bounds a tip-held shard below by its v6 add-epoch (and the slash-log reconstruction by the row's journaled add-epoch), enforcing per-shard `E ≥ E_add + 1` symmetrically: no add-epoch serve credit, no unjust slash for a challenge that fired before the add; a voluntarily dropped shard answers not-held everywhere (grace-tail keeps no drop interval — the drop epoch's pending acceptances are forfeited, the add-forfeit's twin). Unfrozen-at-add fails closed to the LONGEST retention horizon on both corners (`ShardAgeAtAdd::from_add`: `freeze ≥ H_close(add_epoch)` → age max, matching the C++ missing-freeze-row sentinel; previously the same condition earned a 4- or 20-epoch lock depending on when the freeze landed — a 5× early-recycle hole vs. the §L18 age-stratified friction). The drop-after-fire scenario is documented as the RATIFIED grace-tail bounded forgiveness (Pin 3; capped at one cooldown — "stop serving and hold" keeps being slashed), and the slash-apply FATAL's HoldingsUpdate revisit is discharged. The pop-ordering comment now states the real dependency (slash revert FIRST; the slash journal restores the same record fields — the "compose in any order" claim was false). Dedup: shared C++ debit-auth pin (Unbond + HU-drop), shared Rust verify + FFI marshal prologues, shared HU connect-writer scaffold (add/drop differ only in the fold call). - **archival: `bond_duration(age)` retention-horizon freeze — HoldingsUpdate slice A (gate-4 §4.4; `ARCHIVAL_TIMING_CONSTANTS.md` §1; P2B-7 Pin 3 / P2B-8 Q3; `feat/bond-fsm-holdings-update`).** The genesis-frozen per-shard drop-eligibility formula, landed on its own reviewable surface ahead of the HoldingsUpdate verify/connect (slices B/C) that consume it — pure functions, no schema or consensus-path change. - **`bond_duration(ShardAgeAtAdd) -> epochs`** (`shekyl-archival-retention:: bond_duration`): `BASE·(1 + SCALE·age)`, **integer-canonical** (round-half-up, floored at 1; no float in consensus). `BOND_DURATION_{BASE_EPOCHS,AGE_SCALE}` = 4/4 (H2 plateau arm, provisional within the scale band [2,8]) wired from `config/consensus_constants.json` through `build.rs` (emitted into the OUT_DIR-generated include that `bond_floor.rs` pulls in) and, in parallel, through `generate_consensus_constants.py` into the C++ constants header for cross-language parity (no C++ consumer at genesis — the bond-FSM verify is Rust-native). - **`ShardAgeAtAdd` newtype** makes two consensus properties unrepresentable at the type rather than tested-against: the age is evaluated **at add, not at drop** (the sim arms the lock on fresh acquisition and counts down, so a rising-target drop-time age cannot be expressed), and its close reference is **`H_close(add_epoch)`**, the same settlement-close height the reward curve feeds `shard_age_milli` (the constructor derives the close from the add epoch, so a raw within-epoch block height cannot be passed). One age normalization across reward and retention, by construction — the age-realization invariant, pinned in `ARCHIVAL_TIMING_CONSTANTS.md` §1. - **KATs:** endpoints/midpoint/monotonicity/floor, the F1 close-reference check (age uses `H_close(add_epoch)`), and the **F2 full-sweep parity** — the integer artifact equals the sim's f64 model across every `age_milli ∈ [0, WORK_MILLI_SCALE]`, with the **integer authoritative**: a divergence fails loudly as the signal to re-check the sim economics (the genesis 4/4 constants have no half-boundary case, so the sweep is clean; the KAT is the tripwire on any re-pin). - **archival: GF-1 `bond_spend_pk` wire+record — the debit-side enable flip (gate-4 §3.5 step 5 / §4.1 / §9.11; `feat/gf1-bond-spend-pk`).** The sequenced prerequisite the Unbond wiring named is landed; **Unbond txs now verify end-to-end** and `HoldingsUpdate`'s drop-path auth is unblocked. - **Wire:** `txin_archival_bond_post` gains the JoinMarket-coupled GF-1 debit authorizer in all three C++ serializers (binary/boost/JSON) and in the Rust `bond_wire` codec — present with the exact canonical single-key length iff JoinMarket, absent otherwise, enforced both directions on every representation (a misconstruction is loud, never silently dropped). Reconciles the shekyl-wire §9.11 divergence; the §3.4.1 `signature_preimage` binds the key (the operative consensus binding also rides the tx prefix inside the pqc payload); the gate-4 lifecycle KAT fixture is regenerated from the coupled codec and now exposes `bond_spend_pk_hex` for the C++ integration KATs. - **Record (v5):** `ArchivalBondValue` commits the key once at JoinMarket connect — `put_archival_bond_record` takes it as a **no-default** parameter (no caller can silently commit an empty authorizer); v4 rejects at decode (pre-genesis posture: reset the data directory). The real-block-path connect KAT asserts the committed copy. - **Auth (the reject→auth swap, one change):** the §3.5 step-5 selection is live in `check_archival_bond_post_input` as the **shared debit authorizer** — credit paths pin the pqc auth key to the identity `P_pubkey`; debit paths pin it to the record's **committed** `bond_spend_pk` (the signature over the whole-tx payload is verified against that key by `verify_transaction_pqc_auth`). A record committing no key authorizes nothing — fail-closed, never identity fallback. Pinned by the discriminating KATs: committed key accepts, identity key / foreign key / keyless record all reject; the credit-path twin proves the vin's own key does not authorize the credit. - **Wallet seam:** `ArchivalBondPostVin` carries the key (`build_join_market_vin` sets it from `ArchivalPKeys::bond_spend_pk`; `wire_bond_post_input`'s supply-alongside parameter deleted), so the A-1 assembly invariant now pins the key byte-for-byte between the prefix input and the signed vin. - **Review round:** the boost serializer enforces the coupling both directions (it admitted a non-canonical key on load — the one codec bypassing the length pin the record-commit relies on — and silently dropped a stray one on save); JSON `toJsonValue` refuses at write what `fromJsonValue` refuses at read. The FFI vin marshaler threads the real `bond_spend_pk` through both verify entry points and enforces the coupling itself (`SHEKYL_ARCHIVAL_BOND_POST_ERR_BOND_SPEND_PK_COUPLING`, code 23) instead of placeholdering the field into a vin the Rust wire codec refuses to serialize. `check_archival_bond_post_input` gains the §9.11 belts for non-parse callers on both arms and pins the committed key BEFORE the per-shard cursor scans + FFI verify (an unauthorized `Unbond` no longer pays the LMDB walk). The block-level checkpoint fast-path arm executes the swap its belt named: under the fast path it re-pins the debit's pqc auth key against the record's committed `bond_spend_pk` instead of blanket-rejecting every debit kind — without this, a block carrying a valid `Unbond` was rejected by every node, contradicting the enable flip; debit-side posts now also feed the per-`P` block-unique pass. The gate-4 lifecycle KAT reads gate-2's integration section directly instead of embedding a copy (the committed copy had silently drifted on dev with no test failing). - **archival: `Unbond` release-gate hardening + block/template dedup (review round, `feat/bond-fsm-unbond-connect`).** The release cooldown gains a second predicate — `slashes_settled_through`: the slash scheduler's settled watermark (`archival_last_slash_epoch`) must have reached `P`'s last-served anchor, so a held-but-unserved failure at or before the last serve is slash-processed on bonded collateral before the exit verifies. This closes the one-block race where the connect dispatch precedes the per-block slash fold. Epochs after the last serve are exit-forgiven by construction: slashability ends at the `Unbond` connect and the refund is never clawed back (so the pop-order slash revert is a defensive belt, not a real-case dependency; `apply_archival_slash_one` now FATALs on a shard the record does not hold rather than silently no-op). A `CompleteTree` record's cooldown anchor is sourced from an all-shards `P`-prefix serve-credit scan (`archival_bond_all_last_served_epochs`) instead of its empty stored shard list. The block-level pass gains a GF-1 fail-closed belt (any debit-side bond-post kind rejects the block, matching the per-tx rejection the `PER_BLOCK_CHECKPOINT` fast path skips), and the mempool/template fill dedups archival block-unique keys (bond-post `P`, emission `(P,E)`, serve-credit `(P,shard,E)`) so two conflicting txs cannot co-occupy one self-invalid template (a mining-stall vector). A serve-credit + `Unbond` for one `P` in one block is deliberately allowed (served epochs are slash-immune; the re-armed span is the exit-forgiven tail). - **archival: `Unbond` C++ dispatch wiring — connect arm, pre-image journal, pop twin, verify dispatch, per-`P` block pass (gate-4 §3.5/§4.3/§5; P2B-8 increment c, `feat/bond-fsm-unbond-connect`).** The `add_transaction` bond arm dispatches on `post_kind`: Unbond → `apply_archival_unbond`, the single writer that journals the record's full pre-image (`m_archival_bond_unbond_log`, BE(height)‖BE(seq) — `ArchivalBondUnbondRevertValue` carries exactly the three mutated fields, disjoint from the emission journal's), applies the Rust fold's write set verbatim (Exited record + clean interval-close + counter debit), and FATALs on any fold error. Counter threading is per-post (live `get → fold → set` inside the writer) — armed by the two-`P`-one-block KAT, which a hoisted per-block read would fail. `pop_block` calls `revert_archival_unbonds_at_height` after the slash revert as a **defensive ordering belt** — slashability ends at the `Unbond` connect (an Exited record holds no shards, so it is never a slash candidate; ratified 2026-07-12), so nothing trails the clean close and the pop fold's trailing-close check holds unconditionally; the fold validates the Exited shape + trailing close before re-crediting, and the pre-image restores byte-exactly (real-block-path connect/pop KAT). Verify dispatch: `check_archival_bond_post_input` gains the chain-height operand and an Unbond branch marshaling record facts + the P2B-8 Q1/Q2 cooldown anchors (`archival_bond_last_served_epochs`: one reverse-cursor seek per held shard over the BE serve-credit key; never-served omitted). The per-`P` block pass is marshaled beside the emission `(P,E)` pass (`shekyl_archival_bond_post_block_unique`, reject-not-serialize). **Named blocker (rule 22, gate-4 §3.5 + FOLLOWUPS): Unbond txs remain verify-rejected fail-closed at the §3.5 step-5 debit-auth step** — no record commits a `bond_spend_pk` (the §9.11 field the Rust wallet wire carries is absent from the C++ vin serializer and the v4 record), and the identity key must never authorize a value-out (GF-1). The GF-1 wire+record sub-increment is the enable flip. - **archival: `Unbond` connect fold + pop twin, Rust-native (gate-4 §4.3 "On confirm" / §5; P2B-8 increment b, `feat/bond-fsm-unbond-connect`).** `shekyl-archival-retention::bond_connect` is the single implementation of the release's write set — the post-connect record (`bonded_total = 0`, compact-and-empty holdings; the record persists for backlog claims), the `total_bonded_atomic` debit, and the §4.3 **clean interval-close**, landed as a **zero-length interval `[E_unbond, E_unbond)`** in the record's interval log: `good_through`-inert by construction (KAT-pinned, including next to an open slash interval), it records the exit epoch for the later `W`-lapse / `p_slot`-burn step with **no schema change**. The pop twin validates the tip record is the connect's product (Exited state + trailing clean close) before re-crediting the counter; record fields restore from the connect's full pre-image journal (emission WS-2 §6.3 shape — the vin carries the *post*-state, so holdings are otherwise unreconstructible). FFI: `shekyl_archival_unbond_connect` / `shekyl_archival_unbond_pop` — the C++ connect site (follow-on wiring increment) writes exactly what the out-params dictate, no consensus arithmetic caller-side; all non-OK codes map to FATAL, never a soft skip. The refund needs no connect write: `bond_debit` is the CT-balance **source term** on the wire (§2.4 — the refund vouts are ordinary hidden FCMP++ outputs that mix normally). Riders: `verify_unbond_bond_post` gained the **`IntervalLogFull` belt** (a record whose interval log is at the codec cap rejects at verify — a verified-but-unconnectable tx would be a deterministic halt); gate-4 §4.1 executed P2B-8 Q2's forward amendment (`last_served_epoch` dropped — derived, never stored) and now pins the landed `bond_event_log` representation; §3.5 documents the connect/pop write set and names the wiring obligations (block-level per-`P` bond-post pass, journal table). **Review round (same increment):** the marker's storage half is now KAT-pinned end-to-end (`archival_substrate_lmdb.unbond_clean_close_marker_round_trips`: v4 codec encode→decode→re-encode identity on `[E, E)`, the production LMDB writer/reader on the Exited shape, and the production `good_through` read path — verified besides that **no** codec/marshal/decode path anywhere asserts `start < end`); `kMaxBadIntervals` pinned as a **genesis-frozen consensus constant** (verify validity keys on it; static_assert + Rust-twin lockstep note) with the interval log's **two entry kinds** documented at both `BadInterval` types; and the block-level pass's **decision function landed** (`bond_post_block_unique` + `shekyl_archival_bond_post_block_unique`, the `emission_block_claims_unique` sibling keyed on `P` alone, reject-not- serialize) — covering the whole same-`P` same-block class (double-JoinMarket credit, double-Unbond debit, mixed kinds), which the §4.5 conservation audit cannot catch (a double-credit doubles both sides consistently). C++ marshaling of the pass stays with the wiring increment (§3.5). - **tests/wallet/rpc: emission-claim PR-4a — staker regtest harness up to the daemon submit gap, its enablers, and the Q11 blob-boundary arm (`EMISSION_CLAIM_BUILDER.md` §8 "PR 4 split"; `feat/emission-regtest-e2e`).** Driving the harness live surfaced that the daemon's Rust submit engine accepts neither transaction kind the E4-gate e2e needs (no bond-post Phase-C battery; Phase A rejects `Input::ArchivalRewardEmission`), so PR-4 split into 4a/4b/4c — 4a is this batch, 4b is the daemon submit batteries (FOLLOWUPS V3.0 item), 4c is the e2e proper. Landed here: - **tests:** the CB-4 **Q11 blob-boundary invariance arm** (`archival_emission_ct_balance.cpp`): the emission fixture made valid under the full `ver_non_input_consensus` dispatch (valid-domain output keys, fee-input key image, per-input `pqc_auths`), plus the arm asserting the CT-balance verdict is invariant under `canonical_bytes` variation with an oversized-`pseudoOuts` control. Closes the Q11-KAT FOLLOWUPS item. (Header hygiene rider: `tx_verification_utils.h` now includes `blobdatatype.h` — it referenced `blobdata` without declaring it.) - **archival:** a FAKECHAIN-only **settlement-epoch-blocks override** (`SHEKYL_SETTLEMENT_EPOCH_BLOCKS`, read once and **armed explicitly**: an unarmed process — every wallet, every public-net daemon — runs the genesis schedule no matter what the environment says, and the wallet warns once when it ignores a set lever), so the regtest e2e reaches an epoch close in minutes instead of hours. The daemon arms only on FAKECHAIN at `Blockchain::init`, refusing on the lever's presence on public nets and on an invalid value (loud, never a silent clamp-to-genesis), and **pins the effective schedule into the fakechain datadir** (LMDB `properties`) so a reopen under a different schedule than its epoch-derived rows were written with refuses with the remedy named. FFI exposes the effective value, overridden flag, presence, and the arming entry point. - **rpc:** a FAKECHAIN-only **serve-credit-bit injection RPC** (`inject_archival_serve_credit`, WS-1 bits-only form over the production writer `set_archival_serve_credit_bit`) — Gate-6's regtest stand-in; `CORE_RPC_VERSION` → 3.19. The poke is deliberately **not** pop-symmetric; the e2e's pop leg is bounded above the injection height. - **wallet:** `LocalNodeRpc`, the local-posture `PersonaIsolatedTransport` (fail-closed loopback pin via `IpAddr::is_loopback` + `localhost`; in-URL credentials rejected — authority-scoped — without leaking them) — the DQ-T2.3 choice site's local arm. `SimpleRequestRpc`'s credential parse is likewise scoped to the URL authority (a whole-URL `@` split misparsed path/query content as credentials). - **wallet:** the **`SweptFeeInputs`/`ClaimOperands` designation-event witness** (PR-3's named forward item, landed with the seam as scoped): `DesignatedBacking::fee_sweep` is the sole mint of a sealed fee selection carrying the designation's anchor and exclusion, `with_paths` zips membership paths into sealed `ClaimOperands`, and the handler's two step-2 runtime refusals (`StaleClaimAnchor`, `BackingInFeeSet`) are deleted — cross-pairing is now unrepresentable, refusals moved to mint time (`ClaimFundingError`), all three KAT'd at the mint. - **wallet:** the **CB-3 dispatch seam** (`engine/claim_dispatch.rs`): `submit_emission_claim` — claimant id from the actor's read-only `PersonaIdentityOf` projection (no activation: a claim is read-and-sign, never a rotation) → orchestrate → **persist-before-dispatch** (a sealed `PendingEmissionClaim`, pending-post block v4, carries the bytes, the fee-gindex reservation feeding the shared `reserved_gindexes` union, and the claimed epochs; one live claim per persona is the in-flight epoch dedup) → submit through the audited posture→`BroadcastSubmitter::local` choke point (scheduling stays external, the GF-4 seam; claim retirement/resubmit = the WI-3- sibling claim driver slice); tripwire test pins the pipeline route, the choke point, and the seal-before-send ordering. - **tests:** the **staker regtest harness** (`regtest_e2e.rs`): staker wallet lifecycle against a live `shekyld --regtest`, persona funding over a production FCMP++ transfer, P-scan discovery, live-estimate fee sizing, production bond assembly (with not-yet-spendable retry), and dispatch — asserting the daemon's exact Phase-C refusal (`RejectedTerminal { Malformed }`) as a **tripwire** that fails loudly and promotes to accepted-and-applied when PR-4b lands the battery. (The live `FeeTooLow` drawn en route disproved `phase_a.rs`'s stale "bond posts can't clear Phase A" docs — flagged to PR-4b.) ### Fixed - **wallet: payments encapsulated to `montgomery(view_pk)`, not raw Edwards bytes (PR-4a, harness-surfaced).** `sign_tx` passed the recipient's Edwards view key straight into the output builder's X25519 KEM-encapsulation slot, so every cross-wallet payment produced outputs the recipient's scanner could never recover (funds visible only to the sender's change path). The view key is now converted via `ed25519_pk_to_x25519_pk` at the single build site; KAT'd end-to-end (decoded-address payment recovered by the recipient's `GuaranteedScanner`). - **wallet: P-scan exhaustiveness gate fetches full tx bodies (PR-4a, harness-surfaced).** The SP-6 exhaustiveness check recomputes each body's committed hash from received material, but the block source fetched **pruned** bodies — a storage-pruned FCMP++ spend hashes with the null prunable component, so the scan wedged permanently (`BodyMismatch`) at the first spend-bearing block. `TxBodyForm::{Pruned,Full}` now parameterizes the fetch (`block_fetch.rs`), the P-scan block sources request full bodies, and `parse_full_tx` rejects prunable-stripped spends — naming the storage-pruned-daemon cause and its remedy (run an unpruned node) when the split body is exactly a canonical pruned spend, since that is a supported daemon mode and not corruption; the bandwidth-cheap pruned option over Tor is a rejected-with-reopening FOLLOWUPS item (V3.2). ### Added - **wallet: emission claim handler + Engine-side orchestrator — claim-builder PR 3 (`EMISSION_CLAIM_BUILDER.md` §8 PR 3; `REWARD_EMISSION_VIN_PLAN.md` §8.0.2/§8.0.3).** The builder half of the E4 gate is complete: the production path assembles a signed `txin_archival_reward_emission` transaction end-to-end. Six commits: - **wire:** the emission input arm (dense tag `0x04`, opaque `canonical_bytes`) — closing the latent break where a wallet scanning a chain containing any emission claim failed to parse the block. - **pscan:** the `EmissionReward` scan arm (GF-4b item 2) — own emission vins classify their vouts' lineage, so first-emission backing change lands as `BondPostChange` instead of stranding in rung 3. - **engine:** the designated-backing selector (GF-4b §5 item 1): `BackingSet::from_spendable` (spendability + lineage filters, survivor tripwire) → `designate_backing` (most-recent-eligible, reproducible from chain state) → `DesignatedBacking`, whose private record and `fee_sweep`-owned Q11 exclusion make backing-as-fee-input unrepresentable through the claim path. - **engine:** the `AssembleEmissionClaim` handler (CB-2 `AssembleBond` sibling): derive + assemble via the PR-2 module, F-C1c assembly order (fee inputs + vouts + extra → signable hash over the vin-less prefix → membership proof (`prove_backing_membership`, `spawn_blocking`) → dual auths from the **derived bundle** (Auth-B backing key, Auth-P identity hybrid key; CB-2: no master-seed re-borrow) → full prefix hash → fee-side FCMP++ proof + tx PQC auths), with the daemon-side differential KAT re-deriving the whole verify chain over the produced bytes. - **engine:** the claim orchestrator (`engine/claim_orchestrator.rs`): fetch → reference-height anchoring (daemon's two-sided age gate reconciled with `REF_ANCHOR_AGE`) → provable-record filter → designate → fee sweep → membership-path assembly → handler dispatch; the reply returns **unbroadcast** (CB-3: dispatch is the GF-4 seam). End-to-end integration test over a real 30k-block `CurveTreeClient`, a canned RPC transport, and the real actor, with daemon-side re-derivation against the real tree root. - **cleanup:** every PR-1/PR-2 staging allow whose named consumer was PR-3 is deleted; skip diagnostics now log locally (their stated purpose); the one remaining claim-surface allow is the orchestrator + its reply's fields, staged narrowly against the CB-3 dispatch seam. FOLLOWUPS: the reward→identity structural-binding forward-dep discharged (the vin binds identity via the hybrid pubkey chain, never the deleted ed25519/spend-key equality); the `AtomicUnits::mul_div_rem` deferral re-evaluated on its named shape (criterion not met — the rebased arithmetic is `u64` consensus surface; re-anchored V3.1). PR 4 (regtest e2e + the Q11 blob-boundary arm, both named in the §8 PR-4 scope row) is the chain's only open item, gated on #281. - **wallet: WI-2 Engine-side `assemble_bond_post` orchestrator.** Wires §3.3 steps 1–6 over an independently constructed `PendingPostStore` (F-1), named `daemon_claimed_tip` for `anchor_t0` (F-2), and the existing curve-tree anchoring procedure for `ReferenceBlock` (F-6). Domain-newtype carrier: `PFundingOutputRecord` / sweep amounts / `funding_gindexes` → `PSlot` / `TxHash` / `GlobalOutputIndex` / `AtomicUnits`; `PSCAN_STATE_VERSION` 6 + `PENDING_POST_VERSION` 3 (pre-genesis fail-closed). Go-live remains compile-blocked on `SpentRecordsDurablyPruned` until 2d-1 / SP-R0; dead_code allows on the assemble surface retire only when SP-R0 **and** RPC entry land. (`feat/wi2-bond-orchestrator`) - **sim: leg-(b) wall-clock sweep-phase arm graded in-model (`ARCHIVAL_BOND_WI4_MEASUREMENT.md` §19; WI-3 §3.2 part 3; WI-4 reconvergence leg (b)).** The §13.3 closing requirement — sub-block wall-clock emission + re-grade of the channel the dispersal draw exists to defend, which no block-resolution re-run can see — landed in its harness-timestamp form: spec + a-priori bound (`r = P(link)·(N−1) < 2`, sibling identification) committed in the doc one commit ahead of the grading code (§3.5 ordering), then `wallclock_leg.rs` models the wallet cadence phase (`T = 60 s`, mirror of `DEFAULT_PSCAN_CADENCE`) with the live driver's `bounded_uniform(bound_ms − 1)` dispersal shape and grades a circular-phase observer. Measured (1 000 trials/row): production dispersal `U[0,T)` at `r ∈ [0.84, 1.32]` across `m ∈ {1,4,12,52}` and `N ∈ {10,20,50}` — **PROVISIONAL-PASS**; the sub-`T` cliff is steep (`T/4` fails at every `m`, `T/2` from `m = 4`), so the production range is load-bearing, not slack; `2T` confirms over-dispersal is a no-op. Controls bite (phase-lock caught at `P = 1.000`; severed-wallet coupling tripwire at chance) and the §19.5 observer-strength tripwire is armed (`T/2, m = 52` correctly fails at `r = 5.82`). The leg's verdict joins the `--gf7-timeline` criterion-9.9 **computed conjunction** — an invalid leg grades the whole run INVALID. The finer-hook alternative was rejected (hooks §3 wall-clock payload ban stays intact) with a §19.1 reopening criterion; leg (b) **closes** only at the §19.1 sealing form (receipt-timestamped live-driver re-run), the named open carrier in `FOLLOWUPS.md`. - **sim: §14.4 partition-adversary arm implemented in `shekyl-staking-sim` (`ARCHIVAL_BOND_WI4_MEASUREMENT.md` §14.4; WI-4/GF-7).** Co-first deliverables landed before any grading run per the R4/R5 binding ordering: the **gating lemma** (a-priori absolute i.i.d. founder-clustering bound `M·((c+1)/(W+1))^{M−1} = 3.5393e-5` at `M=5, c=30, W=600`, exact-CDF domination test, no user-cohort reliance) and **compile-time witness-typed controls** (`ControlWitness` is a distinct type with no constructor or conversion path from the production founder-config surface — the five marked controls are un-constructible from production constructors at the type level, §17.7 finding 4). New `partition_adversary.rs`: named feature dictionary (11 posture-conditioned observables per pair), family members 1–7 (k-swept clustering, per-feature outlier, most-cohesive subset, spectral partition, seeded nearest-neighbor expansion, joint-density isolation, most-regular-subset repulsion detector), max-statistic `T` with Jaccard label agreement, permutation-calibrated null, and the five marked controls M-a…M-e. The first graded run surfaced a bound-2 under-specification on the single-member-targeted controls (M-b/M-d), initially pinned; review escalated it to a **detector-architecture finding** — the T-site fact verified at source shows the family-max null re-maximizes per relabeling (≈ 0.66 at `N=10`), rendering lone member-6/7 (Q2-A/Q2-B) signals structurally invisible and a `T`-only bound 1 potentially false-clean. Resolution (§14.4 UPDATE block): an **any-member arm** — per member vs its own null-of-the-mean, exact rank p-value, Bonferroni `α/7` at a-priori `α = 0.05`, with a `perms` resolution guard and `trials = 500` power-sized a-priori to the M-b hypergeometric effect ceiling. Deployed bound 1 requires both arms (family-max at chance and no member flags); whole-set controls retain family-max `+0.30` plus the aimed-member flag; single-member controls grade on the aimed-member flag. Re-graded at `N=10` (500 trials × 200 permutations): deployed clean on both arms, all five controls bite their aimed member — **PARTITION-PASS**, no pins remaining. New `riders.rs` (same round): §16.7 **N-sweep** (`r < 2` holds at every `N ∈ {10, 20, 50}`), **M6.2 coupling control** (blind arm at window 0 must stay ≥ `2/N`; measured `0.231 ≥ 0.200`), **attribute-stratified bridge grading** (holdings→period coupling: deployed at null `0.110` vs `0.115`, positive control bites at `0.985`), and the **lifetime-accumulation sweep** (`1 − (1−p)^events` at the 52-event long-lived-founder anchor). Trackers updated (`FOLLOWUPS.md` WI-4 entry, `IMPLEMENTATION_INDEX.md` WI-4 row, §14.4 status lines). - **docs: WI-4/GF-7 review-closure ratified — distinct-position round R4 lands §17.6 (`ARCHIVAL_BOND_WI4_MEASUREMENT.md`).** The §16.10 questions 2 (partition-family completeness) and 3 (`K_COVER` cycle break) adjudicated by the standing review: six constructions (four landed, two failed, no bound moves), six premise-strengthening findings. §14.4 family grows to seven members (joint-density isolation; most-regular-subset repulsion detector) and five witness-typed controls (M-d correlation-breaking, M-e repulsed/scheduled); a named a-priori **gating lemma** (absolute i.i.d. founder-clustering bound, no user-cohort reliance) becomes the implementation round's first deliverable. P2 reworded (statistical staggering only) with three companion premises (timing-law `K`-invariance, count `K`-independence, founder-bond persistence — lifecycle-verified: offline forfeiture and slashing are involuntary removal paths; the persistence premise is withdrawn at R7, below). §16.2 obligation 3: strip row elevated to primary at gate-open (worst-case zero-pre-gate-cover floor, P3 de-load-borne); cohort-dynamics row rests on the monotone floor with named non-existence behavior. Axis completeness recorded (timing ∪ loud-but-constant amount ∪ SP-T isolation; founder-tier residual named). **§§17.1–17.6 ratified together (2026-07-10); §§14–16 closed; the §14.4 implementation round is released.** Trackers updated (`FOLLOWUPS.md` WI-4 entry, `IMPLEMENTATION_INDEX.md` WI-4 row). Round R5 (§17.7, same day) adjudicated the R4 landing itself under the §17.6-head lens (structural, generator-invisible channels): tier residual promoted to a named channel with per-channel strip floor (`bond_floor` satisfiability verified at source — shard-descriptor choice, never treasury wealth); monotone floor recorded snapshot-by-construction (gate reads `frozen_shard_count` only) with an a-priori induced-forfeiture thinning margin required; premise-2 OR struck (fixed pre-gate schedule only at gate-open); control witness type a compile-time co-first-deliverable. Round R6 (§17.8, same day) adjudicated the R5 landing and **closed §17**: the R5 thinning margin was sized for the transient (restoration-lag) window while the cumulative-trigger/live-property divergence is permanent — verified at source that `frozen_segment_count` has no bond-exit decrement path (`segment_freeze.rs`), the derive-don't-cache exception recorded as deliberate (a live-cover sensor is the M2 flood-then-withdraw DoS surface), P2's persistence commitment reshaped to a **maintenance commitment** (replace attrited personas within a named lag), the margin re-derived against the detection-plus-replacement window, and maintenance breach named as the irreducible post-genesis residual. Thickness-reachability now runs against the union of per-channel floors with a **profile-commonality check** on the descriptor channel (fingerprint-not-zero-cover routes to redesign, never larger `K`); "fixed pre-gate schedule" pinned to {count, completion-before-gate} only (a schedule-fixes-times reading would manufacture member 7's repulsion signature). Round R7 (§17.9, 2026-07-11) reopened R6's landing and **re-closed §17 load-bearing**: the maintenance commitment was a ratified founder-differential mechanism produced by a category error — "founder" carried two referents, the anonymous founder-user population and the consensus-mechanized **Foundation backstop** (reward-invisible, Market-excluded; verified at source) — resolved by a §14-head terminology pin (three referents, three tokens; operator usages scrubbed). The commitment is withdrawn on two independent grounds (categorical: founders are users and owe no continuity; anti-masking: the obligation's coupling is masked only by population flow, thinnest exactly when the floor is load-bearing). A claimed exit-correlated-posting channel was refuted at source (epoch-quantized entry, `RELEASE_COOLDOWN_EPOCHS = 2` decorrelation headroom, no repost mechanism) and withdrawn. Finding 1 re-homed to population-level churn calibration: the §16.2 margin derives against **adversarial max net attrition over the full pre-gate window** (never expected churn); Q3-B re-seated on the same statistical basis. **Final Gate-7 gates are §17.9's** (adversarial-max-attrition margin; per-channel reachability); §17 reopens only against the termination bar — a construction defeating both the categorical rule and the substrate's decorrelation. - **daemon RPC: Axum now enforces `--rpc-max-connections*` + reports a live `rpc_connections_count`.** A purpose-built Rust connection layer (`shekyl-daemon-rpc::conn_limit`: `LimitedListener` + `ConnTracker` over `axum::serve`'s `Listener`) admits each inbound connection against the total and per-IP caps (`--rpc-max-connections` = 100, `--rpc-max-connections-per-public-ip` = 3, `--rpc-max-connections-per-private-ip` = 25 by default; `0` = unlimited), closing connections over a cap; slots release on connection close. The same tracker's live total is injected into `get_info.rpc_connections_count` (REST and json_rpc, unrestricted only), replacing the hardcoded `0`. Caps flow from `core_rpc_server::init` (still parsed/cross-validated in C++) through the `shekyl_daemon_rpc_start` FFI. This lands the two connection-management items the epee-listener deletion deferred; see `DAEMON_RPC_RUST.md`. ### Removed - **daemon RPC: epee HTTP listener + `--no-rust-rpc` deleted (Phase 1 transport cutover; `DAEMON_RPC_RUST.md`).** Axum is the sole daemon HTTP transport. `core_rpc_server` no longer inherits `epee::http_server_impl_base`; MAP dispatch macros are gone (FFI tables only). shekyld does not register inbound `--rpc-login` / `--rpc-ssl*` (unrepresentable; wallet-RPC keeps the full set). CORS default-deny; `--rpc-access-control-origins` honored when set. Dead decoy surface removed: `get_output_distribution` (+ `.bin`), `wallet2::get_rct_distribution`, and `wallet_errors.h::get_output_distribution`. Deleted `tests/rpc_comparison/compare_rpc.sh`. Opportunistic RPC SSL key autogen on the daemon path is gone with the epee listener. (Connection-limit CLI args were inert in this Phase 1 cut; they are now enforced by Axum — see the Added entry above.) - **consensus: pre-activation staker-inflow burn leg deleted (rule 60; `ARCHIVAL_BUDGET_SCHEDULE.md` §2.1 retirement, `REWARD_EMISSION_E3_GATING_ROUND.md` §9.9 landed entry).** The connect-site redirect's `else` branch — destroy `staker_inflow` when the connecting block's version predates archival emission — was unreachable by construction: emission is a genesis fact (`HF_VERSION_ARCHIVAL_EMISSION == 1`, block-version floor 1, versions never decrease), and the leg's lineage is the scrapped direct-distribution model's conditional no-staker burn (`cebbe017b`), carried forward as a placeholder (PR-4) and then version-gated (C-1 block 1) without ever becoming reachable. Deleted with it: - `HF_VERSION_ARCHIVAL_EMISSION` (`cryptonote_config.h`) — both live reads were tautologies at version floor 1. The redirect gate is gone (`blockchain.cpp` now accrues unconditionally per non-genesis block, same operands: verify's `base_reward`, `bl.major_version` into the split/fee-burn); the raw-import guard (`blockchain_import.cpp`) now refuses **all** non-genesis blocks unconditionally, since every block carries emission-era bookkeeping raw import cannot reproduce. - The conservation KAT's burn-leg expectations and `static_assert(HF_VERSION_ARCHIVAL_EMISSION <= k_post_fork_version)` (`tests/core_tests/archival_budget_conservation.{h,cpp}`) — expectations now unconditional (whole inflow in the accrual row, zero burn row in the fee-free fixture); the labeled-row form stays as the guard against inflow mis-routed into the burn row, and the v1→v2 fork table stays because `major_version` remains a live split/fee-burn operand and pop/reconnect replays the boundary. - The unit-level straddle framing: `archival_substrate_lmdb.cpp` KATs reframed as `budget_burn_accrual_partition_and_conservation` (fee-burn rows and accrual rows coexisting at one height; close sums only the accrual side) and `budget_reorg_pop_symmetry` (pop symmetry for both tables) — the storage properties they pin survive the leg. - CI tripwire (`scripts/ci/check_archival_reward_gates.sh`) re-anchored from the redirect block to the accrual block, same banned symbols (`get_block_reward`, `get_current_version`, `get_ideal_version(`) — F-B1b's version-operand discipline outlives the branch it originally selected; arming re-verified. A future post-genesis activation-gated inflow change re-derives the redirect discipline from §2.1's historical record rather than resurrecting the code. ### Added - **docs: epee HTTP listener recorded as deletion-scheduled; `DAEMON_RPC_RUST.md` brought current (2026-07-09).** A liveness check during claim-builder PR 1 (the dual-dispatch-map registration question) confirmed the epee HTTP listener no longer binds in a default daemon: Axum owns the standard RPC port, a failed Axum bind is fatal, and the epee acceptor binds only under the transitional `--no-rust-rpc` flag. `docs/DAEMON_RPC_RUST.md` corrected accordingly (single-listener architecture and port table replace the stale dual-server `+10000` mode; the curve-tree-endpoints-404 section updated to reflect the PR #174 resolution; dispatcher counts refreshed; the validation-results section marked historical). The listener, the flag, the epee dispatch macros in `core_rpc_server.h`, and the dual-server comparison harness are now a named deletion target — `docs/FOLLOWUPS.md` "epee HTTP listener + `--no-rust-rpc`: delete", target V3.0, with the outstanding wallet-sync-over-Axum check folded in as the deletion PR's verification gate (transport only; epee KV serialization and Levin P2P remain separate migration phases). - **wallet: emission claim assembly module — claim-builder PR 2 (`EMISSION_CLAIM_BUILDER.md` §2 steps 1/2/5/7, §8 PR 2).** New pure module `shekyl-engine-core/src/engine/emission_claim.rs`: everything between the decoded claim source and the signed transaction that is deterministic given the source — KAT-able with a fixture `EmissionEpochSource`, no live daemon, no actor harness. `derive_claimable_epochs` consumes **five** boundaries and never re-derives boundary arithmetic: top/expiry/dedup through the read-only predicates (`epoch_is_not_settled` — extracted in this PR's review round, retiring an interim scratch-clone-oracle disposition and discharging its V3.1 FOLLOWUPS item on the named trigger — plus the existing `epoch_is_claim_expired` / `claimed_epochs_contains`; the connect mutator resolves through the same predicates, pinned by a differential test), **join** via the newly extracted `epoch_is_before_join` (verify step 2's `E ≥ E_join + 1` against the record's join epoch — a review finding: without it a retire-then-rejoin record wedged the whole batch behind the cause-blind `SelfCheckFailed`), and **strict finalization** via verify's own `epoch_close_height` predicate (a window epoch with no close height at all is refused `SourceInvalid` — it can never finalize). The share recompute is the shared steps-4/5 evaluation head `claimant_reward_share`, called by the verify body and the derivation alike (never a mirrored copy). The finalization boundary is a review finding: an earlier revision claimed the missing-close-row skip foreclosed the edge — disproved at daemon source (the close of `E` runs while connecting `E`'s last block, `blockchain_db.cpp` `add_block`; gather height and `settled` come from one `db.height()` read, `archival_claim_source.cpp`), so at `chain_height == h_close(E)` the connect window admits `E` while verify rejects it next-block — a once-per-epoch spurious-refusal window, closed by deferring one count (`EpochSkip::NotFinalized`, premise-asserted boundary KAT). Claimable ⇔ recomputed share `> 0` through the verify-exact chain (discharges the M1 round-1 forward obligation; no `K_COVER` read anywhere in the module; zero-share skip cause-blind by construction). Assembly emits the canonical minimal rows (credited shards only, ascending, `serve_credit_bit = true`), with the `u64→u32` scarcity conversion armed at the builder's site (compile-time const-assert as the production guarantee, checked `try_from` refusal + hostile-fixture KAT as non-production defense) and the entry-sum-vs-`work_P` hard check; sizing measures the claims leg with the **production encoder** at structural maxima — on the one vin the assembly's content is returned from (`claims_vin`, the single construction site) — against the **binding per-tx relay bound**, the new `shekyl_wire::TX_WEIGHT_LIMIT` (149 400 B, pinning the C++ `get_transaction_weight_limit`; a review finding re-based this off `MAX_TX_SIZE`, the non-binding 1 MB parse cap, which could pass a permanently-unsubmittable batch) minus a re-derived 48 KiB non-claims reserve, dropping youngest-first (bound-or-split, GF-4b §5 item 5) and refusing when one epoch cannot fit. The PR-1 decode now also enforces the settled/height invariant (`current_settled_epoch == settlement_epoch_at_height(chain_height)`, a `Malformed` refusal otherwise), so the derivation's window boundaries and the self-check's recomputed boundaries provably share one basis. The step-7 self-check (`self_check_claims`) drives the literal `emission_vin_verify_claims` on the assembled rows before any signing — a builder/verifier disagreement surfaces loudly at build time as `SelfCheckFailed`, **cause-blind** per the CB-5 builder pin (unit variant; verify detail log-local only), with a differential KAT proving each of six vin mutations and three context arms flips accept → refuse. CB-5 refusal taxonomy landed minus `InsufficientBacking` (PR 3, with its constructor). Rides along: the dead `shekyl_engine_core::error::EngineCoreError` module deleted (separate commit; FOLLOWUPS item closed — zero live consumers re-verified against the live tree). - **rpc: emission claim-source query — claim-builder PR 1 (`EMISSION_CLAIM_BUILDER.md` §7 / §8 PR 1).** New daemon JSON-RPC `get_archival_emission_claim_source` (both transports; RPC minor bumped 17→18): request carries `p_id` **only**, response carries the part-A claim context (bond record as `get_archival_bond_value` returns it) plus one as-of-`E` snapshot per epoch in `[claim_window_floor(settled), settled − 1]`, unconditionally. The marshal helper (`src/rpc/archival_claim_source.cpp`) is a serializer over the single landed gather in its windowed form (`gather_archival_emission_window_snapshots` — same one row-selection routine, all window epochs in ONE serve-credit table pass, so the unauthenticated endpoint costs one scan per request instead of 26) under one `db_rtxn_guard` read view; the window's low end resolves through a new thin FFI delegate `shekyl_archival_claim_window_floor` (the one landed boundary definition, per §2 step 1's consumption-not-re-derivation pin). The archival LMDB read helpers dropped their bare `if (m_write_txn)` fast path for the thread-aware `block_rtxn_start` selection — the RPC (the first cross-thread caller) can no longer touch the writer thread's live write txn. Wallet-side decode (`shekyl-engine-core/src/engine/emission_source.rs`) produces the verify-side `EmissionEpochSource`/`ClaimantBondRecord` views via the new single-sourced `EpochCloseInputs::verify_view` constructor (also adopted by both verify FFI shims — the construction previously existed as three hand-aligned struct literals), and enforces the untrusted-daemon decode contract loudly: `status == "OK"` required (a `BUSY` body must not decode as "nothing claimable"), `claimed_settlement_epochs` strictly increasing (the binary-search operand), window epochs strictly ascending. Tests pin the two round-2 watch items mechanically: LMDB operand fidelity against a direct gather (§7.1 single-gather trace, now also proving windowed ≡ per-epoch gather), identical window shape for bonded and bond-less claimants + request-member-count == 1 (§7.2 transport cause-blindness), plus epee wire round-trip, the `SIZE_MAX`→`u64::MAX` no-credit sentinel, and the omit-empty-container behavior the Rust decode's absent-equals-empty rule relies on (`tests/unit_tests/archival_claim_source_rpc.cpp` — fixture single-sourced with the substrate emission KAT via the new `archival_lmdb_test_helpers.h`; Rust decode unit tests in-module). - **docs: emission claim-builder design round 2 opened and CLOSED (2026-07-09, `docs/design/EMISSION_CLAIM_BUILDER.md` §7–§8).** The CB-1 ratification residue: the daemon RPC surface + the implementation PR chain. Ratified: §7.1 single-gather pin — the RPC serializes the landed `gather_archival_emission_epoch_snapshot`, recorded as **CB-1(b) itself, daemon-side face** (one invariant, two faces: wallet doesn't re-derive, RPC doesn't re-gather; weakening either reopens CB-1); §7.2 one composite window-batched query (request = `p_id` only — the query cannot encode the claimable subset; CB-5 cause-blindness at the transport); §7.3 field enumeration with four load-bearing exclusions, the `k_cover`/`frozen_shard_count` exclusion recorded as **CB-5-structural** (sending the gate operands would make representable the cause-distinguishing branch CB-5 makes unrepresentable), not payload-minimization; §7.4 gate-6 review (persona transport pin, query timing routed into CB-3's GF-4 joint grade). §2 step 1's claimable range corrected against the rejection predicates: top strict (`E ≥ settled` rejects `NotSettled` — a round-1 off-by-one against source), bottom verified clean-for-a-structural-reason (`epoch_is_claim_expired` is single-sourced through `claim_window_floor`) and pinned to predicate-function consumption, never inline boundary arithmetic. §8 four-PR chain endorsed (daemon RPC → pure assembly → StakeEngine handler → regtest e2e + blob-boundary arm, PR 4 gated on #281's harness), with two grep-checkable PR-1 watch items named at closure. - **wallet-rpc: Phase 4b send lifecycle** (`build_pending_tx`, `submit_pending_tx`, `discard_pending_tx`). Async Engine methods; `SendError` / `SubmitError` / `PendingTxError` map to `-29100..-29107`. Discard is idempotent. `rescan_blockchain` remains `-32601` (FOLLOWUPS). Phase 4b SPECIFIED methods complete except rescan. - **wallet-rpc: Phase 4b refresh** (`refresh`). Calls `Engine::refresh` with default `RefreshOptions`; maps `RefreshError::AlreadyRunning` → `-29200` and daemon/scanner IO → `-29201`. `rescan_blockchain` stays `-32601` until Engine grows an explicit rescan API (`docs/FOLLOWUPS.md`). - **wallet-rpc: Phase 4b read queries** (`get_balance`, `get_primary_address`, `get_transfers`, `get_transfer_by_id`, `get_height`). Typed OpenAPI result structs + ledger projections (no secrets on the wire). Empty-wallet balance is zeros; unknown transfer id → `-29400`; daemon height failure → `-29201`. - **wallet-rpc: Phase 4b lifecycle slice 1** (`WALLET_REWRITE_PLAN.md` Phase 4b; `docs/api/wallet_rpc.yaml`). `create_wallet`, `open_wallet`, `close_wallet`, and `change_password` on `rust/shekyl-wallet-rpc`, Engine-backed (`Engine::`). Tenant holds the open Engine; CLI gains `--daemon-address` and `--network`. Create returns one-shot backup material (`mnemonic` on mainnet/stagenet; `raw_seed_hex` on testnet). Error mapping covers `-29000..-29005`. Remaining SPECIFIED methods still return `-32601` until later 4b slices. - **tests: end-to-end archival budget conservation KAT — the C-1 fast-follow (`REWARD_EMISSION_E3_GATING_ROUND.md` §9.9; `ARCHIVAL_BUDGET_SCHEDULE.md` §7 B1's connect-path counterpart).** New chaingen core test `archival_budget_conservation_boundary` (`tests/core_tests/archival_budget_conservation.{h,cpp}`) drives the real connect path (`handle_block_to_main_chain`) across a v1→v2 fork table and asserts the §2.2 conservation identity per block in **labeled-row form**: accrual row and burn row each checked against the block's own `major_version` — the only form that catches the branch-selection class (F-B1b: wrong burn-vs-accrue branch with byte-identical operands, invisible to operand KATs and the grep tripwire, and sum-invariant because the redirect is a pure destination switch). Also asserts the genesis exclusion, coinbase = `miner_emission`, ledger advance = an independently recomputed modulated subsidy, and byte-identical row restoration after pop/reconnect across the fork boundary. Arming-verified (inverted redirect branch fails at height 1). Wired into CI as the `conservation` subcommand of `run_economics_c2a_prime.sh` with its own workflow matrix entry; the workflow's paths filter covers the KAT's full guarded surface (`src/blockchain_db/**`, `src/shekyl/**`, `src/cryptonote_config.h`). Genesis-posture caveat recorded in the test header: `HF_VERSION_ARCHIVAL_EMISSION == 1` leaves the burn leg compile-alive but unexercised; a post-genesis activation bump to the fixture's top fork version (2) arms it through the same fixture with no test change, and a `static_assert` forces a fork-table extension for bumps past that (no silent de-arming). Fee-leg caveat (disclosed in the test header): the fee-free fixture exercises only the emission half of `staker_inflow` end-to-end — the fee-pool half (`staker_pool_amount`) rides the E4/E5 regtest-e2e residue (chaingen cannot build valid FCMP++ fee txs), guarded meanwhile by the unit-level B5 KATs and the F-B1c-c1 operand pin. Review hardening (same branch): the block-injection helper, independent subsidy recompute, and chain-extension scaffold are single-sourced in `tests/core_tests/economics_chain_helpers.h` (shared with the Layer 3 harness), the helper now cleans up the incoming-blocks batch on the failure path and requires main-chain placement for success, and the fail-closed core_tests CI gate is one parameterized function. Closes the FOLLOWUPS V3.0 conservation-KAT item (fee-leg residue carried as above). - **docs: emission claim-builder design round 1 opened (`docs/design/EMISSION_CLAIM_BUILDER.md`).** The E4-gate part-1 work unit after C-1 (#277) landed: the wallet-side builder that assembles authed `txin_archival_reward_emission` transactions — the piece the Track-2 regtest e2e (E4-gate part 2) is blocked on. Spec: seven-step assembly over landed substrate only (verifier-exact positive-share recompute for claimable-epoch derivation; backing exclusively through `BackingSet` at arity 1 per GF-4b §5; `prove_membership_only`; the shared `auth_msgs` builder + dual hybrid sign; build-time self-check against the landed verify functions), the GF-4b §5 / M1 inherited-obligation discharge table, and round-1 questions CB-1…CB-5 with CB-1 (archival-state recompute sourcing: new daemon RPC vs wallet-side re-derivation vs regtest shortcut — recommended (a), the RPC shaped as `EmissionEpochSource`) as the gating decision. `IMPLEMENTATION_INDEX.md` registers the `CB-` family and refreshes the stale #263/#264/#277 "in review" rows to landed with code-anchored verification stamps. **Round 1 CLOSED (2026-07-09) — Round-1 dispositions + CB-4 tripwire strengthening:** CB-1…CB-5 all ratified at source with two builder-PR pins (sign within `StakeEngine`'s seed-gone-after-`assemble()` discipline; `SelfCheckFailed` refuses cause-blind) and CB-3's joint-grading reopen (GF-4 grades cadence + amount + holdings stratum jointly). The CB-4 source pass found the Q11 balance-exclusion arming KAT (`tests/unit_tests/archival_emission_ct_balance.cpp`) was necessary-not-sufficient: the exclusion is enforced by a **structural three-leg guard** (backing in the vin's opaque `canonical_bytes` never deserialized into `pseudoOuts`; dispatch + leaf size checks at `tx_verification_utils.cpp:175` + `rctSigs.cpp:362`), and CB-4 ratifies on that guard — not on the KAT. Fixed **in this PR** (cheap, in-surface, known-wrong): arm 1 strengthened to derive operands through the **production** functions the dispatch uses (`classify_archival_tx` + `shekyl_checked_sum_amounts`, replacing a test-local mirror), arming the guard's operand + leaf legs; the E3 §2.2 rationale corrected (its claimed identity-catch was green-by-construction — the backing never reaches `verCtSemanticsEmission`); and a general test-doc discipline pinned in `50-testing.mdc` ("Test rationales state their coverage boundary"). The remaining **blob-boundary invariant** arm (drive the full dispatch, assert CT-balance verdict invariant under `canonical_bytes` variation) is harness-gated and homed to the claim-builder PR where the valid-proof emission-tx harness is born (`FOLLOWUPS.md`) — tripwire-completeness, not a consensus change. ### Fixed - **wire: `Transaction::validate()` pins the Bp+ `|L|`/`|R|` round count by the output count (consensus-parity gap, `docs/FOLLOWUPS.md` flagged 2026-06-24).** A tx with a valid output count but an inconsistent/oversize Bp+ `|L|` parsed and passed local `validate()` while the C++ daemon rejects it — a local↔daemon divergence surfacing at submit. Mirroring the C++ split: `BpPlus::read` now rejects `|L| ∉ 6..=MAX_BP_LR_LEN` and `|R| != |L|` (the deserialization-time `n_bulletproof_plus_max_amounts` rejects), and `validate()` enforces `|L| == |R| == 6 + ceil(log2(next_pow2(n_out)))` exactly (parse-time `n_padded >= n_out` + verify-time V/L tightness, `V` restored from `outPk`). `weight()`'s overflow clamp stays for hand-built, not-yet-validated txs. - **consensus: C-1 emission review hardening (PR #277 review, `REWARD_EMISSION_E3_GATING_ROUND.md` §9.5).** Batch of correctness/robustness fixes from the high-effort review of the reward-emission activating cut: - *Raw unverified import fails closed on emission-active blocks.* The `--dangerous-unverified-import` path called `add_block` with `archival_budget_accrual = 0`, so the epoch close froze a present-and-zero (structurally non-claimable) `budget(E)` for every epoch — an import-synced node would then *reject* live emission txs the network accepts (a silent consensus split, not the bookkeeping parity the old comment claimed). Since emission is active from genesis, raw import cannot produce the accrual (verify's `base_reward` + fee summary) without re-deriving the operands the connect path deliberately single-sources (F-B1b); it now aborts on any non-genesis block with a message pointing to verifying import. Whether the fast unverified path is worth re-deriving the split for is a testnet-stress-test question. - *Zero-plaintext-vout ban gated on the full emission classification.* `check_tx_outputs` skipped the v3 zero-amount-vout ban on a bare `emission_vins == 1` count, which also exempted a malformed tx pairing an emission vin with a bond-post; it now uses the same strict classification as `check_tx_inputs`. - *Archival tx-kind predicate single-sourced.* The bond-post/emission/serve-credit taxonomy was open-coded in `check_tx_inputs`, `check_tx_outputs`, and `ver_non_input_consensus`; it now lives once in `classify_archival_tx` (`cryptonote_basic.h`), so the three consensus sites cannot drift on what a tx *is*. - *Emission-vout storage predicate single-sourced.* The store (`add_transaction`) and remove (`remove_tx_outputs`) sides of the amount-0 emission-vout treatment now share `tx_has_archival_emission_vin`, so a pop cannot disagree with the add. - *CT-balance tail de-duplicated.* `verCtSemanticsEmission` and `verRctSemanticsBondPost` shared a near-verbatim flatten + balance-FFI + Bulletproof+ tail; extracted to `verArchivalCtBalanceAndRange`, called with each path's (credit, debit) operands. - *Emission reward-sum arithmetic moved to Rust (rule 20).* The hand-rolled C++ overflow-checked vout-amount summation (two copies) now routes through `shekyl_checked_sum_amounts`, single-sourcing the inflation-audit operand across `check_tx_inputs` and the CT-balance shape check. - *Emission wire tag named.* The bare `0x04` literal at three C++ sites (transport guard, `VARIANT_TAG`, JSON echo) is now `TXIN_ARCHIVAL_REWARD_EMISSION_WIRE_TAG`, mirroring Rust's wire tag. - *Stale dispatch comment corrected:* the C-1 emission branch is LIVE from genesis (the whitelist flip landed in this cut), not the "gate-last / unreachable" the old comment claimed. Two review items were assessed as refuted, not deferred: the block-level (P,E) re-parse is load-bearing (the sole parse on the `PER_BLOCK_CHECKPOINT` fast path, where `check_tx_inputs` is skipped), and the `canonical_bytes` container serializer has no pre-allocation DoS (`remaining_bytes() < cnt` guard + no-op reserve) — changing it would be a rule-42 wire change for a micro-opt. The FFI-surface ergonomics finding (grouping `shekyl_emission_vin_verify`'s 22 positional args into `#[repr(C)]` structs) is a separate follow-up on its own validation surface. - **consensus: redirect keys on bl.major_version + CI tripwire (F-B1b hardening, `REWARD_EMISSION_E3_GATING_ROUND.md` §9.9, `ARCHIVAL_BUDGET_SCHEDULE.md` §2.2).** The burn-vs-accrue decision read `get_current_version()` — correct pre-`add_block` (verified at source: the previous block's `HardFork::add` advances to the connecting block's own voted version, and `m_hardfork->check(bl)` enforces `bl.major_version == get_current_version()` upstream), but resting on the height+1 advance convention §2.2 pins against, and keyed to a different height notion than the split it gates. Now keys on `bl.major_version` — the block's own consensus-checked version, explicit and convention-free. New grep tripwire in `scripts/ci/check_archival_reward_gates.sh` (consensus-invariants gate) fails on `get_block_reward` / `get_current_version` / `get_ideal_version(` inside the redirect block, with positive anchor checks so block moves can't silently retire it; verified armed. The end-to-end conservation KAT (activation-boundary fixture, core-tests harness) is the prioritized fast-follow — `docs/FOLLOWUPS.md` V3.0. - **consensus: budget emission leg uses verify's modulated base_reward (F-B1c-c2 disposition (a), `REWARD_EMISSION_E3_GATING_ROUND.md` §9.9, `ARCHIVAL_BUDGET_SCHEDULE.md` §1).** The staker-inflow emission leg derived from the 5-arg `get_block_reward` (no release multiplier, no weight penalty) while the coinbase and the supply-ledger advance use the 6-arg modulated `base_reward`. The unmodulated leg predates C-1 and was benignly deflationary as a burn (over-destroying); the C-1 redirect inverted its sign — an over-sized accrual makes the excess re-mintable through emission claims, coins `already_generated_coins` never counted (an inflation surface, merge-blocker class). Both legs (pre-activation burn, post-activation accrual) now split verify's `base_reward` itself: conservation by construction (`base_reward = coinbase + staker leg`), and the redirect becomes a pure destination switch — same quantity, burn-vs-accrue on the block's own version. Genesis computes no inflow (its hardcoded emission is paid whole by the genesis coinbase). Disposition (b) (demand-insulated budget) rejected with a rule-21 sim-gated reopen — see §9.9 and the `docs/FOLLOWUPS.md` V3.0 item. - **consensus: budget accrual ordering and redirect operands (F-B1a / F-B1b / F-B1c-c1, `REWARD_EMISSION_E3_GATING_ROUND.md` §9.9, `ARCHIVAL_BUDGET_SCHEDULE.md` §§1–3).** The staker-inflow accrual row was written after `m_db->add_block` returned — after the epoch-close hook had already range-summed the epoch — so every `budget(E)` dropped its final block's inflow (F-B1a, per-epoch under-mint and a §2.2 conservation violation). The redirect gate read `get_current_version()` post-add, the *next* block's version, flipping the redirect one block early at an activation boundary (F-B1b). And the accrual site called `compute_fee_burn` with `tx_volume = 0`, zeroing the fee-pool half of the inflow and the destroyed share's burn record (F-B1c-c1). Structural fix: the computation moved before `add_block` (block's own validated version; verify's exact `get_tx_volume_avg` operand), the amount rides into `BlockchainDB::add_block` as a new `archival_budget_accrual` parameter written before the connect hooks, and the pop-side removal moved into `BlockchainDB::pop_block` (same layer, same wtxn). New production-path KAT (`budget_epoch_boundary_includes_final_block_through_real_block_path`) crosses the epoch-0 boundary through real `add_block`/`pop_block`, verified armed against the F-B1a ordering. The emission-quantity operand drift (F-B1c-c2, 5-arg vs 6-arg `get_block_reward`) was initially parked pending spec adjudication and is resolved by the entry above. - **consensus: emission connect-arm height operands (F-B5a / F-B5b, `REWARD_EMISSION_E3_GATING_ROUND.md` §9.8).** The emission arm in `BlockchainDB::add_transaction` derived its height via `get_block_height(blk_hash)` before the block's `block_heights` row exists, throwing `BLOCK_DNE` on every block carrying an emission vin (F-B5a) — it now reads `height()`, the connecting block's index and verify's pin-(b) operand. `pop_block` passed its post-block chain height (N+1) to the claim-journal revert keyed on the block index N, making the revert a silent no-op that left the claimed-epoch set standing across a pop (F-B5b) — it now reverts at `removed_block_height - 1`. New real-LMDB KAT (`emission_connect_pop_roundtrip_through_real_block_path`) drives the shared fixture vin through the real `add_block → pop_block` machinery and was verified armed against each finding independently; the fixture's claimed epochs moved to {1, 2} to keep the chain build short. - **consensus: bond-post connect arm carried the same latent `get_block_height`-before-write crash (F-B5a sibling, pre-dating C-1)** — same `height()` fix, so a JoinMarket bond-post vin connects instead of throwing `BLOCK_DNE`. New real-LMDB KAT (`bond_post_connect_pop_roundtrip_through_real_block_path`) connects a bond-post through the real `add_block` path, pins the join epoch to the real connect height, and pops it back off. ### Added - **sim: F-B1c-c2 disposition-(b) reopen evidence + closure — swing tolerable, (a) stands permanently (`REWARD_EMISSION_E3_GATING_ROUND.md` §9.9 reopen-resolution addendum).** Two companion sim modes produce the rule-21 reopen evidence the c2 adjudication gated on: `shekyl-economics-sim --fb1c-c2` measures the per-epoch `budget(E)` swing under disposition (a) against the demand-insulated (b) counterfactual across six volume regimes (throttle floors at exactly 0.8× — the `release_min` clamp, never zero; (b)'s worst-window uplift bounded at 25 % of the emission leg; the gap decays to ~nothing at the fee-era handoff), and `shekyl-staking-sim --budget-throttle` cross-checks coverage on the L11 `budget → APR → entry → coverage` transfer curve, 8-seed-averaged, with the emis_frac-weighted effective multiplier (the fee leg is disposition-neutral). No real operating point materially degrades: the deep-throttle regime (mining era, saturated purse) and the coverage-knee regime (fee era, emission leg decayed to ~1 % of budget) are disjoint under the 0.90/yr share decay — the honest counterfactual (full 0.8× on a lean purse, a regime that does not occur) does cross the knee, so the recorded safety is regime separation, not throttle harmlessness. Reopen closed: FOLLOWUPS V3.0 item moved to the audit trail; consequence note updated in `ARCHIVAL_BUDGET_SCHEDULE.md` §1. - **docs: C-1 review closure recorded — blocks 3 and 5 pass at file:line; `IMPLEMENTATION_INDEX.md` emission rows updated to code (rule 94).** Block 3 (F-C1c `signable_tx_hash`, §9.6 pin): the emission vin is removed wholesale from a `transaction_prefix` copy (`blockchain.cpp:3889–3894`, true structural erase, not a zeroed placeholder), and the arity-1 classification gates the branch ahead of the hash by control flow (`:3388–3421` → `:3779`), with the whitelist's `emissions > 1` reject as the second independent layer. Block 5 (WS-2 journal, §6.3): the connect writer journals the full pre-mutation claim state before mutating (`db_lmdb.cpp:5748–5757`, same write txn — atomic), and the prune-straddle KAT's primary assertion is re-claim *rejection* after pop + rebuild (`archival_substrate_lmdb.cpp:1628–1631`), byte-identity the corollary. No findings; the C-1 consensus surface is reviewed end to end. The `IMPLEMENTATION_INDEX.md` §2 PR-E row and §5 emission-leg row now lead with **C-1 IMPLEMENTED — in review (PR #277)**, blocks 4–6 recorded (previously listed as "still missing"), residue named (wallet-side claim builder + regtest e2e, conservation-KAT fast-follow). - **consensus: Q11 balance-exclusion arming KAT — the ratified reopen trigger (`REWARD_EMISSION_E3_GATING_ROUND.md` §2.2).** New unit KATs (`archival_emission_ct_balance.cpp`) pin that the emission vin's membership-only backing pseudo-out is structurally excluded from the CT balance: `backing_identity_O_vs_O_prime` asserts two txs differing only in the backing commitment (backing-on-O vs backing-on-O′) balance bit-identically through the production `verCtSemanticsEmission` with dispatch-derived operands — catching the coordinated regression where the backing is summed and the `pseudoOuts.size() == fee_input_count` check is bumped in lockstep (the naive append already trips the size assert). `backing_inclusion_shape_rejects_in_production` proves the teeth: a fixture balanced *only* with the backing on the pseudo side rejects in production while the manually-included shape passes the underlying single-sourced balance FFI, so the rejection is the Q11 exclusion working, not an unrelated malformation. Flipping these tests is the §2.2 reopen signal, not a test to update in lockstep. - **economics: C-1 commit-block 6 — KAT B5 fee-bearing coinbase foreclosure; regtest e2e deferral recorded (§9.5 item 8, `ARCHIVAL_BUDGET_SCHEDULE.md` §7).** New unit KATs (`economics_b5_fee_coinbase.cpp`) drive the production decision site `validate_miner_transaction` directly (standing `IN_UNIT_TESTS` seam) with fee-bearing operands recomposed through the same single-evaluator helpers the check calls: exact `miner_emission + miner_fee_income` accepts (fix-α full-subsidy out-param pinned), coinbase additionally claiming `staker_pool_amount` or `staker_emission` rejects, and an underclaim rejects — the §2.2 coinbase-foreclosure pin's "exactly" is two-sided. Closes the fee-side gap the `economics_c2a_prime` tests leave (fee-free blocks). The connect-path block vehicle defers with the chaingen FCMP++ fee-transaction gap, and the §9.5 item 8 regtest e2e defers on the wallet-side claim builder (no production path can assemble an emission tx yet; a hand-rolled test builder would prove a parallel path) — filed in `docs/FOLLOWUPS.md` (V3.0 pre-genesis, E4/E5 gate) with the claim-builder landing as the reopening trigger. - **consensus: C-1 commit-block 5 — connect arm: emission-claim single writer + vout storage shape (§9.5 item 7).** `BlockchainDB::add_transaction` gains the `txin_archival_reward_emission` arm: re-extract `(P_canonical_id, settlement_epochs)` from the opaque blob through `shekyl_archival_emission_vin_extract` (the Rust codec is the only parser; an unparseable vin at connect is a hard error, never a soft skip) and hand them to `apply_archival_emission_claim` — the WS-2 single writer whose §6.3 pre-image journal and height-keyed `pop_block` revert are already armed, so the pop side needs no vin arm. Vout storage: emission reward vouts share the coinbase shape (plaintext amount in the tx — the loud mint — real Pedersen commitment in `outPk`, balance already verified by `verCtSemanticsEmission`), so both store as amount-0 RCT records with the commitment kept, keeping them FCMP++-spendable; `add_block`'s `num_rct_outs` counts them and `remove_tx_outputs` keys their removal on amount 0 (connect/pop symmetric). Mint accounting needs no new counter: the staker subsidy entered `already_generated_coins` at accrual (F-C1a), fee-derived budget is recycled circulation, and unclaimed budget is supply never created. KATs: deterministic connect fixture (`emission_connect_kat_v1.json`, length-canonical filler — extraction parses structure, no crypto) generated and drift-pinned by `emission_connect_kat.rs`; C++ side (`archival_emission_connect.cpp`) proves the arm extracts the fixture's operands and calls the single writer once with the connect height, stores both reward and change vouts amount-0 with their `outPk` commitments preserved, and hard-errors on a tag-correct garbage blob without reaching the writer. - **consensus: C-1 commit-block 4 — activation cut: whitelist flip, CT semantics, block-level `(P,E)` Rust-decided pass (§9.5 items 4 + 6 and the item-5 CT cross-checks).** `check_inputs_types_supported` now admits `txin_archival_reward_emission` under the Q3/Q11 gates — exactly one emission vin per tx, co-residence with key-imaged `txin_to_key` fee inputs only (no bond-post, no serve-credit mixing); the block-3 dispatch branch becomes live consensus. CT semantics: new `rct::verCtSemanticsEmission` (`fcmp/rctSigs.cpp`) verifies the emission balance `Σ pseudoOuts + total_reward·H = Σ out commitments + fee·H` by reusing the bond-post credit/debit balance FFI with `bond_credit = 0`, `bond_debit = total_reward` (the mint enters the equation as a debit-side credit), FCMP++ proof presence keyed to the fee-input count (absent when zero fee inputs), BP+ range proofs over all outputs; `ver_non_input_consensus` dispatches emission txs to it (overflow-checked plaintext `total_reward` sum, `pqc_auths` sized to all inputs). `check_tx_outputs` exempts the emission tx's non-zero plaintext reward vouts from the v3 zero-amount rule (they are the claim's public mint; change vouts stay CT), and `check_inputs_overflow` skips the amountless vin. Block-level `(P,E)` uniqueness (§6.2 third layer, item 6): the **decision** is Rust — `emission_block_claims_unique` (`claimed_epochs.rs`, sort + adjacent-compare over the block's flattened `(P_canonical_id, epoch)` pairs) via new FFI `shekyl_emission_block_claims_unique` — C++ in `handle_block_to_main_chain` only extracts each emission vin's pairs (reusing `shekyl_archival_emission_vin_extract`) and marshals; a duplicate verdict rejects the block (per-tx layers cannot see cross-tx duplicates pre-connect; without this the second claim double-mints inside one block). Key-image bookkeeping loops in `cryptonote_core.cpp` skip the key-image-less emission vin. KATs: the block-2 whitelist tripwire flips to admit-at-arity-1, plus two-emission-vins and emission×bond-post / emission×serve-credit mixing rejections (§6.4's dedup KATs), and Rust-side same-`(P,E)`-duplicate / distinct-pairs / FFI negative-surface tests. - **consensus/ffi: C-1 commit-block 3 — emission verify FFI + dispatch wiring (§9.5 items 3 + 5; gate-last, still dead code).** Rust side: the production `AuthVerified` minter `emission_vin_verify_auth` (`emission_verify.rs`) verifies both Q1 hybrid auths — Auth-B under `backing_pubkey` with the leaf-gate pin (`hash_pqc_public_key` must equal the committed `pqc_pk_hash`), Auth-P under `P_pubkey` — over the domain-separated `auth_msgs` binding messages; new `AuthMalformed`/`AuthRejected{role}` diagnostics. Two FFI entries in `archival_ffi.rs` (mirrored in `shekyl_ffi.h`): `shekyl_archival_emission_vin_extract` (pre-parse: `P_canonical_id` + claimed epochs, for C++ operand gathering) and `shekyl_emission_vin_verify` (the full §7.1 body — claims 1–5, membership-only backing 6, hybrid auth 8, auth-before-backing for DoS ordering — one coarse crossing returning verdict + `total_reward` + `epochs_to_commit`); `shekyl_archival_emission_epoch_snapshot` gains `budget_atomic`. C++ side: `ArchivalEmissionEpochSnapshot` + `gather_archival_emission_epoch_snapshot` lifted to the `BlockchainDB` interface (LMDB overrides; default backend rejects via `has_budget_row=false`), and the `check_tx_inputs` emission branch — classify (exactly one emission vin, `txin_to_key` co-residents only), fee-input pre-gates, PQC-slot binding (`pqc_auths[emission_index]`'s hybrid key must derive the vin's `P_canonical_id`), pseudoOuts sized by the fee subset, reference-block age windows + curve-tree context shared by both proofs, **F-C1c signable hash** (prefix hash with the emission vin removed wholesale — circularity exclusion, re-bound per the §9.6 table), per-epoch frozen snapshot gather (absent budget row rejects), ordered reward commit set (non-zero plaintext vouts: commitment ‖ amount LE ‖ one-time key), the coarse verify call, and step-7 fee-input FCMP++ verification (proof must be absent with zero fee inputs). **Gate-last:** `check_inputs_types_supported` still rejects the vin type — the whole branch is unreachable until the block-4 whitelist flip. FFI KATs cover extract/verify round-trips and the negative surface; the 79 C++ archival tests pass unchanged. - **docs: F-C1c pinned in `REWARD_EMISSION_E3_GATING_ROUND.md` §9.6 (for ratification)** — the emission vin's auth messages and backing proof bind `signable_tx_hash`, but the full tx prefix contains the vin itself (its own signatures in the preimage — circular). Pin: `signable_tx_hash` = prefix hash of the tx with the emission vin removed wholesale; every dropped property re-bound (Q1 fields 1–6, the field-7 commit set, tx-level hybrid auth over the complete prefix, fee-input FCMP++ over the unmodified `tx_prefix_hash`). - **consensus: C-1 commit-block 2 — `txin_archival_reward_emission` transport shim (dense tag `0x04`, F-C1b pin).** New vin variant in `cryptonote_basic.h` carrying the complete Rust canonical encoding as an opaque `canonical_bytes` blob — `emission_wire.rs` owns the codec, the parse, and every structural bound; C++ never reads inside the blob. Transport-layer checks only: `ARCHIVAL_EMISSION_VIN_MAX_BYTES` (1 MiB deserializer allocation cap, `cryptonote_config.h`) and the leading-byte echo of the Rust wire tag `0x04`, enforced identically on the binary serializer and the JSON/RPC entrypoint (`json_object.cpp`) so no admission skew between parsers (the PR #229 r3 lesson). Boost archive, JSON/debug `VARIANT_TAG`s, and the signature-size visitor (0 — auths live inside the Rust-verified blob) complete the surface; `check_for_double_spend` passes it through (no key image — emission dedup is the WS-2 journaled check-and-set plus the block-level `(P,E)` pass). **Gate-last:** `check_inputs_types_supported` still rejects the type (the block-4 flip is the activating cut), pinned by a standing tripwire KAT. Transport KATs in `archival_reward_emission.cpp`: dense-tag round-trip byte-identity, wire-tag-echo rejection on both serialize and deserialize, size-bound fail-closed, whitelist-still-rejects. - **consensus/db: C-1 commit-block 1 — `budget(E)` production per `ARCHIVAL_BUDGET_SCHEDULE.md` (ratified; F-C1a closed).** The redirect-the-write lands at the connect site (`blockchain.cpp`): the single per-height `staker_inflow` write switches target on the connecting block's own fork version (`HF_VERSION_ARCHIVAL_EMISSION`, genesis feature set) — pre-activation → burn record (unchanged), post-activation → new `archival_budget_accrual` LMDB row (`add/get/remove_archival_budget_accrual`, burn-record idiom: BE keys, written only when nonzero, pop removes the height row beside `remove_block_burn`). The epoch close materializes the frozen `archival_budget` row (bounded accrual range-sum over `[E·SEB, (E+1)·SEB)`, same write txn as the `Σwork(E)` sigma row — the M1 same-snapshot pin; written unconditionally so present-and-zero ≠ NOTFOUND), the close revert deletes it, and both tables join `prune_archival_epochs_before`. The emission snapshot gather now carries `budget_atomic` + `has_budget_row` (stored-shape probe), so the verify shim reads budget and denominator from the same close event — no new live operand. KATs **B1** (straddle partition / conservation: burned inflow never enters `budget(E_flip)`), **B2** (reorg across the fork: pop symmetry of both targets, `total_burned` untouched by the accrual side), **B3** (close/revert/re-close byte-identity; §3.3 present-and-zero vs never-closed shape) landed in `archival_substrate_lmdb.cpp`. Consensus-inert until the C-1 whitelist flip: nothing reads `archival_budget` until dispatch lands. - **archival: serve-credit equivalence audit — mirrors, standing KAT, fuzz, and the SCE-1 unify (implements `ARCHIVAL_SERVE_CREDIT_EQUIVALENCE_AUDIT.md`; FOLLOWUPS V3.0 audit-half item)**. Pure-Rust mirrors of the three C++ serve-credit consensus decisions land in `shekyl-archival-retention::serve_credit_decisions` (D-SC-A `(P,s,E)` BE key + pre-block membership; D-SC-B wide acceptance gate with ordered first-failing-branch reasons, reusing `serve_credit_epoch_ok` / `challenge_fire_height` / `challenge_leaf_chunk_bounds` rather than re-deriving; D-SC-C block-level uniqueness, first-collision-wins). The **standing equivalence KAT** runs both legs over the shared fixture `serve_credit_equivalence_kat_v1.json`: the Rust leg asserts verdict + reason for every vector, the C++ leg (`archival_serve_credit_equivalence.cpp`) drives the live `check_archival_serve_credit_input` over seeded state and asserts the verdict `bool` only (§5 leg-responsibility split; the four C++-unreachable marshaling vectors are enumerated, not dropped). Fuzz targets `fuzz_serve_credit_gate` and `fuzz_serve_credit_block_unique` join the CI fuzz-harness smoke gate. Both audited decision sites in `blockchain.cpp` carry guard comments naming the fixture. **Audit outcome: no divergence found.** Finding **SCE-1 resolved** post-proof by a standalone behavior-preserving commit: the block-level pass now keys with `ArchivalServeCreditKey` (new `bytes()` accessor), removing the native-endian composite-key construction `db_lmdb.cpp:1657–1659` forbids; the fixture re-pins `expect_equal: true` so a reintroduced split fails both legs. - **wallet-rpc: Phase 4a scaffold — `rust/shekyl-wallet-rpc`** (`WALLET_REWRITE_PLAN.md` Phase 4a; `docs/api/wallet_rpc.yaml`). New Engine-native crate (not the transitional `shekyl-engine-rpc` wallet2 FFI bridge): axum JSON-RPC on `POST /` (TCP or `uds://`), `WalletRpcError` / allocated `-29xxx` codes from the OpenAPI contract, single-tenant `Tenant` type, HTTP basic auth middleware, UDS listener, `spawn_in_process` for Shape B CLI, and `get_version` (`api_version: 1`). Other SPECIFIED methods return `-32601 method not found` until their 4b sub-PRs. Binary name `shekyl-wallet-rpc` (coexists with the C++ binary of the same name until Phase 5 deletion — operators must not confuse them). - **docs: serve-credit C++-decision equivalence audit — design (`ARCHIVAL_SERVE_CREDIT_EQUIVALENCE_AUDIT.md`, rounds 1–3 + post-closure pins CLOSED; implementation go issued 2026-07-09)**. Specifies the V3.0 verification task from the FOLLOWUPS queue: pure-Rust mirrors of the three C++ serve-credit consensus decisions (`blockchain.cpp:4247` (P,s,E) LMDB dedup; `:4224–4396` full acceptance gate, **wide** scope with ordered first-failing-branch reasons; `:4889–4910` block-level uniqueness), equivalence via a **shared-JSON standing KAT with no new FFI** (C++ leg asserts the verdict `bool` only — all the C++ contract exposes; the reason/branch is a Rust-mirror fidelity assertion against a source-inspection-authored column, no `MERROR_VER` log-parsing), Rust fuzz targets, and **mirror-then-fix** discipline throughout. First finding **SCE-1**: the `(P,s,E)` key is built big-endian for the persistent LMDB set (`ArchivalServeCreditKey`) but native-endian for the in-block set — no live bug (the sets never cross), dispositioned **unify-after** (prove equivalence against today's split, then a standalone behavior-preserving C++ commit unifies onto the BE type). Post-closure pins: fixture `substrate_commit` header + C++ gate guard comment as staleness guards; step-9 holds-at-`H_fire` mirrors the **WS-1-corrected as-of reconstruction** already live at `db_lmdb.cpp:5040–5072` (PR #269), with strictly-above slash-boundary vectors on the C++ leg. `SCE-` family registered at birth in `IMPLEMENTATION_INDEX.md` §2 (rule 94 §1) with §5 inventory + §7 doc-directory rows. The decision-site flip stays **V3.1** (FOLLOWUPS), decoupled at the genesis boundary. - **docs: C-1 pre-flight executed against `dev` `6671d565b` (`REWARD_EMISSION_E3_GATING_ROUND.md` §9)** — every C-1 operand read at its production site after #269/#271/#272 landed. All merge blockers green (E1 hybrid auth, E3 verify body, WS-2 write side, as-of-E snapshot, GF-4b §8.0.3 precondition) and all dispatch anchors confirmed (whitelist, `check_tx_inputs` seam, reference-block root sourcing, block-level `(P,E)` pass template, connect-arm slot). Two findings for ratification: **F-C1a** — `budget(E)` has no production source (the burn site `blockchain.cpp:5026` destroys the staker inflow with a redirect promise; recommended: connect/pop-symmetric accrual persisted beside `Σwork(E)` at close, implicit under-mint on claim expiry); **F-C1b** — the C++ `VARIANT_TAG 0x06` pin is pre-renumber drift (dense scheme makes `0x04` next-free and the genesis registry already assigns it; recommended: pin `0x04`, aligned with the Rust wire tag). C-1 scope enumerated in §9.5; E4's deletion surface shrank (`txin_stake_claim` already deleted by the claim-era retirement PR-4). **Both findings dispositioned same day:** F-C1b **ratified at `0x04`**; F-C1a ratified in direction but gated on a spec-first round — the as-recommended phrasing carried a straddle-epoch over-mint (the epoch containing the activation height mixes burned and emittable inflow; activation is HF-gated, not epoch-aligned). - **docs: `ARCHIVAL_BUDGET_SCHEDULE.md` — gate-1 `budget(E)` spec (RATIFIED 2026-07-08 with the C-1 build opening; unblocks C-1 item 4)**. Pins: `budget(E)` = post-activation staker inflow over E's blocks, via **redirect-the-write** (the fork switches the target of the single per-height `staker_inflow` write — pre-activation → burn record, post-activation → budget accrual, by each block's own height) so the straddle is correct by construction, burn-stop/accrual-start are atomic, and pop symmetry is inherited from the landed burn-record idiom; three pop-symmetric writes (per-height accrual row mirroring `block_burn`; frozen `archival_budget` close row beside `Σwork(E)`, deleted by the close revert, pruned with the epoch family; verify reads the frozen row in the snapshot gather — no new live operand); expiry = implicit under-mint (R1.B posture, rule-21 reopen on economics evidence); zero-budget epochs structurally non-claimable through the M1 §2.3 positivity path, with the builder's single positive-share omission predicate named to cover the budget factor; five armed KATs (straddle conservation, reorg-across-the-fork, close/revert symmetry, zero-budget omission/rejection, fee-bearing coinbase foreclosure). The §2.2 coinbase-foreclosure pin names its cross-function dependency (`validate_miner_transaction`'s exact-equality check is what makes the redirect supply-safe; nothing in the budget code references it) and the standing `economics_c2a_prime` assertions that hold it today, with the fee-side coverage gap closed by KAT B5 at the C-1 build. The §8 servo clause carries a scale note: the servo is a design round, not a commit (it reopens conservation, one-write-one-target, the §6 determinism pins, and the digest surface at once). - **docs: C-1 decision-placement pin ratified (`REWARD_EMISSION_E3_GATING_ROUND.md` §9.5)** — source-verified split: computation is uniformly Rust; decisions are mixed (emission verify Rust-decides via typed errors that `archival_ffi.rs` only maps; the older serve-credit path decides in C++ against LMDB at `blockchain.cpp:4247`/`:4312`/`:4889–4910`). C-1 builds every new consensus **decision** in Rust with C++ as marshaler/applier — the block-level `(P,E)` pass (item 6) takes its verdict from a Rust function rather than mirroring the C++ `std::set` idiom; the connect arm's dedup decision is already Rust (`claimed_epochs_check_and_set`), and the mint application stays C++. The three C++ **serve-credit** decisions — and only those; the C-1 emission block-pass is new code with no C++ predecessor and ships at C-1, never deferred — are queued in `FOLLOWUPS.md` as extraction candidates with the audit and the flip **decoupled at the genesis boundary**: the Rust mirror + equivalence proof + standing KAT is V3.0 pre-genesis (a verification task — divergences found are free fixes before genesis; mirror-then-fix, never mirror-a-fixed-version), and the behavior-preserving decision-site flip is V3.1 (unblocked when the equivalence KAT is green and the flip carries no behavior delta). - **wallet: GF-4b backing-lineage pre-join wiring — sweep, lineage ladder, `BackingSet`; the `REWARD_EMISSION_VIN_PLAN.md` §8.0.3 C-1 activation precondition is satisfied** (`ARCHIVAL_GF4B_BACKING_LINEAGE.md`, design + review round GF4b-1…6): - *Shared maturity math.* `eligible_height` extracted to `shekyl_engine_state::transfer` — the single wallet-side definition of "leaf in the curve tree yet" — with `SPENDABLE_AGE` unified onto `shekyl_consensus::DEFAULT_LOCK_WINDOW` (X5's transfer path and the scan seam now call one function; two-constants-both-10 drift seam closed). - *Lineage + spendability at the scan seam (schema v5).* `PFundingOutputRecord` gains `MintLineageOutput` (`EmissionReward` / `BondPostChange` / `ExternalTransfer`; **no miner rung** — `P` is shard-serving only, coinbase-to-`P` classifies rung 3) and `spendable_height` (the shared `eligible_height` result). `PSCAN_STATE_VERSION` 4 → 5; snapshot regenerated; lineage and spendable-height KATs at the dual-extract seam. - *Sweep conversion.* `select_funding_outputs` → `sweep_funding_outputs`: consumes the persona's **entire** unreserved **spendable** eligible set (no early break; a subset is inexpressible) — GF-4b's structural-emptiness claim rests on it. Spendability filter per GF4b-6 (an immature record — fresh funding or adversarial coinbase-to-`P` — survives to the next sweep instead of denying the bond). Gated on the new `SpentRecordsDurablyPruned` witness token (GF4b-5): zero production constructors until SP-R0 durable pruning lands, so premature go-live fails to compile. - *`BackingSet`* (`engine/backing_set.rs`): constructor-gated backing-eligibility type — possession proves every member is rung 1/2; filter-not-fail-closed on legal between-sweeps tranches; GF4b-3 survivor `debug_assert!` armed (doc names its late-surfaced-output false-positive mode). Zero-pre-bond-output test landed. - *Docs.* WI-2 §3.2 extent amendment + WI-4 measurement citation updated; lifecycle GF-4b sequencing corrected to wired state; FOLLOWUPS: "2d-1 WI-2 durable removal" annotated with sweep severity + structural gate, and the GF4b-2 genesis gate added to the V3.0 pre-genesis queue (`stake_in` single-structured-output funding; common-case sweep consumes exactly one input). - *Review-round hardening (2026-07-08).* `BackingSet::from_spendable` now **enforces** the GF4b-6 spendability filter itself (new `reference_height` arg) rather than trusting a caller contract, so the type's "spendable" guarantee holds by construction and immature records are dropped before the survivor tripwire; the GF-4b scan pre-pass `debug_assert!`s the `transaction_hashes ↔ transactions` positional pairing (an invariant violation now fails loudly in debug instead of silently degrading lineage to `ExternalTransfer`, release keeps fail-toward-forbidden); `sweep_funding_outputs` folds its sum + collect into one pass; the `PFundingOutputRecord` test fixture is single-sourced in `engine::test_support::funding_record`. The removed-subset-bound tx-size facet and the `reference_height` freshness contract are recorded as C-1 residue criteria (§5 items 5–6). - **archival: WS-2 emission-claim dedup plumbing — connect-side writer + journaled revert** (`REWARD_EMISSION_E3_GATING_ROUND.md` §3 item 3a / §6.2–§6.4). The write side of the three-layer dedup, landing beneath the verify body's read-only layer: - *Single writer FFI.* `shekyl_archival_claimed_epochs_check_and_set` (`archival_ffi.rs` / `shekyl_ffi.h`) wraps the windowed Rust `claimed_epochs_check_and_set` — validated write-back over a caller-owned buffer, distinct status codes for inserted / already claimed / not-settled / expired / invalid. C++ stores bytes, Rust decides (the `good_through` split). - *Connect path + journal.* `BlockchainLMDB::apply_archival_emission_claim` loads the bond, journals the **full pre-image** (`claimed_settlement_epochs` + `first_paying_emission_height`) into the new `archival_emission_claim_log` table (`BE(height)||BE(seq)`, slash-log idiom), then runs the FFI writer per claimed epoch. A dedup hit or unclaimable epoch at connect is a **hard error**, not a reject — verify's contains-check and the block-level pass make it unreachable, so reaching it means an emission paid without marking `E` (§6.2's Ok(false)-at-connect posture). `first_paying_emission_height` is set-once on the first claim. - *Pop path.* `revert_archival_emission_claims_at_height` restores the journaled pre-image (reverse-`seq`) and consumes the rows; wired into `BlockchainDB::pop_block` after the slash/close reverts. The pre-image shape (not an insert-inverse) is what closes §6.3's double-mint: the connect mutation is insert **plus window prune**, and removing the inserted epoch cannot restore what the prune evicted. - *KATs* (`archival_substrate_lmdb.cpp`): apply/journal/revert roundtrip (byte-identical restore, journal consumed), connect-breach hard errors (dedup hit, unsettled, expired, unknown P), the §6.4 prune-straddle property test — claimed `E_old` evicted by a later claim's prune, popped across the epoch boundary, then **re-claim of `E_old` rejected** (the assertion the naive remove-inverse fails) — and first-paying set-once / pop-symmetric. - *Deferred to C-1:* the block-level `(P,E)` uniqueness pass and its KATs — it iterates `txin_archival_reward_emission`, whose C++ variant type lands with C-1's transport shim. - **archival: emission vin verify body, §7.1 steps 1–6 — KAT-tested, not on the consensus dispatch** (`REWARD_EMISSION_E3_GATING_ROUND.md` §3 item 3; `REWARD_EMISSION_VIN_PLAN.md` §7.1 / PR-E3). New `shekyl-archival-retention::emission_verify` module implementing the fail-fast verify: finalization + claim-age bounds (F-E1, explicit structural checks), bond posture, the WS-2 **read-only** dedup layer (`claimed_epochs_contains` against the pre-block record; the write side stays on the connect path, item 3a), work recompute through `as_of_e_served_work` with **per-shard exactness** (`serve_credit_bit` must equal the ledger fact, `scarcity_milli` must equal the member-masked recompute — a wrong split cannot hide behind a right total), three-channel economics over the **persisted** `Σwork(E)` via `reward_share_floor` plus the loud Σvout zero-tolerance compare, and the membership-only backing gate (leaf-hash equality + `verify_membership_only`). Fail-closed by type (§3.0 gate-last): the accept verdict (`EmissionVerified`) is only constructible from three sealed witnesses — `ClaimsVerified` and `BackingVerified` minted by the verify functions, and `AuthVerified`, which has **no production constructor in the crate**; the ML-DSA witness minter lands with C-1, so an authed acceptance is unrepresentable until the activating cut. No FFI entry point yet, by the same gate-last discipline (deferred to C-1 with the dispatch wiring). Twelve KATs (`tests/emission_verify_kat.rs`): honest-claim accept built from the same sourcing functions (the numerator-is-a-denominator-term identity), F-E1 boundary (reject at `h_close(E)`, accept at `h_close(E)+1`), claim-age boundary at `C − W`, read-only dedup rejection, wrong-epoch / wrong-claimant polarity, per-entry work-claim polarity (scarcity drift, false/denied credit bits, omission, duplicates, nonzero scarcity on an unserved shard), no-credit-sentinel rejection (the Q12 economic leg), economics zero-tolerance (±1 atomic unit, vout drift), and the step-6 negative paths (leaf mismatch, garbage proof). - **archival: as-of-E emission epoch snapshot — the M-2/Q7 verify-side work channel** (`REWARD_EMISSION_E3_GATING_ROUND.md` §3 item 2; `REWARD_EMISSION_VIN_PLAN.md` §8.0.2(B)). The substrate PR-E3's verify shim will consume: a by-value snapshot (Q7: no callbacks across the FFI) carrying the same rows the epoch close computed from, plus the **persisted** `Σwork(E)` denominator — never a recompute, since the M1 `K_COVER` gate's operand (`frozen_shard_count` as-of-close) is a close-only quantity whose outcome reaches verify only through the stored value. Three coupled cuts: - *One gather routine, two consumers.* The epoch close's inline serve-credit/bond/shard row gather is extracted into `BlockchainLMDB::gather_archival_epoch_rows`; the close and the new `gather_archival_emission_epoch_snapshot` both call it, so row-selection divergence between close and verify is unrepresentable (WS-1 §5.5 single sourcing, C++ side). The close-processing boundary `(E+1)·SEB` is single-sourced through the new `shekyl_archival_epoch_close_processing_height` FFI wrapper rather than re-derived by hand. - *One decoder, one work function.* `shekyl_archival_emission_epoch_work` (`archival_ffi.rs` / `shekyl_ffi.h`) computes claimant `work_P(E)` and its `Curve(work_P)` term via `as_of_e_served_work` — the same sourcing function whose output built the persisted denominator — over arrays decoded by the same `decode_epoch_rows` helper the close FFI uses. - *Identity KATs.* Sum-of-capped-terms == persisted `Σwork(E)` at the Rust FFI layer (`emission_epoch_work_sums_to_persisted_sigma`) and at the LMDB layer over a production-shaped close (`emission_snapshot_identity_and_descriptor_immunity`), which also pins live-descriptor immunity: mutating tip holdings after the close (the M2-1 drop-after-serve mutation) leaves every snapshot output bit-identical, and a bonded-but-never-credited claimant resolves to the no-credit sentinel with zero work. - **emission: E3 gating design round closed — leg design complete** ([`REWARD_EMISSION_E3_GATING_ROUND.md`](completed/REWARD_EMISSION_E3_GATING_ROUND.md)). Pre-flight found PR-E2 already landed (stale-doc corrections to `IMPLEMENTATION_INDEX.md` / `REWARD_EMISSION_VIN_PLAN.md`); policy trio closed (Q3 vacuous at arity 1; Q11 same-tx backing+fee ACCEPT with balance-exclusion KAT; Q12 zero-work rejected as foreclosed). **WS-1 ratified:** M-2 supply conservation closes under *bits-sourcing ∧ as-of-fire-height acceptance* — `work_P(E)` sources the per-`(P,s,E)` serve-credit bits, not the tip-mutable `held_shard_ids`; one shared `as_of_E_served_work` both sides; one `at_height`-honoring accessor with two consumers (`blockchain.cpp:4307` acceptance, `db_lmdb.cpp:5270` slash-eligibility); three armed KATs. **WS-2 ratified:** three-layer `(P,E)` dedup (wire / block-level pass / verify-read + connect single writer), with the prune-straddle identified as a **double-mint** (window floor keys on reorg-non-monotonic `current_settled_epoch`) closed by a journaled revert whose row restores the prune-evicted members atomically with the inserted `E` (implemented as the full claimed-set pre-image, per the round's §3a pin); prune-against-finalized recorded as the rule-21 reopen that retires the journal. PR-E3 build list unblocked (round §3). ### Changed - **RCT→CT rename, V3.0 public-API slice (`docs/FOLLOWUPS.md` RingCT→CT sweep umbrella, flagged 2026-06-22).** A Shekyl tx is a confidential transaction — there is no ring — so the public, genesis-locking surface drops the inherited "R" before it becomes breaking. Name-only: no wire, hash, or consensus byte changes. (1) Daemon-RPC transaction JSON keys `rct_signatures`/`rctsig_prunable` → `ct_signatures`/`ctsig_prunable` (JSON-archive-only `ar.tag`; `binary_archive::tag` is a no-op, so binary serialization is untouched; breaking for JSON consumers of `decode_as_json`, tx-pool JSON, `print_tx`). (2) Legacy wallet-RPC `estimate_tx_size_and_weight` request field `rct` → `ct`; `WALLET_RPC_VERSION` bumped 1.30 → 2.0 (incompatible request-field rename), FFI dispatch and Python RPC framework updated. (3) C++ enum variants `rct::RCTTypeNull`/`rct::RCTTypeFcmpPlusPlusPqc` → `rct::CTTypeNull`/`rct::CTTypeFcmpPlusPlusPqc` (type byte values unchanged). (4) Cross-language constant chain renamed end to end: `consensus_constants.json` key `ct_type_fcmp_plus_plus_pqc`, C++ macro `SHEKYL_CT_TYPE_FCMP_PLUS_PLUS_PQC`, Rust `CT_TYPE_FCMP_PLUS_PLUS_PQC`; plus Rust residue (`ct_base_blob` preimage component, stale doc refs). The `rct::` namespace, `rctSig*` struct/module/file names, and the `transaction.rct_signatures` field stay gated on `wallet2` retirement per `CT_SURFACE_NAMING_PIN.md` §5 (V3.1+ internal bulk). Clears the rename dependency on the CT-balance batched-FFI followup. (`chore/rct-to-ct-public-api`) - **rct: delete the dead base-slot `pseudoOuts` and the three standalone object serializers — byte-identical; a claimed coinbase wire bug is REFUTED (`CT_SURFACE_NAMING_PIN.md` §2 correction).** The legacy `rctSigBase::pseudoOuts` was never populated on any live type and — the load-bearing correction — never serialized on any wire: every real byte producer (transaction serializer, tx-hash paths, FCMP++ pre-hash, PQC payload binding) uses the `serialize_rctsig_base` member function, which never emitted it. The `FIELD(pseudoOuts)` branch previously believed to put a varint-0 on every coinbase blob lived in a standalone `BEGIN_SERIALIZE_OBJECT()` with no production caller — a second, disagreeing definition of the rct bytes that was laxer than the real wire (it round-tripped states the member serializer rejects) and whose existence produced the phantom wire-format finding. Deleted together: the field, its ctor init, all eight always-false emptiness guards (`tx_verification_utils.cpp`, `blockchain.cpp`, `rctSigs.cpp`), the base arm of `get_pseudo_outs()` (now unconditionally the prunable vector), and all three caller-less object serializers (`rctSigBase`, `rctSigPrunable`, `rctSig`) so the class of second-serializer phantom cannot recur; the two unit tests that round-tripped the dead serializer are repointed at the member function (the real path), with a new pin that a no-output Null base is exactly the one type byte. The naming-pin doc's step-3 routing (wire change + version bump) is retracted in place, and its deletion inventory's `cryptonote_boost_serialization.h` item is corrected — that line is the **prunable** field, live and load-bearing, and must not be touched by the rename sweep. Verified byte-identical: full unit suite (fcmp/Serialization/archival, 129 tests) plus the shekyl-wire live-oracle KATs (`coinbase_block_and_tx_hashes_match_the_daemon`, coinbase/spend round-trips) all pass against the pre-change pinned blobs; `GENESIS_TX` and all hashes untouched by construction. Rule 42 does not fire (no persisted engine-state block embeds the rct base). - **archival: WS-1 held-sourcing correction — serve-credit ledger is the sole `work_P(E)` source; holdings reads are as-of-fire-height** (`REWARD_EMISSION_E3_GATING_ROUND.md` §5, item 1 of the PR-E3 §3 build list; closes the M-2/Q10 two-conjunct condition "bits-sourcing ∧ as-of-fire-height acceptance"). Four coupled cuts, one validation surface: - *Single sourcing function.* `as_of_e_served_work` (`rust/shekyl-archival-retention/src/consensus_state.rs`) now derives market membership, `R_market`, and per-bond work from the frozen serve-credit pairs alone; `epoch_close_compute` delegates to it, and the PR-E3 verify body will call the same function — numerator/denominator sourcing divergence (the M-2 silent over/under-mint) is unrepresentable, not tested-against. - *Descriptor out of the work channel.* `held_shard_ids` is removed from `EpochCloseBond`, the FFI struct (`shekyl_archival_epoch_close_bond` in `shekyl_ffi.h` / `archival_ffi.rs`), and the LMDB gather marshal — the tip-holdings re-filter that produced the drop-after-serve under-count no longer compiles. Fixture `sigma_work_milli` values recomputed (`consensus_state_kat_v1.json`, `reward_gate_kat_v1.json`). - *As-of-height accessor, one implementation, both consumers.* `BlockchainLMDB::archival_bond_holds_shard` honors `at_height` by reconstructing from tip holdings + the slash log (holdings are shrink-only post-join at the current substrate); the slash log gains a v2 `holdings_pre_kind` field (pre-genesis posture: v1 rejected at decode) which also fixes the pop-revert heuristic that mis-restored a slashed single-shard compact bond to complete-tree. Serve-credit acceptance (`blockchain.cpp`) and slash eligibility (`db_lmdb.cpp`, tip-holdings pre-filter deleted per the round's third-consumer finding) now answer "held at the challenge fire height" identically, with the `h_fire` guard aligned (`h_fire == 0 || h_fire > h_close`) on both sides. - *KATs armed on the new sole gate.* Drop-after-serve (`consensus_state.rs`), acceptance-gate both polarities (`archival_serve_credit_integration.cpp`, asserting the gate queries exactly the derived `h_fire`), slash-side mirror through the production `add_block` → slash-scheduler path plus as-of-fire reconstruction and revert KATs (`archival_substrate_lmdb.cpp`), and the one-strike bad-interval cascade guard. ### Fixed - **staking: S6 session self-cert chi-square graded against flat expected counts, miscalibrating the advertised α=1e-6 to ~1.5e-2** (`rust/shekyl-standoff/src/conformance.rs`, `rust/shekyl-stats`; `ARCHIVAL_BOND_S6_CERTIFY_DRAW_PLAN.md` §2 R0-D3 UPDATE 2026-07-09). `grade_sample` bins the 601 discrete outcomes of `window = 600` into 60 bins — one bin is 11 outcomes wide — but scored the histogram against a flat `n / n_bins` expected count, injecting a noncentrality of ≈ 32.7 at `n = 200 000`. A **correct** `OsRng` therefore failed the wallet-open self-cert (and the dedicated S6 CI test) about once per 70 grades; the R0-D3 false-fail previously attributed to the legitimate α tail was this bug. Expected counts are now proportional to the exact bin widths (new `chi_square_counts_expected` in `shekyl-stats`, fail-closed on degenerate expecteds; exact `u128` binning replaces the `f64` index map, which also lost integer precision above 2^53), restoring the advertised calibration. Regression test pins an exactly-uniform sample to a chi-square of zero. - **gitian: pre-install the pinned Rust toolchain serially so release builds stop racing rustup** (`contrib/gitian/gitian-{linux,osx,win,freebsd,android}.yml`). The gitian containers installed rustup with its *default* toolchain, but `rust/rust-toolchain.toml` pins `1.94.0`; the first `cargo` under the parallel `make` then auto-installed `1.94.0` with the *default profile* from several processes at once, and the concurrent writes to `~/.rustup/downloads` aborted every platform build with `component download failed for clippy-…: could not rename 'downloaded' file`. All four `v3.1.0-alpha.6` gitian jobs (Linux/macOS/Windows/FreeBSD) died this way. The rustup installer now pins `--default-toolchain 1.94.0 --profile minimal`: the pinned toolchain is present before `make`, so no build-time auto-install races, and the minimal profile drops clippy/rustfmt — a gitian build only compiles the Rust FFI, it never lints. The install is also hardened to the house download-then-run form (`curl --retry … -o /tmp/rustup-init.sh` then `sh …`, mirroring `.github/actions/install-rust`) rather than `curl | sh`, which can mask a partial/empty download as an exit-0 no-op. The `1.94.0` pin is added to `rust/rust-toolchain.toml`'s bump-policy lockstep list. `RELEASE_PROMOTION.md` §4 gains a **gitian dry-run gate**: dispatch the gitian workflow against the frozen SHA (no tag pushed) and require all four platforms green before the cut/promote/tag, so release-only build breakage is caught pre-tag instead of forcing a bump. ### Security - **deps: clear the `cargo audit` warning backlog** (`rust/Cargo.lock`, `rust/.cargo/audit.toml`). Bumped `anyhow` `1.0.102 → 1.0.103`, closing the soundness notice RUSTSEC-2026-0190 (the only *unsound* advisory in the tree; patched `>= 1.0.103`). Dropped the now-stale ignore for RUSTSEC-2026-0097 (`rand` unsoundness): the tree already resolves to `rand` `0.8.6`/`0.9.4`, both in the patched ranges (`>= 0.8.6`, `>= 0.9.3`), so the advisory no longer applies. The two remaining notices are crate-level *unmaintained* INFO advisories on transitive, non-consensus, render/build-time deps that cannot be dropped without an upstream move — `atomic-polyfill` (RUSTSEC-2023-0089, via `postcard 1.1.3 → heapless 0.7`, and not even compiled on native-atomic targets) and `ttf-parser` (RUSTSEC-2026-0192, via the `imageproc` chain, same root as `paste`). Both are now documented in `audit.toml` with explicit reopen criteria per `21-reversion-clause-discipline.mdc`, alongside the existing `paste` entry. `cargo audit` is fully green with zero unexplained warnings. ## [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-5d `content_gen` gate and the `SubmitVerdict` success/rejection projection per `DAEMON_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 in `get_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 first `rust/shekyl-wallet-rpc` sub-PR conforms to this file. - **archival: segment-freeze pipeline design round 1 opened** ([`ARCHIVAL_SEGMENT_FREEZE_PIPELINE.md`](design/ARCHIVAL_SEGMENT_FREEZE_PIPELINE.md)) — the production writer/deleter for `m_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** — segment `k` freezes at the first height where `leaf_count ≥ (k+1)·SEGMENT_LEAF_COUNT`, `R_k` read 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). Pins `SEGMENT_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_leaf` is deleted** (doc §6.2) — it is a derived copy of `m_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`](design/ARCHIVAL_SEGMENT_FREEZE_PIPELINE.md) §7, on `feat/segment-freeze-pipeline`). (1) `SEGMENT_LEAF_COUNT = 25 992` through 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 in `db_lmdb` inside the block write txn — `process_archival_segment_freezes_at_height` writes newly-crossed segment rows reading `R_k` from the layer-2 chunk the same-txn grow computed, `revert_archival_segment_freezes` is the derived delete (O-3 pop-symmetry); (4) **`archival_shard_leaf` deleted** — the gate-2 challenge path reads the 38-leaf chunk directly from `m_curve_tree_leaves`; (5) the §9 C++ suite (`archival_segment_freeze.cpp`: first-crossing, multi-segment catch-up, independent `R_k` recomposition, 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 at `25 992`; (6) tripwire extensions (`check_reward_gate_predicate_sites.sh`: writer one-site, cursor accounting, division one-site, `for_kat` containment); (7) docs (`LMDB_SCHEMA.md` v7 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 leading `u64`s); **PF-6a** `KCover` capability newtype (`consensus()` sole production constructor; KAT injection behind the permanent dev-only `consensus-kat` feature). - **archival: O(1) pop-symmetric frozen-shard counter — the M1 gate operand's persisted backing store (schema V8)** ([`ARCHIVAL_SEGMENT_FREEZE_PIPELINE.md`](design/ARCHIVAL_SEGMENT_FREEZE_PIPELINE.md) §4.4; M1 §11.11; closes the `FOLLOWUPS.md` efficiency follow-on from code-review #263). `properties["archival_frozen_shard_count"]` moves in structural lockstep with the segment table: +1 in `put_archival_shard_segment` (now `MDB_NOOVERWRITE` — rows are CREATE-only, enforced at the mutation site), −1 per deleted row in `revert_archival_segment_freezes` (underflow-aborted). `count_frozen_shards_at_close` drops its full-table walk for an O(1) counter read + `MDB_LAST` frontier 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):** DB `VERSION` 7→8; `migrate` refuses pre-V8 databases loudly (delete-and-resync; no pre-genesis migration code per `15-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.md` as a breach (not a precedent); `26-sub-pr-design-discipline.mdc` gains 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_COVER` finalization 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`](completed/ARCHIVAL_REWARD_GATE_M1.md) §11.10 implementation record). Executed on `feat/m1-reward-gate-design` after the pre-flight pass held the §6 enumeration at the audit pin (re-verified post-WI-4-merge at `1e89df832`; M3-3 discharged — WI-4 §16.3 pins `shard_count` as structural, the `K_COVER` calibration derives against segment count). (1) `k_cover` + `k_cover_provisional` constants with `build.rs` validation and a `compile_error!` refusal absent the `provisional-k-cover` feature — armed before the identifier existed anywhere else; sentinel refined in-arc from fail-closed `u64::MAX` to gate-identity `0` (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_close` filter, decode failure aborts loudly) threaded as an FFI `frozen_shard_count` parameter into `EpochCloseInputs::{frozen_shard_count, k_cover}` with the zero-at-top gate factor in `epoch_close_compute`. (3) Wire-level positivity: `WireError::RewardAmountZero` at both `validate()` and `read_payload()` — the zero-amount row is unencodable *and* undecodable; the `EMISSION_AUTH_MSG_V1` corpus zero row replaced and its four pinned digests regenerated in the same commit. (4) `reward_gate_kat.rs` G-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 to `K_COVER` sealing). (5) `scripts/ci/check_reward_gate_predicate_sites.sh` as consensus-invariants invariant 5: single `K_COVER` comparison site, single counting read over `m_archival_shard_segment` (`mdb_stat` refused), `≤` 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`](completed/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 where `shard_count < K_COVER`, enforced as a uniform zero-at-top factor in `epoch_close_compute` and inherited by the emission zero-tolerance compare through the zeroed stored `Σwork`. Spec-first per `05-system-thinking` and `26-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_COVER` finalization 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 and `join_settlement_epoch` as eligibility-only; the non-claimable rule amended to the uniform positive-share form; the provisional `k_cover` sentinel 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 positive `reward_amount_plain`, rejected at `validate()` — the zero-row beacon becomes unencodable, and `K_COVER` is compared at exactly one code site, guarded by a new CI grep tripwire); §2.1 output naming corrected to the real two-field `EpochCloseResult` (no per-`P` surface 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 `Σwork` from 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 to `m_archival_shard_segment` filtered on `freeze_height ≤ H_close(E)`, threaded as a new `EpochCloseInputs::frozen_shard_count` field + 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_segment` has 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 the `freeze_height ≤ H_close(E)` filter; the CI tripwire extended to refuse any other counting read over `m_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 to `frozen_shard_count`; named pre-flight item added requiring the WI-4 §14.4 `K_COVER` calibration 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 the `LMDB_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, not `69af41a`. WI-4 doc merge holds sequence slot 0. Intra-PR order pinned: constants + compile-refusal before the `k_cover` identifier 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↔`P` funding-seam unlinkability property is now *measured*, not just hooked. The a-priori bound `r = 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 the `nearest_principal` placeholder (`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 — blind `0.237` at window 0 → `≈0.13` under full jitter); the stress arm is an **oracle-panel** with an exact seam-consistency-gated member (§4.3.1; `lr > s3` on 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 row `r = 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 (panel `r = 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), and `K_COVER` finalization 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_COVER` calibrates 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-serialized `PublicByConsent` marker, 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 < 2` survives (claim-anchored, observer-independent) but measured `r(N)` is only known at `N = 10` — **N-sweep added** (§16.7 item 4, `r < 2` at every swept `N`, 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 the `K_COVER` cover model). Attack 3: the `K_COVER` cycle traced and **does not close** — the validation projection is `K`-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 — the `r < 2` **mean-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 what `1.86` means — 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 < 2` promotes 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_plain` cleartext, 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 via `holdings`/`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 any `g_age_milli`/`curve_milli` change; `Curve` plateau breakpoints cluster the holdings stratum — §18.3 measures direction), `SETTLEMENT_EPOCH_BLOCKS` as 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 — `P` is the pseudonym, public by construction; only the `P`→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 to `bond_floor(holdings)` (`bond_post.rs` FloorMismatch), 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 transparent `amount·H` cleartext term in the CT balance (`bond_ct_balance.rs`; funding and change are the hidden legs), `commitment_mask` is 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), and `reward_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-transport` `compile_error!` without SOCKS), principal side opt-in (`--proxy` default `None`); 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 (live `BondPostDispatched.at` is the due block; dispersal is sub-block `U[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, feature `gf7-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 the `P`-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 the `Dispatched` transition **before** any send — every attempt, first or resubmit, re-lifts the stored bytes through the `submit_bound` choke (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_BLOCKS` alarm horizon (funds-safety over liveness: the alarmed record is held, not dropped). Schema: `PENDING_POST_VERSION` 1 → 2 for the `Dispatched` arm; v1 fails closed (pre-genesis, rule 15). Wiring: a `DispatchTick` seam at the end of `pscan_sweep` (a failed tick logs and retries next sweep, never halts the scan); production store is the `WalletFile`-backed region-2 AEAD seal of `.wallet.pending`, production broadcast is the `Local`-posture `BroadcastSubmitter`. GF-7: live `BondPostDispatched` emission 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_pscan` lifecycle wiring — WI-1, completing 2d-1 SP-5** (`ARCHIVAL_BOND_2D1_PSCAN_PLAN.md` §"WI-1"; `IMPLEMENTATION_INDEX.md` §4 WI-1 row). The firewalled `P`-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 gets `Ok(None)` and pays nothing) and `Engine::start_pscan` (the on-demand entry mirroring `start_refresh`; `NoStakeEngine` for a non-staker). Both run `PScanConfig::production()` at `DEFAULT_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 returned `PScanHandle` is 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 and `Arc::try_unwrap` succeeds). The transient `#[allow(dead_code)]` chain through `block_source`/`cadence`/`task`/`accrual` is removed; remaining allows each name their later consumer (2d-2 posture selector, SP-7 `C_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_ID` for all three networks (mainnet, testnet, stagenet)** (`src/cryptonote_config.h`). The `GENESIS_TX` for every network was regenerated during the post-`v3.1.0-alpha.3` work, but the `NETWORK_ID` handshake identifiers were left unchanged — the exact silent-fork combination the `RELEASE_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 a `rm -rf ~/.shekyl` + re-sync change with no migration path (per `15-deletion-and-debt.mdc`). Gates the next testnet rehearsal release, which must run `TESTNET_REHEARSAL_CHECKLIST.md` from clean datadirs per §6. - **ci: CodeQL C/C++ runs buildless; ignores vendored `external/` code** (`.github/workflows/codeql.yml`, `.github/codeql/config.yml`). Switched the `analyze-cpp` job to `build-mode: none` and added `paths-ignore: ['external/**']`. The path filter is the goal — dropping the standing vendored highs in `external/db_drivers/liblmdb/mdb.c` (`cpp/integer-multiplication-cast-to-long`, `cpp/comparison-with-wider-type`) and `external/qrcodegen/QrCode.cpp`, which Shekyl never patches in place — but config-file `paths-ignore` only 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-owned `src/`; the crypto/consensus/amount logic that once justified a deep trace-based build now lives in Rust (covered by the separate `analyze-rust` job), 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-id `query-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); the `docs/FOLLOWUPS.md` submit-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 and `V3_ENGINE_TRAIT_BOUNDARIES.md`'s §4 idempotency row/retry prose no longer cite the retired `AlreadyKnown` dedup heuristic — `submit_transaction` idempotency 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.md` `AlreadyInChain` entry, now resolved). The landed-inert submit-watchdog kernel gains its production consumer: `SubmitLifecycleDriver` owns the persistent overlay (escape- ladder wait epochs, alarm latches, F40 re-scan / R2 counters) and, on each `tick`, projects the F14-lock-derived held submits, fetches daemon health (new `DaemonEngine::get_health` surface), runs the escape ladder per held tx (projection → health gate → rung-1 resubmit-same-bytes probe → outcome), and drains the `PendingTxState` rescan 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::FruitlessRescanBreakerTripped` operator alarm; F40-R1 is structural (the lock stays placed — no path here releases it). New bounded diagnostics: `WatchdogAlarm`, `FruitlessRescanBreakerTripped`, `WatchdogProbeDispatched`, `WatchdogProbeResolved`. Exposed via the public `Engine::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 hermetic `StubHost`/`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.md` `AlreadyInChain` entry). The wire verdict grows the required `height` field (frozen fixture updated atomically per the §2.3 third-category rule; new skew test D pins a field-less `already_in_chain` to 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-D `classify_race` settled arm. Wallet-side, `finalize_submit_already_in_chain` is 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 claimed `height`; `height` routes only the release path — above-synced waits for §2.6 path-1 refresh catch-up, at/below-synced emits the new `PendingTxDiagnostic::TargetedRescanRequested` for the reorg-heal re-scan. F40-R1 (rescan never releases) is structural and pinned by regression; the re-scan executor consuming `TargetedRescanRequested` and 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.md` 2c-2a dispatch-shape entry). The closed-set `BroadcastSubmitter { Local, PerP }` enum with match-delegation (RPITIT, no `Box`), the single `for_posture` construction choke point carrying the ②→`PerP` routing guard (an `OwnRemote` broadcast can never ride the shared principal connection — the first-seen-origin leak the dispatch closes), the `PBoundBytes` byte↔persona newtype equality-checked at the `submit_bound` façade (`debug_assert` loud in dev, fail-closed `PersonaMismatch` in release), and `PTransactionSubmitter` tightened to `for_persona` so the persona↔circuit binding is by construction and the retained `PCanonicalId` feeds 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-standoff` gains `plan_entry_seam`/`EntrySeamPlan` — the single-sourced consumer of the full `draw_entry_gap` tuple (order-coin cannot be dropped; the triangular mis-build cannot be re-introduced) — and, behind the non-default `gf7-hooks` feature, the `BroadcastTimelineObserver` seam with the three-axis joint `TimelineEvent` vocabulary. The `StakeEngine::SignBond` handler now consumes both draw values into the plan and replies with `SignedBondPost` (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-timeline` is 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 gains `ci/gf7-no-emit-guard` (§6.4): a dependency-graph assertion that `shekyl-staking-sim` has zero dependents and a per-crate feature-resolution check that `gf7-hooks` never resolves on production edges outside the sim, with a positive control against rename-vacuousness. The §6.5 no-behavioral-delta argument is documented in the `stake_engine` module 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↔`P` timing unlinkability, GATE6 §10.12) is a genesis blocker whose `P(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 injected `BroadcastTimelineObserver` seam (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-default `gf7-hooks` feature compiled out of release (the `shekyl-standoff` `conformance` shape). 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-types` crate — the typed wallet↔daemon submit contract** (`docs/design/DAEMON_SUBMIT_VERDICT.md` §2, PR-2 of the submit-path Rust cutover). `SubmitVerdict` / `RejectCause` / `SubmitTransactionRequest` as the single Rust definition both `shekyl-daemon-rpc` and `shekyl-rpc-client` will consume, replacing the Monero-inherited status-string-plus-booleans reply schema. Ships with the §2.3 version-skew pins (unknown cause → `Unrecognized` fail-safe; unknown verdict tag → `Err` arm; unknown fields tolerated), frozen JSON representations for every variant, and the §3.4 frozen-hex txid KAT (C++-daemon-captured oracle blobs proving `shekyl-wire` txid ≡ 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; the `SEEDHASH_EPOCH_BLOCKS` / `SEEDHASH_EPOCH_LAG` env 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=correctness` three-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 dedicated `runtime-modes` cron job on both active branches (main + dev matrix; opt-in on dispatch via the `runtime_modes` input, 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 a `dtolnay/rust-toolchain@stable` moving-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 hardened `install-rust` composite — cross-target sites add a separate `rustup target add` step (the depends.yml pattern) — so zero `curl | sh` or hand-copied recipes remain. - **pow: native-aarch64 CI lane** (RandomX v2 test-regime hardening PR-2). New `native-arm` job on `ubuntu-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 new `canonical_pins_full` test 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 the `fpu_rounding` FPCR 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-parity` builds 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-parity` cron job in `randomx-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` + `corpus` harness mode), plus a separate-process miner-shaped KAT provenance test (`randomx-v2-miner-kat`) and a binary `nm` symbol-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-layer* `assemble_path` (`join_market_bond_post_fcmp_verify_over_real_tree`, previously `#[ignore]`d) was failing `BatchVerificationFailed`. Root cause: `shekyl-curve-tree::assemble` emits **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** in `shekyl_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's `shekyl_fcmp_verify` — the same Rust `verify` via 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 in `FOLLOWUPS.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 both `c1` and `c2` branch 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 proves `shekyl-wire` serializes 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_identical` in `shekyl-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 in `fcmp_spend_e2e.rs` (above). The synthetic `read(write(x))` round-trip and structural rejection tests in `fcmp_spend_roundtrip.rs` are 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. At `assemble()`, 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-derived `ArchivalPKeys` bundles to the actor, and **drops the borrowed seed** (`&[u8; 64]`) at function end, exactly as for `AllKeysBlob`. The whole `create` / `open_full` call is the blocking unit async callers already wrap in `spawn_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 `StakingBlock` in `WalletLedger`** (`shekyl-engine-state`, region-2 state file; `STAKING_BLOCK_VERSION = 1`, `WALLET_LEDGER_FORMAT_VERSION` bumped, schema snapshot + CI pairing added). Carries `staking_enabled`, the `p_slot` cursor, and `bonded_slots` (the per-persona live-bond record). Chosen over `SettingsBlock` (C++/FFI surface, not loaded by `assemble()`) and `WalletPrefs` (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_slots` is a reconcilable hint, not truth** (flat `Vec`, documented semantics) so the 2d scan-reconcile GCs phantom records (from a persist-before-sign crash) without a second version bump. - **`p_slot` is 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. - **`PersistedBondTicket` persist-before-use typestate** (the cross-split seam): `Engine::persist_bond_record(..) -> PersistedBondTicket` is the sole producer; the ticket is `!Clone`, consumed by value, and minted only after a durable `save_state`, so 2c-2b's `sign_bond(ticket, ..)` makes sign-before-persist uncallable. - **Operation-scoped `PersonaHandle`** minted only for held personas (collapses the can't-happen `PersonaUnreachable` to one slot→handle boundary; keeps `LookaheadExhausted` as a real domain error), `!Clone`, with an actor `generation` counter that rejects a stale handle after rotation (`StaleHandle`) — closing the use-after-wipe on a wiped ephemeral persona. - **`HeldPersona` Bonded/Ephemeral** with a `wipe_ephemeral` path that consumes only the `Ephemeral` variant, 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's `StakeMasterSeed` root, in-handler `spawn_blocking`, and in-actor derivation error path. Landed **inert**: wired, derived, spawned, and test-exercised (12 `stake_engine` + 2 `stake_persist` tests, 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 binds `tx_prefix_hash`, not the proof) is pinned now so the schema and derive-forward set are correct when 2d wires submission. See `FOLLOWUPS.md` "StakeEngine Model D wiring" for the deferred-work reopens. - **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 test `join_market_bond_post_signs_and_verifies_over_real_tree` (in `shekyl-engine-core::engine::local_pending_tx::tests`) spends a drained leaf of the consistent `funded_ledger_and_tree` fixture, assembles its membership path via the production `CurveTreeClient::assemble_path` (the same path `build_then_submit_marks_outputs_spent` drives), carries the bond's `bond_credit` through `shekyl-tx-builder::sign_transaction_with_terms` as the sole output-side cleartext term over genuine depth-2 branch layers, and checks BP+, the RCT balance over prover-emitted commitments (with `8*C → C` cofactor recovery), and `verify_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 upstream `Fcmp::prove`↔`verify` roundtrip (no workspace test verifies a real multi-layer path today; the first attempt returns `BatchVerificationFailed`), so it rides as an `#[ignore]`d sibling (`join_market_bond_post_fcmp_verify_over_real_tree`) rather than a faked pass. See `FOLLOWUPS.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 new `kameo` actor (`shekyl-engine-core::engine::stake_engine`) that owns the wallet's `master_seed_64` and the active archival persona `P`, structurally disjoint from the `LedgerEngine` transfer pipeline. It mirrors the `KeyActor`/`KeyEngineHandle` discipline: - **Operation-shaped protocol, public-only replies.** Messages name operations (`ActivatePersona`, `ActivePersona`); replies carry only public material -- `PersonaIdentity` holds the typed `HybridPublicKey` (`P_pubkey`), a type with no secret field to leak. The secret `ArchivalPKeys` bundle (`!Clone` + per-field `ZeroizeOnDrop`) 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; `StakeMasterSeed` newtype (`Zeroize`/`ZeroizeOnDrop`, not `Clone`, redacted `Debug`) so the wipe discipline lives on the type; typed `StakeEngineError`. - **Lazy derivation off the hot path.** An idle actor holds no persona (`active == None`); the first `ActivatePersona` derives via `derive_archival_p_keys` on `tokio::task::spawn_blocking` (PQ keygen is CPU-bound). The only copy of the seed leaves as a `Zeroizing` buffer 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_64` on every (re-)spawn. - **Fail-stop, not supervised.** A handler panic returns `ControlFlow::Break`; afterward every handle call collapses to the terminal, user-facing `StakeEngineError::StakeActorUnavailable` ("close and reopen the wallet to recover"). Recovery re-derives every `P`. Landed **inert**: registered and exercised by tests only (idle-has-no-persona, lazy derive matches the `ARCHIVAL_P_DERIVE_V1` oracle, 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. - **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 test `join_market_bond_post_signs_and_verifies_through_prover` (in `shekyl-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 with `shekyl-archival-bond-builder`, carries its `bond_credit` through `shekyl-tx-builder::sign_transaction_with_terms` as 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 under `P_pubkey`, the Bulletproof+ range proof, the FCMP++ membership proof, and `verify_bond_post_rct_balance`. It also asserts the verify side *rejects* a wrong `bond_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.rs` emits output commitments as `8*C` (wire cofactoring) while the balance equation is defined over the prime-order `C`, so the KAT recovers `C = (8*C) * 8^{-1}` before the balance check. Adds `shekyl-archival-bond-builder` + `shekyl-archival-retention` as `shekyl-engine-core` dev-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 block `tip − 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 the `SnapshotId` check 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-4 `PendingTxActor` migration 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 a `content_gen` that 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 as `SubmitError::ContentChanged` for re-confirm; broadcasting unauthorized content is unrepresentable. - `ConsumerHeldEntry` carries the rebuild substrate (`request`, `reference`, `content_gen`, fingerprint); `PendingTx` exposes `content_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; see `FOLLOWUPS.md`. - **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-frozen `shekyl-archival-retention` verify side. Three pieces: - **`shekyl-rct-balance` (new crate)** single-sources the genesis-frozen RCT cleartext balance equation `sum(pseudoOuts) + extra_inputs = sum(out_masks) + fee + extra_outputs` and its typed-side terms. The side is a *type*, not a runtime tag: `InputTerm` and `OutputTerm` (checked-`u64` newtypes over `AtomicUnits`) make a wrong-side cleartext term unrepresentable. The verify equation that previously lived in `shekyl-archival-retention::bond_rct_balance` migrated here; `bond_rct_balance` now wraps it with the bond-specific credit-xor-debit term rigidity. Imported by *both* `shekyl-tx-builder` (construct) and `shekyl-archival-retention` (verify), so the two cannot diverge on a consensus rule (§7.2 / §11.1 Q2). - **`shekyl-tx-builder`** gains `sign_transaction_with_terms`, threading the `shekyl-rct-balance` typed-side terms into funds-sufficiency validation; `sign_transaction` is 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 JoinMarket `ArchivalBondPostVin` (`bonded_total == bond_credit == bond_floor`, `bond_debit == 0`), signs its preimage with the `P` identity hybrid key, supplies `bond_credit = floor` as the `OutputTerm`, 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`) derives `P`, constructs + signs the vin, and confirms `verify_join_market_bond_post` + `verify_bond_post_rct_balance` accept 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, no `bond_credit` in the RCT balance, no `P`-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 real `CurveTreeClient` (CT-5); (b) `shekyl-tx-builder` gains a **generic** extra cleartext balance term (the way `fee` is one; the verify side already treats `fee` and `bond_credit` symmetrically as `amount_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_draw` against the wallet's actual CSPRNG), not just `draw_entry_gap`. Review target 2-3 rounds (not 4-6: the construct side is largely determined by the validated verify side). **PR 0 (`archival_p` derivation) 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 and `P`↔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 into `shekyl-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-standoff`** ships a float-free `draw::draw_entry_gap` (`s ~ U[0, window]` via unbiased integer rejection sampling over a minimal `GapRng` trait) 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-generic `certify_draw` self-cert harness behind a `conformance` feature. 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) and `tests/conformance_grading.rs` (`conformance` feature, float grading, x86-only because float is not bit-identical across arches). **`shekyl-stats`** is the new dependency-free home for the generic primitives (`chi_square_upper_crit`, `Z_ALPHA_1E6`, discrete-uniform chi-square, `lag1_autocorr`); `shekyl-staking-sim` and `shekyl-crypto-pq` (its `enc_label` test, 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::sign` now emits `outPk[i] = C = mask·G + amount·H` (×1) instead of `8·C`. The consensus balance check `sum(pseudoOuts) == sum(outPk) + fee·H` (`verRctSemanticsSimple`) sums these points directly and the pseudo-outs are real `C`, so the output side must match — the prior `8·C` broke balance verification. Surfaced and validated by the new `shekyl-wire/tests/fcmp_spend_e2e.rs` (its `verify_rct_balance` stage). The C++/Rust FFI spend-path debugging that surfaced this was otherwise reverted as out-of-scope C++ debt; the findings are captured in `FOLLOWUPS.md` ("C++ FCMP++ wallet send path incomplete"). The wire/scanner contract already required real `C` — the production scanner recomputes `z·G + amount·H` and byte-compares it against the on-wire commitment, and `sign_bridge` copies `SignedProofs.commitments` straight onto the wire — so the fix aligned the builder with the rest of the stack. The downstream documentation and the two `shekyl-engine-core` bond-post round-trip test helpers (which had compensated for the old encoding by multiplying by `8⁻¹`) were reconciled to feed the real `C` directly. ### 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_transaction` route (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_tx` endpoint (§9.3):** handler, epee route maps, the `COMMAND_RPC_SEND_RAW_TX` defs with their eleven-boolean reply schema, the `core_rpc_ffi` DJSON dispatch entries, and the `shekyl-daemon-rpc` axum proxy routes. `wallet2::commit_tx` ports 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++ (empty `key_offsets` → unconditional early-return `true`). - **`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_transaction` helper:** superseded by `submit_transaction` (PR-3). - **Kept per §9.4:** the P2P `add_tx` ingestion path (with the PR-3 double-spend regression pin), `tx_verification_context` as an internal P2P contract, Dandelion++ relay machinery, and the relay-only `on_relay_tx` operator 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`](design/RANDOMX_V2_RUST.md) §15) and the named blocker for the deferred Phase 3c/4 PoW C-core cleanup ([`FOLLOWUPS.md`](FOLLOWUPS.md)); this PR unblocks that cluster. Plan: [`docs/completed/LEGACY_POW_CLEANUP_PLAN.md`](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()`, the `CHECK_PAYMENT*` macros and all per-handler pay-gates, the `m_rpc_payment` / `m_rpc_payment_allow_free_loopback` members + `init()` plumbing, the `COMMAND_RPC_ACCESS_{INFO,SUBMIT_NONCE,PAY,TRACKING,DATA,ACCOUNT}` endpoints (structs + handlers + `core_rpc_ffi.cpp` dispatch), the `rpc_payments` CLI command (`command_server`/`command_parser_executor`/`rpc_command_executor`), and the payment parameters from `bootstrap_daemon`. `RPC_TRACKER` is kept as pure perf timing (its `RPCTracker` payment-tracking class is gone). - **Wire contract:** `rpc_access_request_base` / `rpc_access_response_base` (the `client` / `credits` / `top_hash` fields) collapse into the plain `rpc_request_base` / `rpc_response_base`; `CORE_RPC_VERSION_MINOR` `15 → 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 the `payment_required` error (`wallet_errors.h`). - **Rust wallet:** removed the orphaned `RpcPrefs` persisted-prefs bucket (`persistent_rpc_client_id`, `auto_mine_for_rpc_payment_threshold`, `credits_target`) from `shekyl-engine-prefs` — it had no reader or writer; `PREFS_SCHEMA_VERSION` `2 → 3` (pre-genesis: `rm -rf ~/.shekyl`, no migration owed). `shekyl-daemon-rpc` drops the three `rpc_access_*` restricted-method names; the Python RPC test framework drops its six `rpc_access_*` wrappers. - **Out of scope (still deferred, tracked in `FOLLOWUPS.md`):** RandomX **v1** (`rx-slow-hash.c`) is retained as the consensus rollback hatch; CryptoNight `slow-hash.c` stays (live legacy C++ KDF callers — `chacha.h`, `cryptonote_format_utils.cpp` — migrate to argon2id first); the `RX_BLOCK_VERSION` `#define` stays (still referenced by `mining_parity.cpp` / `chaingen.cpp` on the PoW-dispatch surface); the `IPowSchema` / `pow_registry` / `shekyl-consensus` abstraction layer stays. ### Changed - **wallet: submitter error split from the reservation-bound orchestrator error** (`docs/FOLLOWUPS.md` "Submit-error reservation-id placeholder", closed; 2c pre-wiring). `TransactionSubmitter::submit` now fails with a reservation-unaware `SubmitterError` (`RejectedTerminal` / `RejectedRetryable` / `Ambiguous` — a closed enum carrying no `ReservationId`); the reservation-bound `SubmitError` arms are produced only by `LocalPendingTx`'s finalizers, which bind the reservation under submit. The former `ReservationId::new(0)` placeholder in the submitter arms is unrepresentable, so the 2c `StakeEngine` submit 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: `AlreadyInChain` submit verdict gets its distinct §2.5 lock-lifecycle disposition (interim)** (`docs/FOLLOWUPS.md` "`AlreadyInChain` submit verdict", still open — F40 conformance pending; 2c pre-wiring; `DAEMON_SUBMIT_VERDICT.md` §2.5/§2.6). `TransactionSubmitter::submit` now succeeds with `SubmitSuccess`, splitting the `Ok` arm by disposition: `Broadcast` (`Accepted` / `AlreadyInPool`) places the F14 awaiting-confirmation lock as before; `AlreadyInChain` resolves through the new `finalize_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 (no `mark_spent` re-observation, watchdog confirmed-absent release unbuilt). **Note:** this change predates the F40 design merge (`AlreadyInChain { height }`, §2.2 carve-out); it carried no `height` discriminant and implemented only the "no lock" shape the design round superseded. The F40 reshape landed in this same unreleased span (see the `AlreadyInChain { 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: the `Conceal` path 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 at `disclose_pool_presence` (`MustFullyVerify` on Conceal / `FastPathEligible` on 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 are `pub(crate)` with a single constructor reachable only from `disclose_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-visible `MustFullyVerify` conversion naming F41. Implementation obligations and the ordering constraint (tokens + tests precede the first perf/cache PR touching submit) tracked in `FOLLOWUPS.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 + `PBoundBytes` equality 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 raw `Vec` 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 the `PBoundBytes` value 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 FOLLOWUPS `AlreadyInChain` design half).** The unit `AlreadyInChain` verdict 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-block `height`: the lock is placed in both height cases, and `height` routes 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.md` endpoint coverage recounted post-deletion (the native Rust `/submit_transaction` route 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 the `FOLLOWUPS.md` `fcmp_root_stale` item is **closed ahead of its V3.1 target** — the `ProofStale` reversion clause's reopening criterion fired, satisfied by `RejectCause::StaleRoot` with finer cause resolution than the item's own spec. - **wallet: submit path cut over to the typed `SubmitVerdict` contract** (`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_transaction` posts the native `submit_transaction` route and returns `SubmitVerdict`; the string-keyed `TxRelayResponse` is deleted. The transaction id is computed locally from the held bytes, never read from the daemon (wire minimalism, §2.2). - **Outcome reshape.** `TxSubmitOutcome` is the 1:1 wallet projection of the verdict (`Submitted` / `AlreadyInPool` / `AlreadyInChain` / `Rejected { cause }`). The wallet-side `AlreadyKnown` dedup heuristic is retired in favor of daemon-attested identity facts, and the formerly-deferred `ProofStale` detection is now constructible as `RejectCause::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`, or `Unrecognized` rejection trips a circuit breaker: further builds are refused with `SendError::SubmitLoopBreakerTripped` until 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 **persisted** `awaiting_confirmation` lock (tx hash + accepted height) on each input, released either by observed chain confirmation (confirmed-present → spent-final via `mark_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_VERSION` 7→8, `WALLET_LEDGER_FORMAT_VERSION` 9→10; balance reporting gains an `awaiting_confirmation` bucket excluded from `unlocked`. - **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. - **wallet: migrate the scan/refresh block-parse from `shekyl-oxide` to `shekyl-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'd `shekyl-wire` reader instead of the legacy `shekyl-oxide` block/tx serializer. - **`ScannableBlock` relocated + re-typed.** It is no longer a `shekyl-rpc` type built from `shekyl-oxide`; it is a `shekyl-wire`-typed struct owned by `shekyl-scanner` (`block` / `transactions` / `first_output_index`). The `output_index_for_first_ringct_output` field is replaced by `first_output_index: Option` (sourced from `get_o_indexes`), retiring the inherited `ringct` name (closes the FOLLOWUPS `ringct`-residue rename item for this surface). - **Native fetch.** Blocks are fetched through the new `DaemonEngine::fetch_scannable_block` (default impl in `engine/block_fetch.rs`), which drives the `shekyl-rpc` transport and parses via `shekyl-wire`. `scan.rs` and `engine/curve_tree_decode.rs` consume the wire types directly; `unlock_time: u64` is mapped to the scanner's `Timelock` at the wire→scanner boundary. - **Fixes** `Engine::start_refresh` failing at block fetch with `RpcError::InvalidNode("invalid block")` against a live daemon: the `shekyl-oxide` parse dropped the coinbase `Null` confidential-tx committed base, so real daemon blocks would not deserialize. New live regtest acceptance gate `e2e_refresh_scans_coinbase_balance` mines to the wallet and asserts a matured coinbase balance after refresh. - **Removed** the now-dead `shekyl-rpc` `ScannableBlock` struct and its `get_scannable_block` / `_by_hash` / `_by_number` default trait methods (zero callers post-migration; `15-deletion-and-debt.mdc`). - **Scope.** This is the scanner/refresh slice only; the `shekyl-tx-builder` spend-encode cutover and the final `shekyl-oxide` block/tx deletion remain (the scanner still imports the `Timelock` / `StakingMeta` domain types from `shekyl-oxide::transaction`). See [`docs/design/GENESIS_TX_WIRE_FORMAT.md`](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 from `shekyl-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**. See [`docs/design/RANDOMX_V2_RUST.md`](design/RANDOMX_V2_RUST.md), [`docs/design/RANDOMX_V2_PLAN.md`](design/RANDOMX_V2_PLAN.md), and [`docs/design/RANDOMX_V2_PHASE3_PLAN.md`](design/RANDOMX_V2_PHASE3_PLAN.md). - **Removed from the consensus path** (`60-no-monero-legacy.mdc`): `src/crypto/pow_cryptonight.cpp` and the `get_cryptonight_*` schema; `get_pow_for_height` collapsed to RandomX for every block version; all `RX_BLOCK_VERSION` consensus guards; both block-`202612` *longhash* fossils plus the sibling block-id-`202612` fossil in `calculate_block_hash` (whose vestigial `blob` param was dropped); the dead CryptoNight RPC branch; and the transient `SHEKYL_RANDOMX_V2_VERIFY` build flag (the v2 path is now unconditional). Seed-hash resolution is unconditional. - **New FFI surface:** `shekyl_pow_randomx_v2_hash` and `shekyl_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 is `Keccak(header)` — PoW-independent. `mining_parity.cpp`'s `genesis_identity_is_pow_independent` freezes the per-net genesis nonce + block id (mainnet `919f8db5…` at the time; the credit-wire `attestation_root` header field, PR #395, later moved every genesis id — mainnet is now `bcdcf5e0…` — still cross-checked against the daemon-captured height-0 hash in `rust/shekyl-wire/tests/vectors/regtest_coinbase_hashes.json`); the frozen coinbase/block-hash vectors needed no regeneration then, and were regenerated with #395. - **Deferred to Phase 3c/4** (blocked by the RPC-payment subsystem deletion + `wallet2.cpp` touchpoints; tracked in [`docs/FOLLOWUPS.md`](FOLLOWUPS.md)): deleting `rx-slow-hash.c` / `slow-hash.c`, the `seedheight` export + `shekyl-pow-randomx::consensus` module, the `RX_BLOCK_VERSION` `#define`, and the `IPowSchema` / `pow_registry` / `shekyl-consensus` abstraction layer. - **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. `build` splits into synchronous selection → one batch `AssembleTx` actor round-trip → a fold into the signing context (the tx-level `ReferenceBlock` carries the consensus root + block hash; the per-input paths carry the real leaf chunks and branch layers). Owned outputs resolve by `gindex` (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_index` numbering equivalence; the assembler's reference param drops to `ReferenceBlock` (T1) and the depth is read from the assembled path / a `root_and_depth_at` snapshot rather than a hardcoded `1` (T2). A spend now **requires** the curve tree — the no-tree synthetic fallback is gone (`synthetic_tree` survives only `#[cfg(test)]` for the weight/signing KATs that need depth-controlled fixtures; the A4 reversion clause of `CT5C_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 real `assemble_path` and corrected to `c2 = ceil(B/2), c1 = floor(B/2)`. - **Calibrates the fee model against measured proof sizes:** the deferred-to-CT-5 placeholder (a flat `+320` bytes/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. - **wallet/scanner: `eligible_height` now 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_output` previously set `eligible_height = block_height + SPENDABLE_AGE` unconditionally. 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's `lock_blocks` elapse), 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_height` is now `max(block_height + SPENDABLE_AGE, additional_timelock_block, stake_lock_until)`, agreeing with the tree's insertion height. A wall-clock `Timelock::Time` is 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 the `m`-of-`n` availability margin (L14b condition (b), provisional `11/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-`P` cover 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.md` funding-seam entry-standoff carry (3). - **archival-sim: mid-band `age_weight` lever 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 at `age_weight >= 7`, and the pre-seal lean was to price it into genesis. A fine characterization sweep (`hu_cushion_*`: `age_weight` 3→8 × three seeds) refuted the lean: the 7th replica is **seed-dependent** (reached in only two of three worlds at any `age_weight >= 5`; seed `0x5EED_2222` never 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_under` already `0.0000` at `age_weight = 3`; `gini`/`max_actor_share` flat across 3→8, confirming `age_weight` redistributes a fixed budget rather than scaling emission). Genesis keeps `age_weight = 3`, `r_target_deep = floor + 1`, no cushion — the lever buys no reliable redundancy and costs nothing to leave alone. Replaces the prior `hu_lever_*` sweep with the finer `hu_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.md` mid-band lever item, `STAKER_ARCHIVAL_SIM.md` §L18 finding 6. - **archival-sim: faithful `HoldingsUpdate` release-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 pushed `epochs_remaining = C` but `advance_epoch` decrements before the next best-response, so capital froze for `C − 1`) and, more materially, let `best_response` recycle 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_response` now **pre-charges** the collateral of every deep shard held at epoch start (it seeds `used_bond`, so a same-epoch drop cannot refund a fresh acquisition); pre-charge (drop epoch) + escrow (next `C − 1`) span the full `RELEASE_COOLDOWN_EPOCHS`. `release_cooldown_epochs == 0` pre-charges only locked shards, so every `c0` arm 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 all `c2` gates, so this strengthens the seal (binding `committed_deep_under` moves `0.0138 → 0.0000`) rather than altering calibration. The model correction reveals the `c4` "cliff" was an artifact of the spurious churn (gone; floor holds at `6`), confirms the rationality-preclusion thesis in the budget arithmetic, and surfaces a mid-band `age_weight` lever (`aw ≥ 7` reaches a 7th oldest-band replica) now tracked in `FOLLOWUPS.md`. Also guards `freeze_blocks_recoverage` against `bond_rate <= 0` (#1) and aligns the `oldest_margin` doc comments to the implemented `>= 0` hold-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 for `RELEASE_COOLDOWN_EPOCHS` at all. Added `AgentParams.dodge_pref` / `SimConfig.dodge_pref` and a `hu_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: at `c0` the dodge is pure churn (`1.364` vs rational `0.268`) with **no** committed-coverage harm (`oMinCmtR` holds at `6`, instant re-seat); at `c2` the 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** — every `ship` arm (locks on) holds `oMinCmtR` at `7` at both `c0` and `c2`. 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 the `ship` config): 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 on `BOND_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_sample` reported `uniform_ok = true` for 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 requires `n >= 2` before a uniformity claim. `summarize_gaps` on an empty sample took the `denom = 1.0` fallback and manufactured a `max_decile_dev` of `0.10` (a real-looking *failing* shape) for no data; it now returns explicit zeros. Both are locked by new `empty_sample_summary_does_not_manufacture_a_shape` / `no_observations_cannot_claim_uniformity` tests. The `conformance`-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.yml` now runs `cargo test -p shekyl-standoff --features conformance` on 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 `u64` window and the chi-square critical value fails closed on invalid `df` (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's `u64` contract. `correlated_walk` computed its modulus as `window as i64 + 1`, which wraps to a non-positive modulus (panic in `rem_euclid`) for `window > i64::MAX`; it now walks with overflow-safe `u64` modular add/sub. `grade_sample` sized its bin count with `window as usize + 1`, which overflows at `usize::MAX` and truncates a large window on a 32-bit target; it now computes the count in `u64`. `chi_square_upper_crit` divided by `df` without guarding, yielding NaN/inf for `df <= 0`; it now returns NaN explicitly, which makes any downstream `statistic < crit` comparison false — the cert fails closed rather than passing on a bogus critical value. Locked by `helpers_are_well_defined_at_extreme_windows` (`u64::MAX` window, no panic) and `critical_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.16` to `gungraun 0.19` (2026-06-16, `chore/bench-gungraun-migration`).** `iai-callgrind` was renamed to `gungraun` upstream 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`) move `iai-callgrind = "0.16"` → `gungraun = "0.19"` (latest stable, 0.19.2), the 13 `_iai` bench files swap `use iai_callgrind::*` → `use gungraun::*` (macro API is source-compatible — no structural changes), and `benchmarks.yml` + `capture_rust_baseline.sh` install/invoke `gungraun-runner` (the `_iai` filename suffix and the `iai_callgrind` JSON envelope section key are retained as stable identifiers consumed by `compare.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 from `docs/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 the `instructions=0` capture flake (the producer guard from PR #35 that was hard-failing per-PR `capture-pr` jobs) 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's `header.curve_tree_root`). A mismatch is the inconsistent-liar daemon: a terminal `RefreshError::CurveTreeIngest { recoverable_by_respawn: false }`, before the ledger advances (O2). New production `VerifyRoot` actor message. - **C2 reference-block spendability gate.** Selection now rejects outputs too fresh for the reference block (`eligible_height > tip − REF_ANCHOR_AGE`) with a clean `SendError::OutputNotYetSpendable { eligible_height, reference_block_height, wait_blocks }`, and the pre-maturity window with `SendError::WalletTooYoungToSpend` — distinct from the self-healing `SpendUnavailableRebuilding` (tree lag) and from `InsufficientFunds`. - **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 a `ReferenceBlock`, threaded into assembly. The signer still builds synthetic membership paths (the assembler cutover and `assemble_path` are CT-5c); only the reference *selection* is real. KATs pin the §3.3 mismatch rejection, the C2 `OutputNotYetSpendable` wait signal, the too-young window, and the reference-root threading. - **archival: long-lived `P` is 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 pseudonym `P`) is closed in favor of **long-lived `P`**, 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/rotating `P` is 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 (per `21-reversion-clause-discipline.mdc`): a measured S-2/S-3 per-`P` attribution breach severe enough to reconsider the confidential-staking rejection, via a fresh `PHASE_2B` model-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`, `HoldingsUpdate` drop-shard) is now authorized by a **dedicated `bond_spend_pk`** — a hybrid `scheme_id = 1` (Ed25519 + ML-DSA-65) keypair committed into the `ArchivalBondRecord` at `JoinMarket` and 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 "`P` hybrid 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 at `JoinMarket`, and added to the `ARCHIVAL_P_DERIVE_V1` KAT obligation. Credit/identity paths (`JoinMarket`, `Rebond`, `HoldingsUpdate` add-shard) still authorize against `P_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 is `Unbond` + 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 in `PHASE_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 — `HoldingsUpdate` release-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_cooldown` sweep freezes released collateral for the release cooldown (`World.cooling`; spendable budget reduced by frozen capital in `best_response`). Two source-faithfulness corrections vs. `ARCHIVAL_BOND_GATE4.md` — the frozen amount is **flat `ARCHIVAL_BOND_FLOOR`** (§8.1, not the first-draft age-scaled `bond_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: genesis `RELEASE_COOLDOWN` passes all three absolute seal gates (`committed_deep_under < 0.10`, `sole_source = 0` lag-free, hold-the-floor `oldest_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 makes `bond_duration(age)` genuinely age-scaled, so the rosier flat `0.0064` is the optimistic contrast, not the headline). The flat-floor freeze costs `~0` floor erosion at genesis, so **`r_target_deep` requires no freeze-driven increase**. The `+1` redundancy is a **provisioning** property of `r_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_weight` 3→12) lifts `oldest_min_committed` above `r_target_deep`, because the `1/R` reward is anti-over-replication by construction; a wider band is provisioning-only (raise `r_target_deep`, or lean on the foundation backstop), both priced. The "harmless at `c2`" result is the **conjunction** (`freeze_harm_co` bounded AND `oldest_min` holds), **not** `causal = 0`: the causal/transient detector reads `0` in every epoch of every arm including `c4` where the floor eroded `6→5`. The `0` is **precluded under cooldown-aware optimization, not by construction** (`best_response` budgets on `capital−Σfrozen`, so it never makes the stranding drop-to-reallocate; validated by positive/negative control tests `freeze_predicate_*` in `model.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 by `oldest_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-linear `2→4` degradation 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_label` real-label gate; prove the indistinguishability invariant (2026-06-15, `feat/enc-label-ungate`).** The cooperative-payment-request gate (`operational.cooperative_payment_requests`, env `SHEKYL_COOPERATIVE_PAYMENT_REQUESTS`, `payment_request_flag.rs`, and the `cooperative_enabled` parameters threaded through `outbound_label.rs`, `attribution.rs`, `merge.rs`, the FFI, and `cryptonote_tx_utils.cpp`) is **deleted**. The gate delivered no privacy or consensus value: the universal sentinel-XOR-`k_label` masking 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 URI `rid` when present; receive always classifies/matches. The indistinguishability invariant is now stated normatively in `SUBADDRESS_UNDER_PQC.md` §5.7.10 (cross-referenced from `FCMP_PLUS_PLUS.md` §4) and proven by the statistical KAT `real_label_indistinguishable_from_sentinel` in `shekyl-crypto-pq/src/label.rs` (chi-square uniformity + homogeneity over the real `derive_output_secrets` path, with a plaintext-on-wire negative control). GUI tooling emitting `rid` URIs is the de-facto feature boundary. **Breaking:** wallet config files carrying `cooperative_payment_requests` no 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 drive `ingest_scan_result_into_curve_tree` with `OwnedTxLeaves` parsed from the `ct2_tier_a` oracle and assert the engine-reconstructed root byte-matches the oracle at **every** height: a forward ingest of the `main` chain, and a `reorg_rewind` onto `reorg_deep` exercising engine-path rollback + re-ingest. Root read-back is a `#[cfg(test)]`-only `RootAt` actor 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 (kameo `on_panic` does not restart it, by design) or whose client is `ClientError::Poisoned` is now healed engine-side by a drop-and-reopen respawn rather than failing the refresh permanently. `CurveTreeHandle`'s inner `ActorRef` is wrapped in an `Arc>` shared, swappable cell so a respawn propagates the fresh actor to **every** handle clone at once (including the `LocalPendingTx` spend-gate clone) — without the shared cell a respawn would heal refresh but leave spends pointing at the dead actor forever (a partial heal). The new `CurveTreeHandle::respawn` does kill → `wait_for_shutdown` → reopen → spawn → atomic swap; because kameo drops the actor value (and its redb single-writer lock) *after* `wait_for_shutdown` returns, 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::CurveTreeIngest` gains a `recoverable_by_respawn: bool` classification (`true` for a fail-stopped actor or `ClientError::Poisoned`; `false` for non-consecutive-height / root-mismatch / store / decode failures a reopen would only reproduce). `Engine::ingest_scan_result_with_respawn` wraps the bare ingest with a single respawn-and-retry on a recoverable error and is wired into both refresh ingest call sites (async `run_refresh_task` and 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) and `ingest_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` (the `watch`-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) and `rebuilding_membership` (`true` while 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's `SendError::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 full `ScanResult` and emits it on the pre-merge `Merging` frame (with `rebuilding_membership` read 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 new `RefreshProgress::phase_only` constructor. KATs: `membership_rebuilding_predicate` (the pure ledger-ahead-of-tree predicate across fresh/adopting/caught-up/ ahead) and `hybrid_refresh_from_genesis_surfaces_not_rebuilding` (the wiring smoke test). A non-zero pending-incoming amount and the `rebuilding == true` path 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_clusters` with an epoch-snapped negative. (2) **serial independence** — load-bearing now that rebond/partial-unbond/re-entry make a `P` draw 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 + `--standoff` report 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) and `summarize_gaps` (realized `(spread, order)` distribution: mean spread, order balance, first-decile mass, decile uniformity probe) to `rust/shekyl-staking-sim/src/standoff.rs`, with two validating tests — `correct_draw_is_well_distributed` (flat spread, balanced order) and `double_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 `--standoff` report 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 intent `t0`, draw `s ~ U[0,600]`, event 2 at `t0+s`, fair order-coin (uniform separation, free inversion, max latency 600, per-`P` independence). 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.md` item 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 `--standoff` recommendation (`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 is **`P(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). New `FOLLOWUPS.md` V3.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.md` ledger. Added a tracked row (target version + reopening trigger per `21-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, the `recon_tier_b.rs` `#[ignore]` set) — V3.0, after CT-5; (2) **CT-1 full-segment freeze/prune KAT at `j=2`** (~26k leaves/segment vs the CI-scale `j=0`/Tier-A coverage) — V3.0, with prune-policy; (3) **wallet-local `O.x → position` match index** (§4.3 scan cost) — V3.x perf; (4) **confirm segment layer `j` / shard size `E` against mainnet leaf growth** (§7.2.2, level-2 ≈26k provisional) — V3.x, with the `ArchivalEngine` shard policy; (5) **anonymized (Tor/I2P) routing for non-forward segment fetch** (§7.4) — V3.0, riding the `SegmentSource` seam. 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 from `check_consensus_invariants.sh`) now rejects `f32`/`f64`, `usize`/`isize`, and atomics in `reward_arithmetic.rs`, with an inline `reward-arith-allow` escape 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 a `usize` leaking 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_deep` floor and any sealed `R_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 in `ARCHIVAL_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).** Added [`CT4_ROUND1_CLOSEOUT.md`](completed/CT4_ROUND1_CLOSEOUT.md) recording the two landed CT-4 surfaces: (1) `assemble_path → AssembledPath` (gated on the §3.3 root match, FCMP++ prover `Path` layout pinned at source, C3 `c1+c2+1 == depth` self-checked) and (2) `reference.rs` validity-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 (`ReferenceBlock` now carries `block_hash`). Flipped `CURVE_TREE_CLIENT.md` §9 CT-4 row to **Round 1 closed**. Test gates: `assemble_kat` (independent root recompute + `OutputNotDrained`/`RootMismatch` rejection), `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_p3` path-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).** Added [`CT3_ROUND1_CLOSEOUT.md`](completed/CT3_ROUND1_CLOSEOUT.md) recording 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. Flipped `CURVE_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). Marked `CT3_SYNC.md` CT-3d landed / Round 1 complete. The bulk-leaf RPC endpoint (R1-Q1) and `SegmentSource` seam (R1-Q5) remain deferred-with-recorded-shape, landing with the post-prune refetch path; the F5 store-backed assembly, resume-path/full `R_k` recheck, and CT-5 poison-reaction items stay routed in `FOLLOWUPS.md`. Docs-only; no code or consensus surface touched. - **archival: staking-sim `--standoff` mode — 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 like `fetch_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 in `STAKER_ARCHIVAL_SIM.md` §*Funding-seam entry standoff* and `ARCHIVAL_FIREWALL_GATE6.md` §10.12. New code + docs; 5 unit tests; existing 325-scenario economic sweep untouched (new mode is opt-in via `--standoff`). - **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 `g` operating-band in `REWARD_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-`P` sybil 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 in `ARCHIVAL_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 to `P_canonical_id` because 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 (every `P`'s reward stream traceable to `P_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) in `ARCHIVAL_FIREWALL_GATE6.md`. **S-1:** the money seams (GF-7 funding-in, GF-4 value-out) are the only findings protecting principal↔`P` unlinkability 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_id` bridge 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-lived `P` is 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 the `P`-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 ("`P` hybrid signatures on vin") already presumes the account `hybrid_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 **only `P` path 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 from `bond_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 carries `JoinMarket`/`Rebond`/full `Unbond` + cooldown (`=2 < W`) + `bond_duration(age)`; **voluntary partial-unbond is `HoldingsUpdate` (`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`); since `bond_duration` peaks 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 L14b `m`-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.9 `P`↔principal client/circuit isolation** exit pin (forks 1+4: independent Arti clients/guards + restore-flow discipline — `StakeEngine` must not co-launch `P`'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 that `P`'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-service` feature on `arti-client` 0.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, `P` refuses clearnet-arriving challenges (loud, no fallback); **GF-5** — pre-join backing presentation pinned to a fresh anonymity circuit with no principal stream reuse (it is `P`'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-chain `P`-typing is public-by-function and *not* the concern, tuned ratio carried to testnet replay; **GF-9** — HS identity key `p_slot`-bound + **seed-derived via a new §9.3 HKDF label** (`launch_onion_service_with_hsid`), making `.onion` rotation structural and seed-recoverable, serving-side key-compromise residual named (and the new label must enter the `ARCHIVAL_P_DERIVE_V1` KAT). 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 — `P` lifecycle + pseudonym hygiene (2026-06-13).** Adversarial-pass disposition on `ARCHIVAL_FIREWALL_GATE6.md` §9. GF-1 (critical): rewrote §9.6 with a per-transaction-type verifier contract — the account-level `hybrid_sign_pk` is the bond-record **identity only** (the on-wire `P_pubkey` feeding `P_canonical_id`) and is **never** a per-input `PqcAuthentication.hybrid_public_key`; emission **backing inputs** authenticate against the leaf-committed per-output `pqc_pk` the membership proof binds in-circuit (`FCMP_MEMBERSHIP_ONLY.md` §7, no key image), fee inputs / ordinary transfers / terminal drains use per-output keys, and ordinary `P` transfers carry no `P`-typing (byte-identical to principal transfers — the firewall property, not a gap). This aligns the doc to the already-implemented `FcmpMembershipOnly` contract; no consensus-rule change. GF-2 (high): made the dual-scan firewall **architectural** rather than a naming convention — `StakeEngine` owns `P.view_sk` as an identification context disjoint from the principal `LedgerEngine` scan, separation rests on distinct `combined_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; added `P`-scan ownership rows to the §5 consumer map. Corrections: GF-8 (the §9.3 `L = …` placeholder was cosmetic — per-row `L` is pinned `64/64/64/32`, matching `account.rs` and `derivation.rs`), GF-11 (`MAX_CLAIM_AGE_W = 26 > MAX_SETTLEMENT_EPOCHS_PER_ EMISSION = 15` already pinned in `ARCHIVAL_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-circuit `H(pqc_pk)` extra scalar — implemented; the vin-layer ML-DSA equality check is a not-yet-landed hard merge blocker that must precede the `archival_p` impl + emission verifier); **C-2** — re-anchored the GF-2 ownership boundary, since PHASE_2B §4.6 is claim-era/pending-retool: `StakeEngine` sole ownership of `P.view_sk` is 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 the `MembershipSpendAuth` `R_O` leg 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 §7 `pqc_pk`-mismatch forgery negative rejects until the vin-layer ML-DSA equality check lands), with a stressnet negative-case obligation. The `ARCHIVAL_P_DERIVE_V1` KAT manifest + `shekyl-crypto-pq::archival_p` implementation 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 the `REWARD_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_stake` deletion surface enumerated; `07-consensus-atomic-cutovers.mdc` evaluated (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 behind `shekyl_emission_vin_verify` joining the `shekyl_archival_*`/`shekyl_fcmp_*` FFI family; C++ keeps only the `txin_v` variant + 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`'s `Transaction` hardcoded wire version 2, but Shekyl's sole admissible transaction version is 3 (`CURRENT_TRANSACTION_VERSION`, `src/cryptonote_config.h`; the daemon rejects `tx.version < 3`). The Rust reader gated on `version != 2` and 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::read` gate, `write` emit, `version()`, and the `TransactionPrefix::hash` preimage — the last keeps the tx hash in sync with the daemon's `get_transaction_prefix_hash`) and renamed the single enum variant `Transaction::V2` → `Transaction::V3` so the type reflects the one admissible wire version. The scanner's coinbase `tx.version() != 2` guard was corrected to `!= 3` in lockstep. New cross-language KAT (`rust/shekyl-oxide/shekyl-oxide/src/tests/transaction.rs` `real_mainnet_genesis_tx_parses_as_v3`) parses the verbatim mainnet `GENESIS_TX` and asserts version 3 + coinbase shape; the synthetic round-trip test is now `v3_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.rs` and the standoff docs: (1) `run()` guarded with `assert!(cfg.trials > 0)` (an empty `sets` indexed `p05` / divided by zero) and dropped to private (no caller outside the module); (2) both block→minute conversions centralized in a `blocks_to_minutes` helper that casts to `f64` before multiplying and maps the `u64::MAX` "no finite window" sentinel (returned by `recommended_window_blocks` for `rate <= 0`) to infinity, removing a `u64` overflow; (3) `inversion_prior_break` computed from the discrete gap model `0.5·W/(W+1)` instead of a constant `0.5` — accurate for the integer gap and correctly `0` at `W=0` (events coincide, no inversion possible); the `--standoff` report's inversion arm now reads `0.49834` (was `0.5`), all other metrics byte-identical. Also fixed a doc inconsistency in `STAKER_ARCHIVAL_SIM.md` and `FOLLOWUPS.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_blocks` now 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 read `3.0` instead of the misleading window value (`anon_set` unchanged; the model already used the cluster internally); (5) `recommendation()`'s `entry_latency_frac_epoch` maps the `u64::MAX` "no finite window" sentinel to `∞` (was a huge finite number), keeping `homeostasis_free` meaningful and consistent with `recommended_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 Rust `shekyl-oxide` `BlockHeader` parsed only five fields and stopped at `nonce` (`block.rs`), while the consensus C++ `block_header` serializes a sixth field — the FCMP++ `curve_tree_root` — after `nonce` (`src/cryptonote_basic/cryptonote_basic.h`, `FIELD(curve_tree_root)`). On a real six-field block this left 32 bytes unconsumed and `Block::read` mis-aligned the following miner transaction (parsing the root's first byte as the tx-version varint → `version != 2` reject), so `Block::read` would **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 inherited `202612` Monero test-vector remap), and `serialize_pow_hash` hashed the five-field header so the Rust block hash could not match consensus. This fix adds `curve_tree_root: [u8; 32]` to `BlockHeader` and reads/writes it **unconditionally** after `nonce` — never gated on `hardfork_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 corrects `Block::hash()` (the preimage now includes the root) and thus `ReferenceBlock.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 per `CT5_ENGINE_WIRING.md` §3.6 / §6 (R1-Q6, E3). - **docs: correct stale `REBUILD_AT` default in `CURVE_TREE_CLIENT.md` §8 #3 (2026-06-13).** §8 open-question #3 read `MAX_AGE/2 = 47` as a "first cut", but the landed `reference.rs` defines `REBUILD_AT = FCMP_REFERENCE_BLOCK_MAX_AGE / 2 = 50` (MAX_AGE = 100), pinned by a const-eval assert and `reference::tests` — and §5/§5.2 already say 50. The `47` was 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_record` reconstructs a fresh `ArchivalBondValue` from scalar args and cannot carry the v4 `claimed_settlement_epochs` set or `first_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 writer `BlockchainDB::put_archival_bond_value(p_id, bond)` (LMDB impl + no-op base + testdb stub) that serializes the entire decoded record; `put_archival_bond_record` now delegates to it (its only caller is JoinMarket connect, a fresh-P create where the scalar path is correct). `apply_archival_slash_one` and `revert_archival_slashes_at_height` now write the mutated bond through the full-bond writer, so the dedup state survives a slash and reverts cleanly with `pop_block`. Added a public reader `get_archival_bond_value` and three regression tests in `archival_substrate_lmdb.cpp` (`bond_v4_fields_survive_full_writer`, `bond_v4_fields_survive_load_modify_store`, `bond_v4_claimed_set_survives_reorg_revert`). The `claimed_epochs_check_and_set` C++ FFI stays deferred to its first consumer (the emission write path, PR-E3) per spec §6.3 and `15-deletion-and-debt.mdc`. Also closed a sibling encode/decode asymmetry in the same write path: `ArchivalBondValue::encode()` serialized any `holdings_kind` byte while `decode()` rejects unknown values, so a writer could persist a record no read path can decode. `encode()` now rejects unknown `holdings_kind` alongside its existing at-rest invariants (bounds, claimed-epoch well-formedness), which protects every writer through the single serialization funnel; regression test `bond_encode_rejects_unknown_holdings_kind`. Because `apply_archival_slash_one` was moved to public scope for the slash regression tests, it now carries the same `check_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 test `apply_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-sim` default backend is now the authoritative integer `Curve` (2026-06-13).** `--curve-impl=integer` (imports the canonical `shekyl-archival-retention::reward_arithmetic`, the code the emission vin mints with) is the default for all sweeps; `--curve-impl=float` is 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-impl` value 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). The `CurveImpl` type-level default and the scenario base config flip to `Integer` accordingly; 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-shard `R_market(s,E)`, *not* per-P `capped_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 the `REWARD_EMISSION_LEG.md` §7.2 verify API (the §12 sibling-API gap) as a sibling type to `FcmpPlusPlus` in the `shekyl-oxide` FCMP++ crate; first consumer is the emission vin. `MembershipSpendAuth` keeps SAL's `R_O` leg alone (96 B/input — the self-contained `(x, y~)` opening proof over `(G, T)`); the BP+/`P'`/`L` key-image machinery is not carried; the `Fcmp` membership leg, including the `H(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 (`u32` LE — 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, since `O~` freshness is the entire non-linkability property for an input type publicly tagged as archival activity. Measured strictly smaller than the 1-input `FcmpPlusPlus` order 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: `ArchivalBondValue` v4 — inline `ClaimedEpochSet` + `first_paying_emission_height` (2026-06-12).** Implements the schema half of the Stage-3 gate per `REWARD_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 (`u32` count + `u64` BE entries) and the set-once `first_paying_emission_height` (sentinel `0` = unset, unreachable — no emission pays before the first settlement epoch closes); v3 is decode-rejected per the pre-genesis posture. Decode invariants: cap `W + 6` (= 32, §6.3 pin), strict monotone order, span ≤ `W`; the cap derives from `max_claim_age_w` in `config/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 the `good_through` precedent: window maintenance is part of `check_and_set` (prunes entries below `current − W` on insert), closing the review round's liveness finding — insert-only semantics would have a continuously-claiming honest `P` overflow 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)·SEB` connect; citing emissions valid strictly above it; reorg-safe by pop ordering). Docs: `LMDB_SCHEMA.md` v4 layout, `ARCHIVAL_CONSENSUS_STATE.md` §8/§9.1 schema-implemented status, emission-leg §1.1/§6.2/§6.3/§12, `PHASE_2B_FSM_RETOOL.md` P2B-3 landed, `PHASE_2B_STAKE_LIFECYCLE.md` §2.4 gate line (Stage 3 now gates on gate-6 soundness only), `FcmpMembershipOnly` proof-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_path` no longer takes a loose positional `reference_block_hash: [u8; 32]`; the hash is now `ReferenceBlock.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 shape `CURVE_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) and `ReferenceBlock` literals updated; no production caller existed yet (CT-5 engine wiring is unbuilt). **Docs resync:** the `assemble.rs` "horizon deferred" comment was stale (`reference.rs` landed `select_reference_height` et al. in CT-4) and is rewritten; `CURVE_TREE_CLIENT.md` §9 CT-4 row flipped from a `select_reference_block`/`client` stub to **Landed** with the real `assemble` + `reference` modules and symbols; the non-existent `select_reference_block` symbol corrected to `select_reference_height` / `should_reanchor` in `FOLLOWUPS.md`, `PHASE_2A_SEND_PATH.md`, and `SHEKYLD_PREREQUISITES.md`. **FFI audit:** recorded (not fixed — it is a planning-activity daemon change per `20-rust-vs-cpp-policy.mdc` / `15-deletion-and-debt.mdc`) that `on_get_curve_tree_path` recomputes the key-image generator `I = Hp(O)` via C++ `hash_to_p3` instead of the existing `shekyl_compute_output_key_image` FFI, 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_size` hard-coded the per-leg byte counts, and `FcmpPlusPlus::verify` took the key images and `H(pqc_pk)` leaf scalars as two parallel `Vec`s 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`), with `proof_size` composed from them and transitively asserted against the serialized length by the existing roundtrip tests; and `FcmpPlusPlus::verify` takes a single ordered `Vec` 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 one `InvalidInputCount`; the wrapper in `shekyl-fcmp` already does its own granular length checks before calling the oxide layer, so no downstream granularity is lost. The membership-only verify keeps its single `pqc_pk_hashes` `Vec` — 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 66 `shekyl-fcmp` tests 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_exits` and 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 refreshes `deep_seated`). Re-measured swan-4 arms confirm the predicted +1-per-exit-opened-window undercount exactly (`ssSE` += run-wide `extT` on 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 (`ssOpn` 0 → 5). Banked figures updated in `STAKER_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 (commit `2fdf33650`) aligned the `ScenarioResult` / `ExtinctionScan` doc comments, added the `[0, 1]` loud-failure assert on the shock exit fractions, corrected the `ArchivalLockModel` formula doc, and made `locked_atomic` panic on `blocks_per_shard == 0` instead 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-backed `LeafStore` back to an inclusive fork point, verifies the boundary-adjacent frozen segment's `R_k`, rebuilds in-memory state from the authoritative store, and resumes forward ingest at `fork + 1`. Rollback partitioning is aligned with CT-2's drain boundary (`maturity > drained_through(fork_height)`, while `sync_tip_height` and the orphan filter remain fork-height based), fixing the off-by-one that would otherwise keep `maturity == fork` rows drained and make the first post-rollback block remove already-drained pending rows. New structured store errors identify frozen-record absence and `R_k` mismatch. 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-backed `reorg_deep` rollback/resync KAT against fresh replay and consensus roots. `LeafStore::append_drained` is 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 with `ClientError::Poisoned` instead of relying on caller prose discipline; `verify_frozen_tail` maps a corrupt freeze-segment counter to `StoreError::CorruptMeta` rather than panicking on the rollback path; and the persistent `reorg_deep` KAT asserts its `deep_pop <= main_tip` fixture invariant explicitly. - **crypto: CT-3b persistent client lifecycle (`shekyl-curve-tree`, 2026-06-12).** `CurveTreeClient::open(path)` resumes from a persisted `LeafStore` with 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_block` now writes both tables atomically through `append_block_deltas` *before* 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 retyped `u64` → `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_VERSION` bumps 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 with `SchemaVersionMismatch`; 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).** The `LeafStore` now carries the full CT-3 sync/reorg contract at the store layer (`CT3_SYNC.md` Round 1, closed): redb-typed keys end to end (`BlockHeight`/`Gindex` newtypes plus the `TreePosition`/`SegmentId` retrofit, byte/order delegation KAT-verified), a `schema_version` stamp checked before any write on an existing file (error-without-mutation), `creation_height` on `LeafEntry`/`leaf_meta`, a pending-candidates table, the single-txn `append_block_deltas` write API (collision/missing-row loud, abort-leaves-no-trace), resume read paths with runtime-checked invariants (gindex parity, maturity monotonicity, key-set symmetry), and `rollback_to_fork` — pruned-range-safe partition search, two-class drained→pending migration, the uniform `creation_height > fork` orphan filter, and frozen-segment rollback through shared truncate internals. KAT/robustness coverage includes equal-maturity partition boundaries, a full-snapshot atomicity check, deterministic `rand_chacha` codec 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/n` widening 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 the `N_active = 3` seats sit in distinct failure domains. **(3) Liveness-signal escape inherits the unpredictability prerequisite** (`ARCHIVAL_FAILURE_CONFIRMATION_PIN.md` §3.2): whatever replaces `m`-keyed slashing carries the `H_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 in `Market`, so a sole-source shard reads `R_market = 0` (maximal scarcity, maximal `g`-weighted re-acquisition reward) exactly while the foundation supplies the bytes; the binding constraint is `reseed_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`/`shkExt` events 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. New `reseed_rate` knob caps fresh-deep acquisitions of sole-source shards per epoch (bandwidth-bound foundation seeding over the L16 rendezvous path); new metrics `ssSE` (sole-source shard-epochs), `ssMxW` (max single-shard window), `ssOpn` (windows open at end). Measured (`swan4_vshape_reseed3/12`, `swan4_servo400_reseed3`): at `reseed_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.md` treasury 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):** `extB` bins 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; new `shkExF` counts data-dead only when market holders *and* `foundation_floor_aged` are simultaneously zero (also catches the floor-withdrawal hand-off race). `swan3_vshape_floor` / `swan3_servo400_floor` reproduce identical market damage with **`shkExF` = 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_t9` shows 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; new `shock_exit_top_deep` exits 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-tail `m` (W6) and slash deterrence pull in opposite directions — at `m = 11/n = 13` a strategic actor misses 10 of 13 baselines slash-free, and a crisis-sized `m` makes 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 no `m` satisfies 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.md` treasury 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), so `shkRec` was a metric-recovery claim only; new `extT`/`shkExt` reads 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 = n` wipes failure domain 0 under the L15 `a % n` bucketing — stride was the most benign correlation structure (holder sets straddle it; per-shard total loss ≈ 0 by construction). **Aftershock + timing (W3):** `aftershock_at` re-fires every shock leg against the survivors; `swan2_knee_cascade` fires at the L13 decayed-purse knee; `swan2_cascade_rho` compounds 50 % exit with an entry-suppressing ρ ×2. Five `swan2_*` 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 − m` sized 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`; does `RELEASE_COOLDOWN_EPOCHS = 2` smear or merely delay the unbond cohort?). **W9** AGG reversion clause gains the **downward** `N_P` trigger (reopen iff envelope extends below ~25–30; per-archiver claims scale as `1/N_P` — ~70 kB single tx at `N_P` = 17 year-30; named guard = per-emission claim cap forcing batch splitting) in `STAKER_ARCHIVAL_SIM.md` + `REWARD_EMISSION_LEG.md` §10.1. **W10** gate-7 bonds-only ↔ `R_market` weighting 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). **W11** `swan_regime` verdict 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 in `shekyl-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 reads `shkP`/`shkRec`/`shkBA` (post-shock worst deep gap, epochs-to-recovery, bonded trough). Ten `swan_*` 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`'s `spread`/`sprdW` verdicts now gate on `max_actor_share < 0.20` and oldest-band whale share `wB4 < 0.20` (the per-band distinct-actor-seating term rides on the coverage claims, since `R` counts distinct actors); `gini_actor`/`giniW` are 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 pass `sprdW` across `g ∈ [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 in `STAKER_ARCHIVAL_SIM.md` §Layer-2 results and `REWARD_EMISSION_LEG.md`). Stale pre-Curve-repair attractor counts (~79 archivers) annotated to the post-repair ~113 equilibrium in `ECONOMY_EXPLAINED.md` and 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]` — so `g(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 grew `g` without bound (`≈ 700·w` for a 10-year shard at `SEB = 10 000`), concentrating `Σwork` onto oldest-band holders over mission timeframes — a shape no sim run validated and the whale-capture surface Layer 2 gates. `shard_age_milli` reimplemented (signature unchanged — callers already pass `close_block_height`); `archival_reward_age_weight_milli` 1000 → **2000** (sealed target `g* ≈ 2`, calibration band `[1500, 2500]`, retunable on stressnet evidence without a design round); epoch-close KAT vector re-derived by hand (`Σwork` 3500 → 2590, derivation recorded in the fixture description); `REWARD_EMISSION_LEG.md` Channel-1 prose carries the unit. Mission-timeframe note: this change is what makes the `g ≈ 2` seal 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-sim` axes `layer2_band{,_whale,_coloc}`, `layer2_budget{,_whale}`, `layer2_colocbud`; 29 scenarios; results in `STAKER_ARCHIVAL_SIM.md` §Layer-2 margin-robustness band — results). Decomposition confirmed decisively: `giniW` tracks population leanness (`bondA`) in both sweep directions while every direct whale gauge stays flat (`mxSW` 0.013–0.018 vs the 0.20 bar, `wB4 = 0`, whale Δ`giniW` ≤ 0.003). Dispositions: **spread gate re-anchored on direct whale gauges** (`gini_actor` demoted to trend); **`g` sealed as a band `[1.5, 2.5]`, genesis target `g ≈ 2`** — the band's upper bound is polarized-world coverage oscillation (`g ≥ 3.0` fails at any purse; the `g ∈ {2.0, 2.5}` shoulder heals at +30 % purse, the L13 servo's static image); budget cross confirms the purse as the spread lever (`giniW` 0.598 → 0.217 over budget 100 → 200); `Curve` reserve untriggered. Finding 0: the 2026-06-07 `gate4_fine` readings are superseded — the banded PL Curve repair (`4071ec032`) moved the L11 equilibrium (`bondA` 79 → ~113; coloc pin row oscillates at bare-lean purse); the `bond_rate* = 0.75` pin stands under the re-read (Finding 4). Finding 5: sim-vs-consensus `g(age)` **units gap** (normalized [0,1] age vs raw epoch counts) blocks mapping the sealed band onto `archival_reward_age_weight_milli` — normalization pin queued in `FOLLOWUPS.md` (V3.0). Cross-docs: `REWARD_EMISSION_LEG.md` §1.2 #4 + keystone row + checklist; `WALLET_REWRITE_PLAN.md` status. **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 the `giniW ≈ 0.599` grazing reading before sweeping: the whale gauges have wide margins (`max_actor_share ≈ 0.011` vs 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 at `bond_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-tail `Curve` (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 per `DESIGN_CONCEPTS.md`). Linked from `PUBLIC_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 the `N_P` envelope {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: built `FcmpMembershipOnly` > 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.md` deletes the admission branch (no `admission_proof` vin field §5.3, no admission-threshold verify step §7.1, `backing_ok` loses the `Σ ≥ ADMISSION_MIN_ATOMIC` conjunct §7.2, §7.4 amount proof deleted-not-gated, §10.2 records the closure + reversion pointer); `PHASE_2B_STAKE_LIFECYCLE.md` closes §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.md` admission bullet updated. `ADMISSION_MIN_ATOMIC` is **not a consensus constant**; reinstatement only via the G7 reversion clause (`STAKER_ARCHIVAL_SIM.md` ledger). 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: `ArchivalLockModel` in `shekyl-economics-sim` derives locked supply from the consensus pins (compile-tied to `shekyl-archival-retention::ARCHIVAL_BOND_FLOOR_ATOMIC` / `SETTLEMENT_EPOCH_BLOCKS`), denominating `stake_ratio` and the burn input against **consensus circulating** per the pinned build constraint; eleven `gate7_*` scenarios behind `--gate7` (arm A across the `N_P` envelope + shard-geometry sensitivity, arm B `MIN` grid, volume-stress pair, asserted comparators); legacy eight-scenario output verified **byte-identical**. Result: derived lock collapses to `bond_floor × R × shards(t)` at `lock/circ ≤ 10⁻⁴` in every arm; all three macro gauges insensitive to both arms at every `N_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 in `calc_burn_pct` is effectively inert in V3. - **archival: 2B design dispositions — bond-duration shape, `ClaimedEpochSet` encoding, 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 testnet `fetch_latency_per_unit`; `STAKER_ARCHIVAL_SIM.md` §L10-hardening disposition, `ARCHIVAL_TIMING_CONSTANTS.md` §1, `ARCHIVAL_BOND_GATE4.md` §3.4); (2) **`ClaimedEpochSet` encoding decided** under `W = 26` — inline sorted absolute-epoch list on `ArchivalBondValue` (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.md` P2B-3); (3) **iteration-5 gate-7 locked-supply re-pricing sim scoped** — derived archival-lock model replacing the asserted `stake_ratio` in `shekyl-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. Parent `CURVE_TREE_CLIENT.md` §8 #6 / §9 CT-3 row updated; store-backed assembly routed to `FOLLOWUPS.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 and `leaf_meta` gain `creation_height` and 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); deferred `get_curve_tree_leaves` endpoint + KAT now a tracked `FOLLOWUPS.md` row 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 between `rollback_to_fork` and `truncate_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` / `Σwork` aggregation — now lives in `shekyl-archival-retention::consensus_state::epoch_close_compute` and crosses the FFI as one coarse `shekyl_archival_epoch_close_compute` call. 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`) and `shekyl_archival_good_through` replace them. KAT `consensus_state_kat_v1.json` gains an `epoch_close` section; LMDB gather/store/revert covered by `epoch_close_gather_compute_store_revert`. Sim integer/float curve backends harmonized: degenerate caps credit zero. Fixes pre-existing LMDB cursor bug in `delete_archival_*` loops (`MDB_GET_CURRENT` after delete of last record). - **ci: PR pushes no longer double-run workflows.** `build.yml` and `randomx-v2-differential.yml` triggered on both an unrestricted `push` and `pull_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). Their `push` triggers are now restricted to `dev`/`main` (post-merge validation); PR branches are covered by the `pull_request` event 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 `redb` 2.6.3 → 4.1.0.** Intentional 1.94.0 pin for RandomX PoW + CT-1 `redb` 4.1.0 (rustc ≥ 1.89), aligned with `shekyl-gui-wallet` `rust-toolchain.toml`. `shekyl-pow-randomx` uses `Box::new_zeroed_slice` (stable 1.92.0). CI adds `rust-msrv-gate` (`dtolnay/rust-toolchain@1.94.0` + `cargo check --workspace`). Pre-genesis greenfield: redb v3 format only. ### Added - **engine: Phase 1 orchestrator closeout.** The `shekyl-engine-core::Engine` domain orchestrator (lifecycle, refresh + scan merge, pending-tx send lifecycle, query surface, tracing forwarder) is complete per the done-matrix in `docs/completed/PHASE_1_ORCHESTRATOR_STATUS.md`. `docs/design/WALLET_REWRITE_PLAN.md` reconciled to the binding `Engine` naming (decision log 2026-04-27): frontmatter `phase0_closeout` and `phase1_domain_model` marked 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_offload` bodies stay blocked on `shekyl-crypto-pq` view-only/HW constructors (FOLLOWUPS V3.0 entry holds the reversion clause). - **engine: `Engine::primary_address()` accessor + Phase 1 query-pattern docs.** Thin `&self` accessor assembling the wallet's one reusable `ShekylAddress` (End-state 5) from the `KeyActor`'s cached public projection and the engine's cached network; `ShekylAddress` re-exported from `shekyl-engine-core`. `engine/mod.rs` gains a "Query surface (Phase 1 disposition)" section documenting the `ledger()`-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_password` on-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_open` verifies the rotated envelope through an independently constructed `WalletFile::open` (new password loads state; old password fails with `InvalidPasswordOrCorrupt`), and `change_password_with_new_kdf_rewrites_envelope_header` asserts the rotated Argon2id parameters on the raw on-disk header via `inspect_keys_file`. ViewOnly / HardwareOffload coverage rides the capability-dispatch commit when those `open_*` 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 the `shekyl_log_*` C ABI and one `tracing-core` dispatcher shared by all Rust call sites. `shekyl-ffi` folds in `shekyl-logging`, making `libshekyl_ffi.a` the wallet-side image; the new link-image crate `shekyl-daemon-image` (`shekyl-ffi` + `shekyl-daemon-rpc`, no logic) is the daemon's image, selected per binary by a generator expression in `cmake/BuildRust.cmake` (`SHEKYL_RUST_IMAGE_DAEMON` target property). The standalone `libshekyl_logging.a` link and the force-load (`WHOLEARCHIVE`/`--whole-archive`) machinery are deleted — with one image per binary there is no resolution race to arbitrate (the MSVC `LNK1104` flag-parsing failure goes with it). A post-link `nm` gate fails the `shekyld` build on any second `GLOBAL_DISPATCH`. New C-ABI export `shekyl_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`); `shekyld` calls it after `mlog_configure`. State-machine integration test plus C-harness coverage in `shekyl-logging`. - **crypto-pq: ADDRESS_DERIVATION_V1 freeze hardening (enforcement only).** Published CODEOWNERS-protected KAT corpus at `docs/test_vectors/ADDRESS_DERIVATION_V1/` with a `kat_address_derivation_v1.rs` consumer that verifies the manifest's self-describing fields (`vectors_sha256_hex` = `sha256sum vectors.json`, tier counts) and Tier-2 account distinctness; wired `scripts/lint_cpp_clamp_ban.sh` into the `rust-audit-and-test` CI job; added `ADDRESS_DERIVATION_MANIFEST_HASH` tripwire in `address_derivation_freeze.rs` with `shekyl-cli derivation-freeze-self-check` as the operator surface (no C FFI export: no C++ consumer exists). Zero semantic changes to the frozen v1 derivation pipeline in `account.rs`. - **archival: consensus state emission read surface (ARCHIVAL_CONSENSUS_STATE).** Integer `reward_arithmetic` + `consensus_state` in `shekyl-archival-retention`; banded float/integer `Curve` in `shekyl-staking-sim` (`--curve-impl=float|integer`); LMDB `archival_r_market` / `archival_sigma_work` epoch-close sweep; partial-slash `bad_interval` F3 fix; KATs (`consensus_state_kat_v1`, gate4 phase-2 `emission`); docs `ARCHIVAL_REWARD_ARITHMETIC.md`, `ARCHIVAL_SIM_ECONOMICS_VERDICT.md`; FA-6 Pi scenario B capture in `PERFORMANCE_BASELINE.md`; CI archival reward gates and aarch64 determinism KAT on `depends` ARM v8 job. - **curve-tree: CT-1 LeafStore on redb (Round 1).** `LeafStore` persists drained leaves and frozen segment metadata (`R_k`, `end_block_height`) in redb; height-gated segment freeze, ACID reorg truncate, pin/prune seam, and `mixed_composition_root` hot path with full-rebuild fallback. `CurveTreeClient` mirrors drained leaves on ingest. Tier-A `store_kat`, `upper_layers_kat`, and unchanged `recon_kat` / `assemble_kat`. Pins: `CT1_ROUND1_PINS.md`, close-out `CT1_ROUND1_CLOSEOUT.md`. Robustness sweep (`CT1_ROUND1_CLOSEOUT.md` §5): `root_at` / `verify_root` / `assemble_path` are `&self` queries 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_drained` rejects non-canonical Selene scalars at write time (`StoreError::InvalidLeafBytes`); store bounds checks read `META_LEAF_COUNT` inside 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 plan `stabilize_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 and `transfer_details` stored-secret restore copy removed); `USER_GUIDE.md` seed copy fixed to 24-word BIP-39 + opt-in passphrase (Electrum Phase-6 residue). `ATOMIC_UNITS_NEWTYPE.md` and `PRIMARY_CLAIM_DERIVATION_RENAME.md` archived to `docs/completed/`; the 104 references broken by the earlier DAA/Electrum moves to `docs/completed/` repaired repo-wide (source comments, CI workflow headers, consensus-constants JSON, workspace rules, docs). Four FOLLOWUPS V3.0 items filed: dedicated `ADDRESS_DERIVATION_V1` KAT corpus, `lint_cpp_clamp_ban.sh` CI wiring, `ADDRESS_DERIVATION_MANIFEST_HASH` tripwire, USER_GUIDE Rust-CLI realignment. CODEOWNERS freeze-spec path fixed to `WALLET_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 per `PHASE_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-`P` claimed-epoch bitmap). `AtomicUnits::mul_div_rem`'s reopening criterion re-anchored from `entitlement.rs` to 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). Per `21-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_wallet` cleanup is superseded by the Phase 5 wholesale deletion (reopens on new consumers or Phase 5 slipping past stressnet), and the `epee::wipeable_string` mlock 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 inherited `rct::` / `rctSigs` naming: `ct_signatures` alias is partial fix; verifier → `ct_semantics` at `wallet2` cutover; Rust vocabulary effective now across FFI; no rename PR pre-Phase 5. Updates `FOLLOWUPS.md` and `STRUCTURAL_TODO.md`. - **engine: Phase 2a-4 — TestDaemon fee-bound build→submit; engine send substrate complete.** `daemon_fee_estimator_maps_test_daemon_priority_tiers` and Custom sanity-ceiling unit tests; output locks reserved before async sign with `release_build_reservation` on sign failure; integration tests for daemon-derived `tx.fee`, submit dedup, missing key-image rejection, and reserved-output blocking. Doc closeout in `PHASE_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 in `PHASE_2A_SEND_PATH.md` §10. - **wallet: Phase 2a-3 sign, wire encode, submit.** `KeyActor`/`LocalKeys` sign bridge, `shekyl-tx-builder` `wire` module, async `Signer::sign_transfer`, `TransactionSubmitter`, non-empty `tx_bytes`, depth-1 `fcmp_proof_size` KAT, and `predict_weight` self-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`](THREAT_MODEL_WALLET.md) — pit-of-success vs adversary, T6/T7, R2-F9 phishing tier. FA-10 cooperative attribution foundation pin in [`POST_QUANTUM_CRYPTOGRAPHY.md`](POST_QUANTUM_CRYPTOGRAPHY.md). FA-4 recorded closed (FA-7 + §5.7.8/§5.7.9); `USER_GUIDE.md` Monero 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`), scanner `tx_extra 0x07` parse tests (first-match; daemon parity unverified). Close-out `docs/completed/CT2_ROUND1_CLOSEOUT.md`; `CT2_DRAIN_ORDER.md` empty-window prose aligned to fixture truth. - **crypto-pq: FA-6 §8.5.1 decap pre-filter gate harness.** `fa6_decap_prefilter_gate` example (`--path fa6|classical`, scenarios smoke / a / b), classical counterfactual derivation, criterion smoke bench `fa6_decap_prefilter`; Pi capture script `scripts/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-out `docs/completed/FA-6_CLOSEOUT.md`; Round 3 closed / Round 4 scoped in `SUBADDRESS_UNDER_PQC.md` §10.1. Matrix in `PERFORMANCE_BASELINE.md` §FA-6; Pi scenario B capture pending. - **archival: serve-credit consensus integration test (gate-2 §10 step 3).** `archival_serve_credit_integration.cpp` seeds in-memory archival substrate from KAT fixture `integration` block (CT-2 opening + epoch-0 challenge geometry) and exercises `Blockchain::check_archival_serve_credit_input` end-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_height` on block connect applies per-shard floor slash when `challenge_failed`; updates `total_bonded_atomic` / `total_burned`; journal + `pop_block` revert. 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.json` exercises join → serve `E_first` and bonded aggregation; paying-emit stub `null`. FFI: `shekyl_archival_verify_join_market_bond_post` (distinct `SHEKYL_ARCHIVAL_BOND_POST_ERR_*` per reject reason), `shekyl_archival_serve_credit_epoch_ok`. `ArchivalBondValue` LMDB v3 stores per-P `bonded_total_atomic`; pre-v3 bond blobs rejected at decode. `blockchain.cpp` delegates join-market semantics and `E_first` lower bound to Rust; hybrid pubkey + `P_id` hint stay C++. - **archival: JoinMarket bond-post verify and LMDB connect (gate-4 §3.4.1).** `txin_archival_bond_post` wire (`tag 0x05`), `put_archival_bond_record`, and `total_bonded_atomic` on connect/pop; JoinMarket-only at genesis. - **archival: bond-post RCT balance verifier (gate-4 §3.2 / §3.5 step 6).** `verRctSemanticsBondPost` closes `sum(pseudoOuts) + bond_debit = sum(out masks) + fee + bond_credit`; commitment sum is verified in `shekyl-archival-retention` via `shekyl_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 from `verRctSemanticsSimple` in NIC verify. - **archival: bond + shard-registry LMDB substrate (gate-2 §5.3 steps 2, 6–7).** `archival_bond`, `archival_shard_segment`, and `archival_shard_leaf` subdbs with `put_*` 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-ffi` `shekyl_archival_verify_serve_credit_vin`; `Blockchain::check_archival_serve_credit_input` and pure archival tx admission in `check_tx_inputs`; `archival_serve_credit` LMDB subdb (`serve_credit_bit` write on connect, revert on `pop_block`). - **archival: `txin_archival_serve_credit_response` deserializer (gate-2 §10 step 2).** C++ `txin_v` tag `0x04` with path bounds; `shekyl-oxide` `Input::ArchivalServeCreditResponse` delegates to `shekyl-archival-retention::wire`; KAT cross-check in C++ and Rust unit tests. - **archival: `serve_credit_bit` rename sweep (gate-2 §11).** `retention_bit` / `proven_retention` → `serve_credit_bit` across emission/consensus specs, `WorkClaimVector` field pin, and `shekyl-staking-sim` T-A1 fingerprint helper. - **archival: gate-2 serve-credit KAT fixture (v1).** `tests/fixtures/gate2_serve_credit_kat_v1.json` tripwires challenge replay, wire bytes, signature preimage, and CT-2 founder opening; regen via `regenerate_gate2_kat_fixture`. - **archival: serve-credit response vin wire (gate-2 §5.1.1).** `ArchivalServeCreditResponse` encode/decode (vin tag `4`), `encode(path)` for signature preimage, roundtrip + preimage determinism tests in `shekyl-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_bundle` hardcodes `PRIMARY_CLAIM_INDEX_LE`. Deleted public `SubaddressIndex`, `subaddress_lookahead` (settings v2, prefs schema v2), `AddressBookEntry::is_subaddress` (`BOOKKEEPING_BLOCK_VERSION` 5); paired `WALLET_LEDGER_FORMAT_VERSION` 6→7 (`wallet_ledger.snap`). Spec: [`PRIMARY_CLAIM_DERIVATION_RENAME.md`](completed/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), sliding `m` sweep, `policy_pin` + Round-2 joint-`m` gate note. `ARCHIVAL_FAILURE_CONFIRMATION_PIN.md` restructured: §1 pinned policy, §5 rejected-alternative stratum; `R_market` serve-credit substrate in consensus §3.3. - **docs: archival failure-confirmation policy pin (2026-06-08).** [`ARCHIVAL_FAILURE_CONFIRMATION_PIN.md`](completed/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-`P` confirmation FSM. Cross-links in gate-2 §0 and `STAKER_ARCHIVAL_SIM.md` L14b. - **archival: `shekyl-archival-retention` verify crate (gate-2 §10 step 1).** Challenge leaf index + beacon fire height (`cSHAKE` domain labels per `ARCHIVAL_RETENTION_GATE2.md` §3.3–§3.4); `verify_segment_path` replays `shekyl-fcmp::tree` to `R_k` with Selene leaf-layer chunk scalars; CT-4 assembled-path cross-check KAT. Provisional `CHALLENGE_BEACON_SEAL_BLOCKS = 1`. - **docs: gate-2 Round 1 on-demand serving obligation (2026-06-08).** [`ARCHIVAL_RETENTION_GATE2.md`](completed/ARCHIVAL_RETENTION_GATE2.md) §0 — solved shape: affirmative serve-credit per epoch; leaf `ℓ` deterministic, `H_fire` beacon-sealed; no 24/7 disk proof. `serve_credit_bit` rename; L15 = reach not durability; traffic-pay reopen §0.3. Retires §7.5 reacquisition panic ([`ARCHIVAL_RETENTION_PROOF_8C_FEASIBILITY.md`](design/ARCHIVAL_RETENTION_PROOF_8C_FEASIBILITY.md)). - **wallet: FA-8 — payment requests + cooperative attribution (V3.0 substrate).** `PaymentRequest` / `ReceiveAttribution` types; `BookkeepingBlock` v4 (`payment_requests`); `TransferDetails.receive_attribution`; ledger block v6 / `WALLET_LEDGER_FORMAT_VERSION` 6 (paired `wallet_ledger.snap`). Inbound: after scan merge, classify decrypted label (sentinel vs REQUEST) and match open requests when `operational.cooperative_payment_requests` or env `SHEKYL_COOPERATIVE_PAYMENT_REQUESTS=1` (default off). Outbound: `shekyl-address` payment URI parse/format; `construct_output_with_label_plaintext`; C++ send path echoes REQUEST via `shekyl_construct_output_labeled` when destination `original` is a `shekyl:…?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`](design/ARCHIVAL_RETENTION_PROOF_8C_FEASIBILITY.md) §7.5 reacquisition-asymmetry hinge OPEN; economics reopen if hinge fails; `m=3` scoped to partial-deletion only. [`ARCHIVAL_RETENTION_GATE2.md`](completed/ARCHIVAL_RETENTION_GATE2.md) downgraded to Round 0 draft; `H_anchor` must fall inside epoch `E` (fixes `H_close+1` bug); bytes/KATs held for Round 1. - **docs: archival retention gate-2 Round 0 spec (2026-06-08).** [`ARCHIVAL_RETENTION_GATE2.md`](completed/ARCHIVAL_RETENTION_GATE2.md) — challenge derivation (`cSHAKE`, `m=3`, `R_k`-local Merkle openings), `txin_archival_retention_response` wire, 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`](design/ARCHIVAL_RETENTION_PROOF_8C_FEASIBILITY.md) — BUILD disposition: Merkle opening PoR to shard `R_k` on `shekyl-fcmp::tree` primitives; ZK and FCMP++ membership rejected for genesis. - **docs: archival corpus fossil sweep (2026-06-08).** [`ARCHIVAL_CORPUS_FOSSIL_SWEEP.md`](design/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`](design/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 conflated `REORG_HORIZON`. F4 drain inequality signed. `shekyl-staking-sim --timing-cluster` + `src/timing_cluster.rs`. `config/consensus_constants.json` documented (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` (deleted 2026-08-26) §7 — archival threat model (T-A*, G11 extended, F1/T-A1 gate); claim-era wargame → §7.A. §7.11: LMDB on `dev` verified (`m_properties` scalars, `already_generated_coins`, `pop_block` revert 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.rs` re-pointed: timeline channel (baseline-relative rotation advantage) + cohort channel (portfolio co-holder size). Axes `ta1_f1` (four scenarios) and `ta1_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_shared` positive 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`](design/F1_TA3_TA7_LIFETIME_WINDOW.md). See [`STAKER_ARCHIVAL_SIM.md`](design/STAKER_ARCHIVAL_SIM.md) §*T-A1 / T-A2*. - **docs: P2B-6 §7 threat model review round 1 (2026-06-07).** `PHASE_2B_SECTION7_DRAFT.md` (deleted 2026-08-26) — 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`](design/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`](design/ARCHIVAL_BOND_GATE4.md) — consensus-balance model; `bond_credit`/`bond_debit` balance 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`](design/ARCHIVAL_BOND_GATE4.md) — `Unbond` clean release + cooldown; escrow vs spend-lock (reject attestation-only); supply/burn coupling; `E_join+1` boundary; unified Market predicate; HoldingsUpdate principle; `bond_floor` CompleteTree 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`](design/ARCHIVAL_BOND_GATE4.md) — join-Market seam (lag-forced distinct from paying mint); `txin_archival_bond_post` sketch; reorg. Emission §6.4/§8, PHASE_2B §2.4, archival state `Market`, gate-6 lifecycle amended. - **docs: PHASE_2B FSM retool disposition P2B-1–6 (2026-06-07).** [`PHASE_2B_FSM_RETOOL.md`](design/PHASE_2B_FSM_RETOOL.md) — master key `P_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 — `P` hybrid derivation draft (2026-06-07).** [`ARCHIVAL_FIREWALL_GATE6.md`](design/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`](design/ARCHIVAL_FIREWALL_GATE6.md) — Round 0: four firewall layers + bond-funding, `P` lifecycle, 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_stable` re-pointed to coverage oscillation (`oUmx`), not participation churn (F2 bank — pin rows pass `all_pass`). `ARCHIVAL_CONSENSUS_STATE.md`: timeline-axis honest residual (F1); §9.2 settlement-epoch length joint gate-2/6; `good_through` interval event log (F3); `W` × per-`P` batch 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.md`](design/ARCHIVAL_CONSENSUS_STATE.md) rewritten: two-half contract (read surface + invariants), public `P_id` keying, derived `R_market`, `W` prune 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_fine` endogenous; `l11_bud` bond-rate caveat; churn vs spread). - **staking-sim: windowed spread discipline gate (`sprdW`) (2026-06-07).** `shekyl-staking-sim` reports `gini_actor_window` / `max_actor_share_window`; `ALL` sub-claim uses windowed spread (L9 lesson). Keystone holds at `bond_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`](design/ARCHIVAL_CONSENSUS_STATE.md) — gate 2/3 schema, `MAX_CLAIM_AGE_W`, interface vs 8c deferral. `REWARD_EMISSION_LEG.md` Layer 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`](design/REWARD_EMISSION_LEG.md) §4.0: genesis form **C** `budget·Curve(work)/Σ Curve(work)` (reject post-servo cap); market-only `Σwork`; per-epoch `good_through(E)`; drop consensus backing rotation; `Σwork` accumulator. 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`](design/REWARD_EMISSION_LEG.md): three-channel reward stack; open §4.5 accumulator timing (joint gate 1); ordinary-transfer admission (no `C_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↔`P` as ordinary FCMP++ main-tree transfers; bond + reward emission as the two irreducible special surfaces; reward dedup via per-`P` claimed-epoch bitmap on bond record (no `N_arch` tag); 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 in `V3_STAKER_ARCHIVAL.md`, `WALLET_REWRITE_PLAN.md`, `FCMP_PLUS_PLUS.md` §15 (3C not genesis), `CONFIDENTIAL_STAKING.md` status, `STAKER_ARCHIVAL_SIM.md` gate 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). Updates `SUBADDRESS_UNDER_PQC.md` §9–§11 (FA-7 landed, FA-2/FA-8 PR refs). `PHASE_2A_SEND_PATH.md` §6 gate table + `FOLLOWUPS.md` subaddress-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; removed `KeyEngine::derive_subaddress`, subaddress registry/labels from `BookkeepingBlock` (version 3), `TransferDetails::subaddress` (ledger block version 5), `WALLET_LEDGER_FORMAT_VERSION` 4→5 (paired `wallet_ledger.snap`), `TxRequest::from_subaddress`, and `TxOutputContext::Change` subaddress index. Closes FA-2 item in `docs/FOLLOWUPS.md` subaddress 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 in `docs/design/SUBADDRESS_UNDER_PQC.md` §5.7: one primary address per account, no `create_subaddress` at V3.0; payment requests + `enc_label` for invoice attribution. Supersession entry in `docs/V3_WALLET_DECISION_LOG.md`; `PHASE_2A_SEND_PATH.md` §3.10.3 interim `SubaddressIndex` note; `STAGE_1_PR_3_KEY_ENGINE.md` §3.1.3 superseded banner. Closes FA-7 item in `docs/FOLLOWUPS.md` subaddress round. ### Added - **wallet: Phase 2a-1 — daemon fee-snapshot + broadcast primitives.** Implements the `DaemonEngine`-trait primitives on `DaemonClient` (`docs/design/PHASE_2A_SEND_PATH.md` §3.3/§3.6, §5 row 2a-1): `get_fee_estimates` issues **one** `get_fee_estimate` JSON-RPC call and stores a single snapshot-level `quantization_mask` across all tiers (no tier-vs-tier skew); `submit_transaction` parses `tx_bytes`, computes the `TxHash` **locally** (never trusts a daemon-returned hash), and maps the daemon relay verdict onto `TxSubmitOutcome` via 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 a `TxRelayResponse` so the full daemon verdict (including `Failed` statuses) is always surfaced to the caller. `ProofStale` detection is **deferred to Phase 6** — the C++ daemon reports a stale FCMP++ root as a generic verification failure with an empty `reason`, indistinguishable client-side; the reopening criterion (a daemon-side `fcmp_root_stale` signal on `send_raw_transaction`) is recorded in `docs/SHEKYLD_PREREQUISITES.md` §5 and `docs/FOLLOWUPS.md`. The `LocalPendingTx` async 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 `LocalPendingTx` async build path per `docs/design/PHASE_2A_SEND_PATH.md` §5 row 2a-2: `FeeSnapshotSource` / `DaemonFeeSnapshotSource` (PF2) fetch one `FeeEstimates` snapshot per build; `FeeEstimationContext` carries `output_count`, `fee_snapshot`, and `tree_depth` (PF1); structural `predict_weight` / `converge_fee` / `FeeDirective` land in `tx_fee_model`; `assemble_tx_to_sign` builds populated `TxToSign` from ledger `TransferDetails`; `TxInputSigningContext` / `TxOutputContext` / `FcmpPlusPlusContext` reshaped per §3.9. Canonical dust threshold (`shekyl-rpc::tx_fee::dust_threshold`) shared with `shekyl-scanner::coin_select`. Real `sign_transfer` / `tx_bytes` / submitter remain 2a-3; `fcmp_proof_size` KAT and `predict_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) and `creation_height` (u64) are cleartext public wire fields** (`CONFIDENTIAL_STAKING.md` §6.4.8; verifier recomputes `h_bind` + multiplier from them, §6.4.3) — not merely brute-forceable, **published directly**. The reveal is **load-bearing for inflation-safety** (the single revealed `tier` excludes tier-forgery by driving both `h_bind` and 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-opens** `CONFIDENTIAL_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) **cleartext `creation` bucketing** (the cheap middle — bind a bucketed creation into `h_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 §14 `band_sum` floor 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 width `W`**. **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 the `tier` field from the wire (cohort → `{creation_bucket}`), but `creation`/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_stake` range 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, **T7** `G_S` NUMS 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.md` V3.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 is `W=1` (per-block, scale-resistant); `W>1` is a tunable multiplier *and* restores dilution-with-growth. Cost is a bounded, **tier-relative** lock-shift (flat `W=720` ≈ +72 % on a tier-1 1,000-block lock vs ~0.5 % on tier-3 150k) → **scale `W` per tier**, all publicly derived from the revealed `tier` (no new leak). **Window-adaptivity (open `W`-choice):** sliding the window to a participant floor `k` — *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 at `k`); *soft* DAA-style `W(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 per `75-system-autonomy.mdc`); honest limit — no scheme makes `k`-anonymity from ` 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 the `file_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 confirmed `C_stake = z·G + amount·H` is **plain Pedersen with `z = OutputSecrets.z`** — the standard output blinding, **not** a fresh subtree mask and **no** `τ·H_t` term (`CONFIDENTIAL_STAKING.md` §"Pinned commitment construction (staked outputs, Decision 3C)" + the `z` mask-table row) — so `(amount, z)` recover from the staked output alone via `derive_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_epochs` only**; 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` move, not a sealed blob); `Snapshot` carries 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-grade `x` from persistence) to claim-grade `(amount, z)`: nothing claim- or theft-grade is at rest. **Held-on-hydration over fully-transient (sub-decision):** `x` stays transient-per-claim (theft-grade, claim path already composes it); `(amount, z)` is re-derived once at `Restore` and **held in memory** rather than re-derived per operation, because (a) it is read on every display poll and holding avoids a per-read `KeyEngine` round-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 / `PrepareClaimBuild` would 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-D8 `x`-cost clause (reopen *persist-`x`* only if measured resync derivation dominates UX budget and cannot be batched, `PERFORMANCE_BASELINE.md` evidence); (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-process `Secret` move, not sealed blob), the `StakeInstance` doc, the §4.7 message table (`Restore` re-derives, `Snapshot` opening-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.md` needs no change (consensus-side; it only references `x` non-persistence and defers to §3.3.1). - **wallet: `StakeEngine` async/sync split resolved — last Phase 2b field residual (`PHASE_2B_STAKE_LIFECYCLE.md` §4.6).** Verified at source that RPITIT-`+ Send` is the established idiom across **five** engines (`KeyEngine` `key.rs:803/887`, `RefreshEngine` `refresh.rs:268`, `PersistenceEngine` `persistence.rs:204/244/280`, `PendingTxEngine` `pending_tx.rs:197/263`, `DaemonEngine` `daemon.rs:206/235`), each **mixing** sync reads with RPITIT actor ops and carrying the `Send` bound in the trait contract for the async wallet-RPC/CLI/GUI call sites. **Ratified, not chosen:** `StakeEngine` stays `Send + Sync + 'static`; every actor-routed method returns `impl Future<…> + Send` (not bare `async fn`, not `async_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, no `StakeView` cache** (anti-pre- provisioning): safe because `prepare_claim_build` is the authoritative path (stands alone, re-validates against current `claimed_epochs`/`claim_pending_epochs` in-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 kept `claimable_rewards_atomic`/`unclaimed_epochs` async "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.md` evidence, not a passive "snapshot already exists") with a **named Stage-5 candidate** (`ArchivalEngine` cross-actor `is_active_staker`/`stake_tier` polling); its shape (almost certainly **uniform** over all reads, `prepare_claim_build` the 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):** the `register_pending_stake` opening-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. **`StakeView` pinned as a concrete owner-grade struct** (`id`, `tier`, `band`, `state`, per-stake `claimable: AtomicUnits`, `claimed_epochs`, `unlock_height`) — it was previously prose-only ("public fields + claimable amount"), so the per-stake `claim(stake_id)` decision had no surfaced bridge from the global `claimable_rewards()` total. **Trust-grade split made explicit:** per-stake `claimable` is a deterministic function of the confidential principal and public params, so it discloses the staked amount (and `claimed_epochs` the claim pattern); `StakeView` is 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 are `AtomicUnits` end-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-`uint64` Monero 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; `u64` reappears only at the true edges (postcard, FFI, consensus amount boundary). Consequence: `claimable_rewards_atomic -> u64` becomes `claimable_rewards -> AtomicUnits` (the type carries the "atomic" meaning, so the suffix is redundant), `projected_yield -> AtomicUnits`, `ClaimableRewardsAtomic` message → `ClaimableRewards`, and `StakeOpening.amount_atomic`/`Accruing.accrued_rewards_atomic` drop the suffix. **`StakeFilter` declared** as a closed by-state enum (`All`/`Accruing`/ `Claimable`/`FullyUnstaked`) mirroring `StakeState` — the open-ended `filter` param had no named consumer (rule 21). **Abandon-claim wiring pinned (§3.4/§6):** a built `claim()` is an ordinary claim-type `PendingTx`; the orchestrator's general pending-tx discard path dispatches `AbandonClaim` for claim-type txs specifically, so "rides the normal path" does not silently drop the `claim_pending_epochs` clear (`AbandonClaim` touches `claim_pending` only, never `claimed`, 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 stale `StakeOpening` doc-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, no `z`, no per-output secret correlation" wording was written for the secret class and does *not* cover `claimable`/`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 through `Reservation::extensions` is the rejected Option-C boundary break). The discriminator + key therefore live in the **orchestrator** (the one site holding both the `ReservationId` from `PendingTxEngine::build` and the `(stake_id, epochs)` from `prepare_claim_build`), which records `ReservationId → (stake_id, epochs)` for claim builds and consults it on discard. (3) **`StakeFilter` made total** — corrected from the 4-variant subset to a **1:1 mirror of all 7 `StakeState` variants** + an `All` sentinel, both `#[non_exhaustive]` and held in lockstep, so filtering is total (no inexpressible state, no dead variant). (4) **`AtomicUnits` is forward intent, not an existing type** — verified at source (rule 17): the newtype does **not** exist anywhere in `rust/` (the wallet uses raw `amount_atomic_units: u64`). The §6 sign-off is therefore recorded **"modulo the `AtomicUnits` convention"** (§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 in `docs/FOLLOWUPS.md` (V3.0). Note the most load-bearing constraint for that PR: `OutputClaim.amount_atomic_units` is a **secret** (wiped on drop, `[REDACTED]` Debug), so the newtype must preserve `Zeroize` + non-leaking `Debug`. - **staking: `EconomicsEngine::rate_at_epoch` finalized; `pool_weighted_total` retired (`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`** is pinned as the sole yield-schedule surface `StakeEngine` consumes, with seven semantics pins: (1) `rate_epoch` is a rate-epoch *index*, not a height — the caller converts via the public `rate_epoch_blocks`; (2) returns the public rate `ρ_e` as fixed-point `u64` (a rate, not an amount, so **not** `AtomicUnits`; consensus-defined scale the wallet consumes); (3) **fallible with `Ok(0)` ≠ `Err`** — `Ok(0)` is a settled empty-staker epoch, `Err` is "cannot determine" (unsettled epoch / unsynced mirror / overflow), un-overloading the `0` that the infallible `pool_weighted_total` conflated; (4) consensus-derived from the on-chain `band_sum`, not wallet-recomputed (Bug-2-class avoidance); (5) the **`AtomicUnits` crossing** is the yield product `own_weight · K_S`, not the rate itself (this is the checked-arithmetic centralization site the `shekyl-units` newtype exists for); (6) no per-stake state — index in, no `stake_id` (§8.2 reversion preserved); the signature uses the trait's `Self::Error` convention (the earlier `EconomicsEngineError` sketch was wrong vs `base_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 was `StakeEngine::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 from `rate_at_epoch` + chain state). The `band_sum` mirror (`ChainEconomicsSource::active_weighted_stake`) is repurposed as `rate_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_yield` consumer 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-track `pop_block` nullifier-revert cross-track dependency — no further wallet design work blocks Round 3. - **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 the `rate_at_epoch` landing (`rate_at_epoch` is 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 described `StakeEngine` as owning "principal-pool aggregation state at Stage 4" — an **8th** dead pool-denominator site the literal-name `pool_weighted_total` sweep 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_block` carry 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 consensus `pop_block` path 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 confirming `pop_block` behavior upstream (`CONFIDENTIAL_STAKING.md`, consensus Round 2), not by any wallet change. This makes the consensus `pop_block` nullifier- 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 is `OPEN` and Round 3 closes only when each carries a disposition (mitigated / FOLLOWUP+trigger / priority-reject+ criteria, rule 21). Cross-track deps recorded: consensus `pop_block` carry (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 `D` pinned + remainder range-proof construction closed (Round 2, §6.4.1 decisions 1(a)/1(b)).** Corrected the underspecified `D = SCALE` to `D = D_tier · SCALE_rate = 2^(k+1)`: `D_tier = 2` is the LCD of the pinned tier multipliers `{1.0, 1.5, 2.0}` (so `tier_num_reduced ∈ {2,3,4}` exact, one bit) and `SCALE_rate = 2^k` is the precision dial. **Decision 1(b):** the remainder commitment `C_ρ` is **folded into the reward output's existing native 64-bit `AggregateRangeProof`** as 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 needs `reward > 2²⁰³` while the reward output's own range proof bounds it `< 2⁶⁴` — a 139-bit margin at the pinned `k`), so any width `∈ [⌈log2 D⌉ … ~203]` is sound and the cheapest is the one already on the wire; a tight `ρ < D` would only forbid harmless under-claim. Because the fold fixes the proof width at 64, precision became **free** up to the field-wrap margin, so `k` is re-pinned from the obsolete "smallest `k` clearing a 0.1 % floor" (`k = 38`) to the **floor-dominated knee** **`k = 48 ⇒ D = 2⁴⁹`** (`SCALE_RATE_K`): the precision sweep's worst-case relative rate-quantization error falls until `k = 48` (`2.1e-5`) and then plateaus (the irreducible reward floor, not the rate scale, dominates), with a 139-bit margin and 132-bit `N·amount` overflow 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 in `rust/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). `μ_claim` covers 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 swapping `C_ρ` flips the challenge and rejects. The entitlement Schnorr challenge is `keccak256_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 in `rust/shekyl-staking/src/entitlement/transcript.rs` (`EntitlementChallengeInputs`, `entitlement_challenge_preimage` via `offset` constants) 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_tag` from `derive_view_tag_x25519` to `derive_view_tag_prefilter(ml_kem_ss)`; scanner leg-swap (universal decap → tag compare → X25519 on match). HKDF salt `shekyl-view-tag-prefilter-v1`. Regenerated `docs/test_vectors/PQC_OUTPUT_SECRETS.json`. Regenerated mainnet/testnet/stagenet `GENESIS_TX` in `cryptonote_config.h` (coinbase `view_tag_prefilter`; block 0 hash changes). Reference hex in the `shekyl-dev` repo at `tools/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_layers` returned the bare layer-0 Selene leaf node as the root for any `1..=SELENE_CHUNK_WIDTH`-leaf tree, diverging from the daemon's `grow_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_layers` now stops at "single node at layer ≥ 1" — every non-empty tree is depth ≥ 2; the empty tree remains the `selene_hash_init()` sentinel. Proven by the new CT-2 KAT; corrects the CT-0 freeze model (`undeepen_helios_collapse_39_to_38` renamed to `helios_root_shrinks_39_to_38_without_undeepening`; `reorg_compound_45_to_35` target re-wrapped). Docs: `CT2_DRAIN_ORDER.md` §5, `CURVE_TREE_CLIENT.md`. - **Genesis `GENESIS_TX` regen (FA-11 `enc_labels`).** Rebuilt mainnet, testnet, and stagenet genesis coinbase hex with current `build_genesis_coinbase_from_destinations` so `enc_labels` matches `enc_amounts` on 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 the `shekyl-dev` repo (`tools/genesis_builder/`); address generation: `rust/shekyl-crypto-pq/examples/gen_genesis_addrs.rs`. ### Added - **engine: `KeyEngine` migrated into a `kameo` actor (Stage 2).** Key material now lives in a `KeyActor` running on its own task and owning `AllKeysBlob` privately; the orchestrator holds a `Clone` `KeyEngineHandle` instead of `keys: Arc`. `Engine` stays seven-parameter `Engine` (no inline `K: KeyEngine` generic — the deferred-inline-shape reversion clause resolved in favor of the handle). The request half of the actor protocol is public (`OutputDetectionInput` → `OutputClaimResult`); the reply `OutputClaim` carries secret-derived bytes (the decrypted amount plus the privacy-linkable handle/key_image) and is `ZeroizeOnDrop` (see **Security** below); the `SignTransaction` message exists but its reply is a Phase-1 stub. Handle-resolved reads (`account_public_address`, primary `derive_subaddress`) serve from an immutable `KeyPublicProjection` with no mailbox round-trip; non-primary `derive_subaddress(Audit)` is served from a construction-time `AuditSubaddressSecret` projection (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-time `HandleDerivationViewSecret` (the 6-i disposition: `apply_scan_result` is synchronous under a `std::sync::RwLock` guard, so routing it through the actor mailbox is foreclosed until the Stage 4 Ledger-actor lock cutover). `KeyActor` is **fail-stop by construction**: a handler panic runs `on_panic` → stop (not restart), `on_stop` wipes the blob, and the handle maps every `kameo::SendError` to the terminal, non-retryable `KeyEngineError::KeyActorUnavailable`. `KeyEngineHandle::spawn` **requires 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, and `kameo`'s spawn is `tokio::spawn`, which any flavor hosts, so the spawn needs no `rt-multi-thread`. Tests that reach `spawn` use `#[tokio::test]`; sync tests driving a post-create method that forbids an ambient runtime enter a leaked runtime only around `create`. `tokio`'s `rt-multi-thread` feature is promoted to production `[dependencies]` as an **independent** fix for the pre-existing `drive_persistence::block_in_place` feature-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`](completed/STAGE_2_KEY_ENGINE_ACTOR.md). - **bench: Stage 2 §5.3 B9 dispatch-overhead + merge-path benchmarks.** Five `bench-internals`-gated bench targets in `shekyl-engine-core`: `engine_trait_bench_key_dispatch` (criterion; three IDs — `baseline_claim_mine` direct `LocalKeys::try_claim_output`, `actor_claim_mine` via the `KeyEngineHandle` `ask`, and the cheap `actor_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 pair `engine_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 actor `ask` paths 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. New `KeyDispatchBenchHarness`, `KeyBaselineBenchFixture`, and `MergeProjectionBenchFixture` shims are exported through `__bench_internals`; `populate_engine_handle_fields` was widened to `pub(crate)` so the merge bench drives the real post-pass. `compare.py` routes all five IDs into the `engine_trait_bench` threshold class by prefix (no script change). The baseline iai drives its synchronously-completing future with a no-op-waker poll (a current-thread Tokio `block_on` under Callgrind collapsed the count to ~4.8k handshake instructions instead of the ~15.2M decap); the three B9 rows were added to `scripts/bench/capture_rust_baseline.sh` so CI captures them. Baselines captured by CI `workflow_dispatch` (run 26732235292, SHA `d377edfdb`) and transcribed into [`docs/PERFORMANCE_BASELINE.md`](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: `EconomicsEngine` trait 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` (the `u128` pool-weighted stake denominator), and `parameters_snapshot` — as the `pub(crate)` `EconomicsEngine` trait with the `LocalEconomics` Stage 1 implementor, its `ChainEconomicsSource` read-abstraction (`LedgerChainEconomicsSource` adapter), and the `EconomicsParametersSnapshot` / `CalibrationStamp` return types. The snapshot's `CalibrationStamp.params_digest` is a canonical Blake2b-256 over the **full calibration surface the snapshot exposes** — the `EconomicParams` sub-digest plus the staker-emission consts and the `shekyl-staking` tier table — so a staker/tier recalibration that omits a `generation` bump cannot false-accept a stale snapshot (`shekyl_economics::params_digest` stays `EconomicParams`-scoped for the C4 fixture lineage). The `Engine` generic list grows the `E` slot (`Engine`); the `economics` field 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_amount` point-estimate / `as_of_height` staleness) and G5 (`parameters_snapshot` generation/digest re-compare) consumer contracts. Differential tests replay `RecordedChainFixture` vectors through a `ChainMirrorSource` against `shekyl-economics`; Criterion + iai-callgrind benches cover `base_emission_at` and `parameters_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 the `shekyl_base_block_reward` primitive, **not** to this trait — the trait remains consumer-free until its own cutover per [`docs/design/STAGE_1_PR_7_ECONOMICS_ENGINE.md`](completed/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-rolled `Zeroize` silently omitted `recovered_spend_key` (a per-wallet, per-subaddress linkable identifier) and `h_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::Scalar` once per scanned output on a daemon-drivable path — and the per-output `ho`/`y`/`z` scalars and the decrypted-amount bytes were left resident on the stack on every exit path (`Scalar`/`MontgomeryPoint` are `Zeroize` but not `ZeroizeOnDrop`). Enabled the `zeroize` feature on `curve25519-dalek` and `Zeroizing`- wrapped the locals in `scan_output_recover`. - *Finding 3:* swept the same bare-scalar pattern across the rest of `output.rs`'s production scan/construct/key-image paths and `Zeroizing`- 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`**: `OutputClaim` now is (its decrypted `amount_atomic_units` is treated as a secret, reversing an earlier "balance-display, non-secret" disposition), closing the `ask`-cancellation leak where the channel drops an un-taken reply. A `reply_path_wipes_on_drop` test asserts the reply's `Drop` wipes. No consensus or wire-format change. Design: [`docs/completed/STAGE_2_KEY_ENGINE_ACTOR.md`](completed/STAGE_2_KEY_ENGINE_ACTOR.md) §4.3.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 `dev` reality: the orchestrator is the seven-parameter `Engine` and `EconomicsEngine` is landed (PR #94), not the pre-#94 five-parameter / "economics spec-only" shape — updated `V3_ENGINE_TRAIT_BOUNDARIES.md`, `FOLLOWUPS.md`, and appended a dated closure checkpoint to `STAGE_1_COMPLETION_AUDIT.md`. (2) Recorded the deliberate `KeyEngine` inline-orchestrator-integration deferral as an explicit reversion-clause `FOLLOWUPS.md` entry (reopen criterion: the Stage 2 `KeyEngineHandle` actor migration). (3) Removed obsolete `#[allow(dead_code)]` annotations on the now-production-dispatched `LedgerEngine::synced_height` / `snapshot` and `RefreshEngine::produce_scan_result` trait methods (the `bench-internals`-only `LedgerEngine::balance` allow is retained with an updated rationale). (4) Swept stale in-code narrative across the engine crate: dropped dangling deleted-`MockLedger` precedent references, rewrote the obsolete `Mock*` test-substrate framing to the actual no-Mock substrate, normalized current-behavior `C5α`/`C5β` stage labels into behavior descriptions (preserving genuinely-historical notes), and fixed stale four-/five-parameter `Engine<…>` 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::render` now dispatches on a closed `Algorithm` enum (`AperiodicTile` / `Phyllotaxis` / `Truchet` / `Crystalline`) instead of a `&str` match with a `panic!("Unknown algorithm: …")` fallback. Dispatch is compile-time exhaustive, so an unknown algorithm is unrepresentable rather than a runtime panic. `candidate.rs` carries the layer identities as `Algorithm` constants and derives the serialized `CandidateRecipe` strings via `Algorithm::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 four `candidate.v1` renderers (`aperiodic_tile`, `phyllotaxis`, `truchet`, `crystalline`) now draw their structural parameters from a dedicated per-algorithm namespace (`shard.v1.render.`) rather than sharing the legacy eight-word `hash_words` pool. 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_tile` previously consumed zero hash entropy (identical features → identical geometry) and now draws a rosette rotation and palette-spread jitter. The now-dead `RenderParameters::hash_unit` / `hash_words` accessor 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_reward` base subsidy delegates to Rust (Stage 1 PR 7 §5.8).** The duplicated C++ ESF base-subsidy formula (`(MONEY_SUPPLY − already_generated) >> esf` + tail floor) in `cryptonote::get_block_reward` (`src/cryptonote_basic/cryptonote_basic_impl.cpp`) is deleted; the 4-arg path now calls the canonical Rust primitive `shekyl_base_block_reward` (7-base) through the new `shekyl::base_subsidy_before_penalty` thin wrapper in [`src/shekyl/economics.h`](../src/shekyl/economics.h), mirroring the `compute_fee_burn` / `compute_emission_split` shape. 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-base `dev` tip (7-base merge `fed6f594b`; C2a′ ancestor by topology). Closes the `docs/FOLLOWUPS.md` base-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 `--config` flag exists), so the file was inert — the existing `RUSTSEC-2026-0097` (rand) ignore never applied. The config now lives at `rust/.cargo/audit.toml`, making both ignores live (verified under `cargo audit --deny warnings`). Added `RUSTSEC-2024-0436` (`paste` unmaintained) to the ignore list with rationale: `paste` is a compile-time-only proc-macro pulled transitively by `shekyl-shard-visual`'s `imageproc` dependency, emits no runtime code, and is not security- or consensus-load-bearing; reopen if `imageproc` / `nalgebra` drop `paste` or if the crate replaces `imageproc` with hand-rolled rasterization. Pre-existing `atomic-polyfill` / `bincode` unmaintained notices and the `aes` yanked notice are unrelated to this change and left untouched (plain `cargo 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 through `LedgerEngine::apply_scan_result`, which discarded the inserted-index `Vec` and never ran `populate_engine_handle_fields`, leaving newly-merged transfers without `output_handle` / `source_ciphertext`. The mutator is removed from the `LedgerEngine` trait (now read-only: `synced_height` / `snapshot` / `balance`); `run_refresh_task` and `Engine::start_refresh` are specialized to `LocalLedger` and merge through the inherent `Engine::apply_scan_result`, which runs the merge body and the M3b post-pass under one `LocalLedger` write guard. This closes P1 (post-pass skip) and P3 (discarded `Vec` allocation) in a single commit. The dead `FaultInjecting` test wrapper and `replace_ledger` test helper were deleted; hybrid retry tests drive `ConcurrentMutation` producer-side. Shape (b) per [`docs/design/STAGE_1_PR_4_REFRESH_ENGINE.md`](completed/STAGE_1_PR_4_REFRESH_ENGINE.md) §8; atomicity rationale per [`docs/completed/STAGE_1_PR_3_M3B_PREFLIGHT.md`](completed/STAGE_1_PR_3_M3B_PREFLIGHT.md) §3. ### Documentation - **stake-lifecycle: §5 reorg reconciliation is forward-rebuild only — drop the `pop_block` nullifier-revert as wallet-tracked work.** `docs/design/PHASE_2B_STAKE_LIFECYCLE.md` previously narrated the consensus `pop_block` stake-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 rebuilds `claimed_epochs` forward from the canonical post-reorg chain (derive-don't-accumulate, the same pattern as [`CURVE_TREE_CLIENT.md`](design/CURVE_TREE_CLIENT.md)'s `from_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's `pop_block` mechanism (`CONFIDENTIAL_STAKING.md`, deleted 2026-08-26) §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** ("does `claimed_epochs` recompute 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` / `KeyEngine` trait surfaces.** Documentation-only rustdoc work bringing the three trait files to the `LedgerEngine`/`DaemonEngine` bar per [`docs/V3_ENGINE_TRAIT_CONFORMANCE_LENSES.md`](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; hydration `open`/`create` stays on `Engine` per Q9.11), the CL-2 `# Supertrait bounds` section (`Send + Sync + 'static` driven from the sync lifecycle via `drive_persistence`, shared by `Arc`; **not** `Clone` — single-owner artifact + advisory lock), the full CL-5 `# Stage-4 swap-in` section, the CL-4 C/I/P triad on all six methods (including a new `save_prefs` body), and per-attribute CL-6 `#[allow(dead_code)]` reasons on `base_path`/`network`/`capability`. `save_prefs` is documented as **not** panicking on mutex poisoning — verified against the implementor, which routes through `shekyl_engine_prefs::save_prefs` and never acquires the `WalletFile` mutex. - `pending_tx.rs`: added the CL-4 C/I/P triad to `build`/`submit`/`discard`/`signal_mempool_evicted` and `# Panics` to `outstanding`, plus a module-level note on the deliberate poison-handling asymmetry (mutators map `Mutex` poisoning to a domain error; `outstanding` panics on it). - `key.rs`: added `# Panics` to all four methods (`account_public_address` lockless; `derive_subaddress` / `try_claim_output` `RwLock`-poison; `sign_transaction` M3a stub does not panic), reconciled `sign_transaction`'s `# Cancellation` from **b** to **a** to match the §4 table (with a PR-5 reversion-clause forward note), and rewrote the stale `# M3a transitional bridge` section (which referenced the removed `TxInputSigningContext::source_secrets` field) to state the M3a stub reality. - Reconciled [`docs/V3_ENGINE_TRAIT_BOUNDARIES.md`](V3_ENGINE_TRAIT_BOUNDARIES.md) §4: `discard` `async → sync` (cancel `b → n/a`, idempotency `yes → no`), added the missing `signal_mempool_evicted` row, and appended the PR-5 forward trigger to the `sign_transaction` cancel cell. Flipped the [`docs/V3_ENGINE_TRAIT_CONFORMANCE_LENSES.md`](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 in `docs/FOLLOWUPS.md` (V3.0). - **Codify the seven engine-trait conformance lenses.** Added [`docs/V3_ENGINE_TRAIT_CONFORMANCE_LENSES.md`](V3_ENGINE_TRAIT_CONFORMANCE_LENSES.md), which enumerates the documentation-as-contract discipline previously implicit in the `LedgerEngine` / `DaemonEngine` reference surfaces and scattered across `V3_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-`dev` conformance scorecard for all seven traits, and pins the add/remove-a-lens discipline per `21-reversion-clause-discipline.mdc`. Cross-referenced from `V3_ENGINE_TRAIT_BOUNDARIES.md` §1.6 and hooked into the per-trait PR pre-flight via `STAGE_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 (`libunbound` stub, vendored-code warnings, vcpkg manifest-mode) moved from [`docs/STRUCTURAL_TODO.md`](STRUCTURAL_TODO.md) into [`docs/FOLLOWUPS.md`](FOLLOWUPS.md) §"V3.2 — Rust cutover and cleanup"; `STRUCTURAL_TODO.md` repurposed 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 in `rust/shekyl-logging/src/legacy.rs` and `.cursor/rules/93-legacy-symbol-migration.mdc` retargeted to `FOLLOWUPS.md`. (2) Fixed stale `scan_range_start` / `effective_floor_at_tip` references in `FOLLOWUPS.md` and `STAGE_1_PR_4_REFRESH_ENGINE.md` (helpers removed in the P2 TOCTOU fix, commit `87264a3a2`; the producer now derives its scan start from the anchored snapshot). (3) Added a dated closure checkpoint to [`docs/completed/STAGE_1_COMPLETION_AUDIT.md`](completed/STAGE_1_COMPLETION_AUDIT.md) recording P1/P2/P3 closure and confirming C2c economics cutover remains the sole open V3.0 economics item (standalone 7-cutover PR per `STAGE_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).** `LocalRefresh` carries `scan_start_floor` from `sync_state.restore_from_height` and session `skip_to_height` / `refresh_from_block_height` overrides. Refresh preflight anchors the ledger at `floor - 1` when needed so the merge gate stays `start == synced_height + 1`; the producer scans from the floor through tip. `Engine::create` persists `restore_height_hint` into `sync_state.restore_from_height`. - **Workspace MSRV raised 1.85 → 1.88; `kameo = "=0.20.0"` pinned (Stage 2 gate).** Satisfies the three preconditions in the `docs/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 via `rust-version.workspace = true` across 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). No `rust-toolchain.toml` added — 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_state` now takes session-cached `wrap_key_region_2` only; password-taking steady-state save deleted. `shekyl_wallet_save_state` FFI drops the password parameters. Design: [`docs/design/STAGE_1_PR_6_PERSISTENCE_ENGINE.md`](completed/STAGE_1_PR_6_PERSISTENCE_ENGINE.md). - **Wallet file format v1: per-region HKDF wrap keys (spec + implementation).** [`docs/WALLET_FILE_FORMAT_V1.md`](WALLET_FILE_FORMAT_V1.md) §2.6 prescribes `wrap_key_region_1` (label-only HKDF) and `wrap_key_region_2` (`info || addr`) via HKDF-SHA-256 Expand from CSPRNG `file_kek`, replacing direct `file_kek` AEAD for regions 1 and 2. On-disk layout and `file_version` unchanged; Tier-3 KAT fixtures regenerated. See [`docs/design/WALLET_FILE_FORMAT_V1_HKDF_REGION_DERIVATION.md`](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.md`](completed/STAGE_1_PR_6_PERSISTENCE_ENGINE.md) records historical wallet-disaster lessons, OS discipline for F5(b), Stage 4 actor-model pins (bounded mailbox, save coalescing, supervisor `stop`), and substrate verification (KDF params in wrap AAD; HKDF `address` = 65-byte classical address). Blocking PR 6: panic-hook redaction test, mlock honest contract, region-2 `OsRng` nonce rustdoc. V3.1/V3.x items in [`docs/FOLLOWUPS.md`](FOLLOWUPS.md). - **Stage 0 PR-A — iai-callgrind symmetry rule** (`3d313256c`). Backfill. [`docs/design/STAGE_0_HARNESS.md`](completed/STAGE_0_HARNESS.md) §4.2 codifies the symmetry rule (setup *and* fixture teardown excluded from the measured region; criterion amortizes `Drop` via `b.iter`, iai-callgrind does not) and adds Finding 5 to the §4.4 gap-check inventory. Closes the Drop-contamination capture (`synced_height` reported 60,033 instructions vs the expected low-tens, a property-preservation gap), class-level across every `engine_trait_bench_*` bench. - **Stage 0 PR-A-extension — iai-callgrind boundary rule** (`2e5309ad3`). Backfill. [`docs/design/STAGE_0_HARNESS.md`](completed/STAGE_0_HARNESS.md) §4.2 adds the boundary rule (iai-callgrind measures function-boundary value movement; `Engine` is 6,296 bytes, so by-value fixture passing cost ~600 instructions of memcpy) and the §4.4 unified `(Box>, 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`](completed/STAGE_0_HARNESS.md) §4.2 adds the hoisting rule (criterion-side `b.iter` iter-amortization can elide state-dependent compute the author meant to count) and the §4.4 two-anchor static check (predict criterion `median_ns` from 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`](V3_ENGINE_TRAIT_BOUNDARIES.md) §2.6 amends steady-state `save_state` / `save_prefs` to F5(b) sealing keys; `shekyl-engine-core` adds `PersistenceError`, `OpenError::Persistence`, `ChangePasswordError`, `StateWrapKey`, and the `PersistenceEngine` trait module; `shekyl-crypto-pq` adds `seal_state_file_with_wrap_key_region_2`; `shekyl-engine-file` adds `Mutex`, `rotate_password` on `&self`, additive `save_state_with_wrap_key_region_2`, and `base_path()`. `WalletFile` trait impl and `Engine` wiring follow in C3–C5. Design: [`docs/design/STAGE_1_PR_6_PERSISTENCE_ENGINE.md`](completed/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 per [`docs/completed/RANDOMX_V2_PHASE2H_PLAN.md`](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-inputs `compute_hash` divergence FOLLOWUP; that FOLLOWUP closed on `dev` via [PR #79](https://github.com/Shekyl-Foundation/shekyl-core/pull/79) (`989610cac`, 2026-05-26; root cause: `RANDOMX_FLAG_V2` missing at `randomx_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.rs`](../rust/shekyl-randomx-differential/src/adversarial_canonical_outputs.rs) lands 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 via `gen_canonical_outputs.rs` Family-1 branch. - **C2 `PreparedCache::from_raw_for_testing` accessor.** The R1-D2 close cache-level test-internals accessor lands on [`rust/shekyl-pow-randomx/src/prepared_cache.rs`](../rust/shekyl-pow-randomx/src/prepared_cache.rs) under the existing `test-internals` feature gate (R5-D1 carve-out shape; sole consumer is `shekyl-randomx-differential`). C-side symmetry via the pre-existing `randomx_get_cache_memory` extraction path keeps the production surface unchanged. A round-trip test asserts that `from_raw_for_testing(seedhash, bytes)` of a fresh `derive` output's bytes reproduces the same `PreparedCache` (cache-bytes byte-identical; superscalar programs re-derived from the seedhash). - **C3 recipe types + first-class evaluator.** New [`rust/shekyl-randomx-differential/src/adversarial/`](../rust/shekyl-randomx-differential/src/adversarial/) module landing `types.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 the `recipes/` 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 the `gen_canonical_outputs.rs` Family-1 generator branch; each entry pins the expected `(seedhash, hash)` against the C reference per M1 canonical-output discipline. - **C6 `mode_adversarial_ratio` binary mode.** New [`rust/shekyl-randomx-differential/src/mode_adversarial_ratio.rs`](../rust/shekyl-randomx-differential/src/mode_adversarial_ratio.rs) implements the worst-case-ratio measurement mode replacing the §3.19 R7-D4 diagnostic-only branch at `main.rs`'s `--mode=adversarial-ratio` dispatch (renamed from `--mode=worst-case` per 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 structured `T6_OBSERVATION` / `T6_CLAIM_2_TRACKING` JSON 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 under [`rust/shekyl-randomx-differential/tests/`](../rust/shekyl-randomx-differential/tests/). T2 asserts byte-equality between Rust and C across the recipe corpus; T6 invokes `mode_adversarial_ratio` and enforces Claim 1 + emits Claim 2 tracking signals. C7 cross-input diagnostics revealed that the Phase 2g `compute_hash` FOLLOWUP'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.yml`](../.github/workflows/randomx-v2-differential.yml) gains a per-PR `cargo test --ignored T2` step gated behind `if: false` until the divergence FOLLOWUP closes. New [`.github/workflows/randomx-v2-adversarial-ratio.yml`](../.github/workflows/randomx-v2-adversarial-ratio.yml) workflow_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); same `if: false` gating 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.sh` implements R2-D4's mechanical citation validation: parses recipe `rationale` fields and validates per-category prefix (R1-D8 taxonomy invariant), cited plan-doc existence under `docs/design/`, cited source-file existence under `rust/shekyl-pow-randomx/src/` (for `*.rs`) or `external/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-PR `structural-validate` job 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_hash` divergence" entry is amended to record the Phase 2h cross-input findings (universal-across-inputs scope correction; the 192-byte `t16` vector 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.0 `shekyl-pow-randomx::compute_hash`-divergence-from-C-reference FOLLOWUP by passing `RANDOMX_FLAG_V2` at `randomx_create_vm` in `COracleSession::new`. Following PR #79's merge, this PR rebased onto the post-#79 `dev` and landed four commits discharging the activation-surface contract that C7/C8 established: 1. **`c71ce2413` — `RANDOMX_FLAG_V2` extension to `COracleSession::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 to `Self::new`. New [`rust/shekyl-randomx-differential/tests/c_oracle_session_round_trip.rs`](../rust/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. 2. **`6fc059e1e` — lift T2 `#[ignore]` + workflow `if: false` gating.** Removes the `#[ignore]` attribute on `t2_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 per `21-reversion-clause-discipline.mdc`; preserves the R1-D6 close Reframe 1 substrate-broken vs ignore-ladder distinction as the discipline's authoritative instance. The `randomx-v2-differential.yml` workflow's `if: false` gate on the dedicated T2 step is lifted; the preceding default `cargo test` step gains `-- --skip t2_adversarial_corpus_byte_equality` so T2 runs exactly once per CI invocation (release-mode via the dedicated step) within R1-D6 close Reframe 2's `T2_PER_PR_BUDGET_MS` budget. 3. **`1b1bda7df` — lift T6 workflow `if: false` gating + reframe T6 docs.** Rewrites the `worst_case_ratio` module rustdoc's "C7 close" section as past-tense "Post-PR-#79 substrate note (FOLLOWUP closed)" and lifts the `randomx-v2-adversarial-ratio.yml` workflow step's `if: false` gate. 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 `--ignored` flag persists on this basis. `workflow_dispatch` cadence is unchanged. 4. **`72a4a9eed` — reframe T16 docs as regression guard.** Rewrites `divergence_triage` module 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 commit `33d22a83b`, 2026-05-25). Final sub-PR of the Rust pure-software RandomX v2 verifier port per [`docs/design/RANDOMX_V2_PLAN.md`](design/RANDOMX_V2_PLAN.md) §"Track A — Phase 2" and the design plan [`docs/completed/RANDOMX_V2_PHASE2G_PLAN.md`](completed/RANDOMX_V2_PHASE2G_PLAN.md). Stack landed across the planned C0–C10 work commits plus follow-ups (two Copilot review rounds, two mechanical `rustfmt` absorptions, 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 new `rust/randomx-v2-sys` bindings 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 from `shekyl-pow-randomx`; the verifier crate's `cargo test` still succeeds without the C library present). Preceded by the R5-D1 substrate-amendment PR #74 (merge `93d1155bb`) that landed the `test-internals` feature gate on `shekyl-pow-randomx` exposing `PreparedCache::cache_block_bytes_for_testing` (gated by `cfg(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-sys` skeleton + CMake gate** (commits `8d8d4a109`, `013984118`, `e8d2eaccf`, `8b67d7775`, `e8dec6278`, `432162ddb`; C0–C4). New [`rust/randomx-v2-sys`](../rust/randomx-v2-sys) crate is the sole consumer of the v2 fork's C ABI per §5.3 R1-D14; its `build.rs` resolves `RANDOMX_V2_INSTALL_DIR` (preferred) or falls back to the in-tree install layout under `/external/randomx-v2-install` per the R5-D2-refined R4-D3 soft-fail discipline (commit `429044cf8`'s plan-doc refinement landed the soft-fail-on-missing-library framing before C3's implementation). The harness crate [`rust/shekyl-randomx-differential`](../rust/shekyl-randomx-differential) is a `bin` with hand-rolled argument parsing (no `clap` dependency) and dispatches `--mode=correctness`, `--mode=latency`, `--mode=concurrent`, plus a deferred `--mode=worst-case` that exits with an informative diagnostic pointing at the post-2g design round per §3.19 R7-D4. `CMakeLists.txt` gains the `BUILD_RANDOMX_V2_DIFFERENTIAL_HARNESS` option (default OFF; flipped ON in CI) per Phase 0's miner-only-build-flag pattern. - **C5a corpus + canonical outputs (R6 cluster)** (`5e00f457e`, `12884d945`). `corpus.rs` builds the random `(seedhash, data)` corpus with a bimodal block-template-shaped `[200 .. 600·1024)` byte distribution per §3.16, and `canonical_outputs.rs` carries 1024 pre-computed `(seedhash, data) → hash` canonicals generated against the pinned C reference at the workspace-pinned fork SHA (per §5.4 R6 cluster's canonical-pinning discipline). The `gen_canonical_outputs.rs` tooling binary regenerates the canonicals as a separate operation outside the harness's runtime modes (skip-listed in `.cargo/mutants.toml` per 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_t` edge-case data corpus) under two independent substrate findings: (i) the verifier-accessor gap (the class-heaviness grinding methodology requires a `test-internals`-gated opcode-stream accessor whose implementation would duplicate `compute_hash_inner` under 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 in [`docs/FOLLOWUPS.md`](FOLLOWUPS.md) (V3.0 pre-genesis queue) as "Post-2g adversarial-corpus methodology + implementation." - **Cache-precondition + Rust/C oracle wrappers** (`558eba59a`). `cache_precondition.rs` derives the Rust and C caches from the same seedhash, compares them block-by-block via the `test-internals`-gated `cache_block_bytes_for_testing` accessor, 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.rs` wraps `shekyl-pow-randomx`'s `PreparedCache` + `compute_hash` surface; `c_oracle.rs` wraps `randomx-v2-sys`'s `randomx_alloc_cache` / `randomx_init_cache` / `randomx_create_vm` / `randomx_calculate_hash` lifecycle. 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_correctness` walks the random corpus + canonical-output corpus, asserting Rust hash = C hash = canonical hash at every entry. `mode_latency` benchmarks 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_concurrent` orchestrates multi-worker correctness assertion plus Linux-specific RSS-ceiling enforcement via `/proc/self/statm` sampling (`RSS_CEILING_BYTES`, `RSS_TOLERANCE`, `RSS_SAMPLE_INTERVAL`, `RSS_STEADY_STATE_WARMUP_HASHES` constants documented inline); the smoke test surfaced the known V3.0 `compute_hash` divergence at large data inputs per `docs/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.rs` defines the 11-field structured-JSON failure schema (M4/T11) emitted to stderr on Rust vs. C divergence; the schema carries `mode`, `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 from `cache_precondition.rs` when relevant. `invocation_banner.rs` emits the M4/T17 banner to stderr before any test output, recording mode, corpus sizes, seedhash count, fork pin, and the `test-internals` feature- gate citation as the harness's authority-claim line. The rustfmt drift commit `b63cd2592` absorbed 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.yml`](../.github/workflows/randomx-v2-differential.yml) runs the `structural-validate` job per-PR (build cleanliness, unit/integration tests, invariant-script coverage, `cargo fmt` + `clippy`) and the `mutants` job on a staggered weekly cron; runtime modes (correctness, latency, concurrent end-to-end) are deferred behind `continue-on-error: true` with a comment pointing at the `docs/FOLLOWUPS.md` V3.0 `compute_hash` divergence 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.yml` and `.github/workflows/codeql.yml` gain `--exclude shekyl-randomx-differential` on their workspace `cargo build` / `cargo test` invocations so the daemon CI matrix does not link against the C library it does not need; `build.yml`'s `lint-rust-debug-macros` step gains a `*/src/bin/*` exclusion so `gen_canonical_outputs.rs`'s `println!` (legitimate CLI output) is not flagged. [`scripts/ci/check_randomx_crate_invariants.sh`](../scripts/ci/check_randomx_crate_invariants.sh) is extended to scan `randomx-v2-sys` and `shekyl-randomx-differential` for Pattern A (`OnceCell`/`OnceLock`/`Lazy` imports) and Pattern B (column-0 `static` declarations); `shekyl-randomx-differential` is also scanned for Pattern C (FFI exports), while `randomx-v2-sys` is exempt from Pattern C (it is the FFI consumer crate). New [`rust/shekyl-randomx-differential/tests/crate_invariants.rs`](../rust/shekyl-randomx-differential/tests/crate_invariants.rs) integration tests T13 (script coverage), T14 (`randomx-v2-sys` sole-consumer property — verified by walking `cargo metadata` to confirm no other workspace member depends on `randomx-v2-sys`), and T15 (`randomx-v2-sys` signature-audit pin — matches the bindings file's fork-pin SHA against the submodule HEAD at `external/randomx-v2`). New `.cargo/mutants.toml` (deleted 2026-08-19 with the T18 re-scope; see `docs/design/RANDOMX_V2_MUTATION_REGIME.md` MR-DQ-8) configured `cargo-mutants` with `timeout_multiplier = 5.0` and skip-globs for tooling binaries (`src/bin/**`) and canonical outputs (`canonical_outputs.rs`) per the skip-list discipline. New [`.github/pull_request_template.md`](../.github/pull_request_template.md) carries 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.rs` warning message uses `/external/randomx-v2-install` as the documented fallback path string; (2) `RANDOMX_V2_PHASE2G_PLAN.md` §3992's embedded `build.rs` example matches the live wording; (3) the `randomx-v2-sys` bindings 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_hex` now 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's `Seedhash::Display` via a new `parse_seedhash_hex_rejects_uppercase` regression test; (2) `mode_latency::median_p95_max`'s doc-comment now states `samples[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 a `Copy` type) over `.clone()`; (4) `failure_output::timestamp_increases_across_constructions` is replaced by `timestamp_is_nonzero_and_recent`, which asserts the timestamp is non-zero and within a plausible epoch window (post-2020 / pre-2200) instead of relying on `SystemTime::now()` monotonicity through a `thread::sleep(1s)` (the prior test was flaky under clock adjustments); (5) `cache_precondition::build_divergence_window` is refactored from O(N) re-iteration of the cache block stream to O(1)-block streaming construction by passing the `current_block`, buffering the `prev_block`, and accepting the `remainder_iter` as 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.rs` doc-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`):** Format `cargo fmt --all -- --check` ✓; Lint `cargo clippy -p shekyl-randomx-differential -p randomx-v2-sys --all-targets -- -D warnings` ✓; Test `cargo test -p shekyl-randomx-differential -p randomx-v2-sys --release` ✓ (unit + integration including T13/T14/T15); Crate-invariant gate `scripts/ci/check_randomx_crate_invariants.sh` ✓ (extended scan scope across `shekyl-pow-randomx`, `randomx-v2-sys`, `shekyl-randomx-differential`); FPU unsafe grep `scripts/ci/check_randomx_fpu_rounding.sh` ✓ (inherited from 2d); workspace test `cargo test --workspace --exclude shekyl-randomx-differential` ✓ on the daemon CI matrix (the harness crate's tests are exercised on the `randomx-v2-differential.yml` matrix 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 in `docs/FOLLOWUPS.md` V3.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-case` flag is reachable but emits the deferral diagnostic referencing the FOLLOWUPS entry. - **`compute_hash` divergence 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 in `compute_hash`'s VM path. **Closed 2026-05-26 by substrate-triage** on `chore/randomx-v2-c-oracle-flag-v2` — root cause was the harness's C oracle and canonical-output generator both passing `RANDOMX_FLAG_DEFAULT` (v1, PROGRAM_SIZE = 256) to `randomx_create_vm` against a Rust verifier implementing v2 (PROGRAM_SIZE = 384). The Rust verifier was correct throughout. Fix: expose `RANDOMX_FLAG_V2 = 128` in `randomx-v2-sys` with cache- vs-VM flag-split docs (cache memory is V2-flag-invariant per `external/randomx-v2/src/randomx.cpp:79`'s `(JIT | LARGE_PAGES)` mask at `randomx_alloc_cache`; only `randomx_create_vm` honors the V2 bit); switch the two callsites at `c_oracle.rs` + `gen_canonical_outputs.rs`; regenerate `CANONICAL_RANDOM_HASHES` under the v2 flag (`v1-c5a-nightly-1024` → `v2-flag-nightly-1024`; `CANONICAL_CACHE_SHAS` unchanged, as predicted by the mask). End-to-end `--mode=correctness` re-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 in `docs/FOLLOWUPS.md` "Recently resolved (audit trail)" section; post-mortem with the missed-altitude finding in `docs/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 runs `mode_latency` informationally, not as a CI gate. - **RandomX v2 Track A Phase 2f implementation core landed** (`feat/randomx-v2-phase2f-impl`, 2026-05-23). Implements [`docs/completed/RANDOMX_V2_PHASE2F_PLAN.md`](completed/RANDOMX_V2_PHASE2F_PLAN.md) Round 2 + Round 3 dispositions on [`rust/shekyl-pow-randomx`](../rust/shekyl-pow-randomx) in five commits versus the §8 Round 3 ceiling of six (commit 5 omitted per Branch A — see prediction-vs-measured reconciliation below). - **`Seedhash` newtype + `PreparedCache` bundle** (`e687cf68b`). Closes the Phase 2c-inherited consensus-correctness footgun where `compute_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. New [`src/seedhash.rs`](../rust/shekyl-pow-randomx/src/seedhash.rs) introduces `pub struct Seedhash([u8; 32])` with `from_bytes` / `as_bytes` / `Display` (lowercase hex per §1.1 Round 2 + post-closure pin #1) replacing every `&[u8; 32]` seedhash parameter site. New [`src/prepared_cache.rs`](../rust/shekyl-pow-randomx/src/prepared_cache.rs) bundles `Cache + Seedhash` with `PreparedCache::derive` as the single public construction path; `Cache` transitions `pub → pub(crate)` per §1.1 Round 2. `compute_hash` signature transitions from `(&Cache, &[u8; 32], &[u8])` to `(&PreparedCache, &[u8])`. Atomic codebase sweep updates every in-crate call site (Phase 2c/2d tests, `vm.rs` tests, `cache.rs` tests, benches) per §3.1 Round 2 sweep-discipline. Per `16-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. - **`CacheStore` two-slot type** (`31aa0ff9d`). New [`src/cache_store.rs`](../rust/shekyl-pow-randomx/src/cache_store.rs) implements §3.1 Round 2's frozen API: `lookup`, `lookup_or_derive`, `set_canonical`. Internal sync-shape per §3.1 Round 2: per-slot `RwLock>>` (canonical non-evictable + transient displace-on-publish), `Mutex>>` 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`). New [`scripts/ci/check_randomx_crate_invariants.sh`](../scripts/ci/check_randomx_crate_invariants.sh) enforces §3.6 R1-E1 patterns A/B/C: pattern A bans imports of `once_cell` / `lazy_static` / `OnceLock` / `LazyLock` (stricter than module-level-static-only by rejecting at the import); pattern B bans column-0 `static` declarations (function-local statics live inside fn bodies and are indented; `const` items are a different keyword); pattern C bans `#[no_mangle]`, `#[unsafe(no_mangle)]`, `#[export_name`, `#[unsafe(export_name`, and `extern "C" fn` definition form anchored at column 0 modulo attribute indentation (so the `lib.rs` rustdoc 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 if `cargo fmt --check` is a CI gate (which it is per Phase 2c R0-D6). New `.github/workflows/build.yml` step sibling to the FPU-rounding step. New [`tests/crate_invariants.rs`](../rust/shekyl-pow-randomx/tests/crate_invariants.rs) cargo-test wrapper makes the gate runnable via `cargo test` for local pre-PR checks. Verification at HEAD: zero hits across `rust/shekyl-pow-randomx/src/`. - **Cfg-gated `VmStatePool` + four-bench A/B harness** (`3121b726d`). New [`src/vm_pool.rs`](../rust/shekyl-pow-randomx/src/vm_pool.rs) gated 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>` storage with capacity cap; `acquire()` returns a `VmStateGuard` whose `Drop` returns the instance to the pool if capacity allows. `vm.rs` factors `compute_hash` into a thin wrapper over `pub(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_inner` zeros `state.fprc` on entry (the only `VmState` field with observable carry-over across pooled reuse, since CFROUND writes `fprc` during `execute_program` but 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. New [`benches/per_call_alloc.rs`](../rust/shekyl-pow-randomx/benches/per_call_alloc.rs) measures B-2 (scratchpad zero-init) + B-3 (register-file/program alloc); `compute_hash_alloc.rs` extends 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.md`](../rust/shekyl-pow-randomx/BENCH_RESULTS.md) records 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-gated `VmStatePool` stays 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 production `compute_hash` body. **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`](../.cursor/rules/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`):** Format `cargo fmt -p shekyl-pow-randomx -- --check` ✓; Lint `cargo clippy -p shekyl-pow-randomx --all-targets -- -D warnings` ✓ (feature off); Lint `cargo clippy -p shekyl-pow-randomx --all-targets --features internal-pool-bench -- -D warnings` ✓ (feature on); Test `cargo 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); Doc `cargo doc -p shekyl-pow-randomx --no-deps` ✓; FPU unsafe grep `scripts/ci/check_randomx_fpu_rounding.sh` ✓ (inherited from 2d); Crate-invariant grep `scripts/ci/check_randomx_crate_invariants.sh` ✓ (new gate landed in commit 3); Bench delta informational — `compute_hash_alloc::per_call` 307.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 against `84d5ba72a` and 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.rs` rustdoc claimed an active regression-detection mechanism the scan scope did not realize.** The cargo-test wrapper preamble described "would-match" comments inside `tests/crate_invariants.rs` as 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's `CRATE_SRC="rust/shekyl-pow-randomx/src"` constant excluded the test directory entirely, so the regression-detection claim was a fiction. **Fix.** Expanded `CRATE_SRC` from a single path to an array `("rust/shekyl-pow-randomx/src", "rust/shekyl-pow-randomx/tests", "rust/shekyl-pow-randomx/benches")` in [`scripts/ci/check_randomx_crate_invariants.sh`](../scripts/ci/check_randomx_crate_invariants.sh). The recursive `grep` arm gains `--include='*.rs'` to skip C/C++ reference-vector generators at `tests/vectors/reference//_generator/*.{c,cpp}` (legitimate column-0 `static` declarations under C/C++ semantics; out of scope for a Rust-targeted invariant gate). The per-file `awk` multi-line scanner already iterated via `find ... -name '*.rs'` and picked up the change automatically. The verifier crate's `tests/` and `benches/` 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 in `tests/` → gate FAILS; multi-line bypass plant in `benches/` → gate FAILS; column-0 `static` plant in `tests/` → gate FAILS; `pub extern "C" fn` plant in `benches/` → 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) the [`tests/crate_invariants.rs:146-147`](../rust/shekyl-pow-randomx/tests/crate_invariants.rs) would-match comments, exactly as the rustdoc claim describes; (2) legitimate function-local indented `use std::sync::OnceLock;` statements inside `#[cfg(test)] mod tests { }` blocks at `src/cache_store.rs:596`, `src/vm.rs:2767`, and `src/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.rs`](../rust/shekyl-pow-randomx/tests/crate_invariants.rs) preamble 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 against `321b89edb` and 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 per `21-reversion-clause-discipline.mdc`'s post-closure-pin discipline. - **NF7 — `PATTERN_RUNTIME_STATE` regex bypassed by rustfmt-default multi-line grouped imports.** §3.6 Round 3 froze Pattern A as a column-0-anchored `grep -E` regex matching banned identifiers (`once_cell` / `lazy_static` / `OnceLock` / `LazyLock`) anywhere on the same line as a `use` statement. The single-line grouped form `use std::sync::{Arc, OnceLock};` is correctly caught (`OnceLock` appears on the same line as the column-0 `use`); the rustfmt-default multi-line grouped form, where the `use` opener 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 default `imports_granularity = "Preserve"` accepts the multi-line form and a `cargo fmt`-mediated rewrite from the single-line form is a one-`max_width`-overflow away (or a future `imports_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 `awk` scanner that complements the single-line `grep` regex in [`scripts/ci/check_randomx_crate_invariants.sh`](../scripts/ci/check_randomx_crate_invariants.sh). The scanner triggers on any column-0 `use` statement opening an unclosed brace block, accumulates subsequent lines tracking nested-brace depth via balanced `{`/`}` counts (so `use 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-line `grep`, multi-line `awk`) 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 wrapper [`tests/crate_invariants.rs`](../rust/shekyl-pow-randomx/tests/crate_invariants.rs) invokes 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 against `d4d88bdc1` and 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 per `21-reversion-clause-discipline.mdc`'s post-closure-pin discipline. - **NF3 — `lib.rs` crate-level rustdoc still framed `dispatch_instruction` as 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's `compute_hash_alloc` description to record the post-2d baseline alongside the 2c stub-NOP number. - **NF4 — `benches/compute_hash_alloc.rs` rustdoc framed the per-call cost composition under the stub-NOP body** ("Under the stub-NOP `dispatch_instruction` body, 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 + the `PER_CALL_SAMPLE_SIZE` rationale to record the post-2d baseline. - **NF5 — `Cargo.toml` `internal-pool-bench` feature comment claimed `VmStatePool` is a `pub(crate)` type whose `Default` panics in non-test builds.** First half is wrong post-PR-72 F2 (the type is `#[doc(hidden)] pub` so the criterion bench in `benches/compute_hash_alloc.rs`, a separate cargo target, can name `VmStatePool::new` and `compute_hash_with_pool` across the crate boundary; `pub(crate)` would forbid that). Second half is incomplete (the panic is gated specifically by `cfg(all(not(test), feature = "internal-pool-bench"))`; the no-feature production build never compiles `vm_pool` at 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 when `Default::default()` is called outside `#[cfg(test)]`, enforcing §3.5 R1-D5 explicit-capacity at Phase 3a). - **NF6 — `DerivationSlot::wait_for_result` deadlocks on leader thread panic** in [`rust/shekyl-pow-randomx/src/cache_store.rs`](../rust/shekyl-pow-randomx/src/cache_store.rs). Round 2 §3.1 pinned the in-flight-derivation rendezvous as `Mutex>>` at the architecture level; the implementation used a `Mutex>>` per-slot rendezvous with a `Condvar` for follower wake-up. Pre-fix, if the leader thread panicked inside `PreparedCache::derive` (e.g., allocation failure during the 256 MiB Argon2d-512 fill), `slot.publish` never ran, the `inner` mutex stayed at `None`, the `Condvar` was never broadcast, and (a) every follower already parked on `cv.wait` blocked forever; (b) the `in_flight` HashMap entry was never removed, so subsequent callers for the same seedhash acquired `in_flight.lock()`, found the orphaned slot, became followers of the dead leader, and joined the deadlock cascade. Production builds set `panic = "abort"` for `dev` / `release` (process aborts before any of this matters), but `cargo test` always builds with `panic = "unwind"` per the test-harness contract — so a test exercising the failure path would hang rather than fail with a diagnostic message. **Fix:** replaced `Mutex>>` with `Mutex` (`Pending` / `Published(Arc)` / `LeaderAborted`); added `LeaderGuard<'cs>` that owns the leader's slot Arc + a borrow of the in-flight mutex + a `success: bool` flag, with `Drop` that always removes the in-flight entry (cleanup-on-publish + cleanup-on-panic in one path) and conditionally broadcasts `LeaderAborted` via `publish_aborted_if_pending` when `mark_success` was never called; `wait_for_result` now panics with a diagnostic message on `LeaderAborted` instead of looping on the condvar. The `lookup_or_derive` leader branch wraps its `PreparedCache::derive` + `slot.publish` + `transient.write` sequence in the guard's scope with a `mark_success` flag 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-13 `cachestore_leader_abort_wakes_followers_and_cleans_in_flight`:** white-box test using a standalone `Mutex>>` mock so the test runs without paying the ~150–200 ms `PreparedCache::derive` cost; spawns a follower thread on `slot.wait_for_result()`, drops a `LeaderGuard` without `mark_success`, and asserts (1) in-flight entry removed; (2) slot in `LeaderAborted`; (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 against `7b5302ee9` and 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 per `21-reversion-clause-discipline.mdc`'s post-closure-pin discipline. - **NF1 — `PATTERN_FFI_EXPORT` blind spot in [`scripts/ci/check_randomx_crate_invariants.sh`](../scripts/ci/check_randomx_crate_invariants.sh).** The Round 3 §3.6 R1-E1 pattern C `extern "C" fn` arm anchored `extern` as the first non-whitespace token; `pub extern "C" fn`, `pub(crate) extern "C" fn`, `unsafe extern "C" fn`, and `pub unsafe extern "C" fn` all 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 (add `pub extern "C" fn` first, attach `#[no_mangle]` later) fires the gate at the first commit rather than only the second. **Fix:** extend the regex to allow optional `pub` / `pub(crate)` / `pub(super)` / `pub(in path)` visibility prefix and optional `unsafe` keyword before `extern`, 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::lookup` linearizability race on transient→canonical promotion** in [`rust/shekyl-pow-randomx/src/cache_store.rs`](../rust/shekyl-pow-randomx/src/cache_store.rs). The Round 2 §3.1 per-slot `RwLock>>` shape was specified at the architecture level; the implementation acquired and released each slot's read guard sequentially across the comparison sequence. A concurrent `set_canonical` could promote an entry from transient to canonical between `lookup`'s two slot inspections, causing `lookup(&S)` to observe canonical=Some(prior) → released → transient=Some(prior_canonical) → return `None` despite the requested entry being live in the canonical slot the entire time. Soft consequence: a `lookup_or_derive` consumer 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 matches `set_canonical`'s canonical-write-then-transient- write order, so there is no deadlock cycle. Updated `lookup` rustdoc with explicit linearizability + lock-ordering discussion; updated the `CacheStore` struct's `# Synchronization shape` rustdoc 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-12 `cachestore_lookup_linearizable_under_canonical_swap`:** seeds the store with two pre-derived prepared caches in distinct slots, runs alternating `set_canonical(p_a) / set_canonical(p_b)` calls in one thread while a second thread tightly polls `lookup(&seedhash_a) / lookup(&seedhash_b)` for 2,000 iterations and asserts both never return `None` (both entries are live in *some* slot at every observable moment, so a linearizable `lookup` must always find them). With the buggy implementation the test fails probabilistically; with the fix it passes deterministically because a concurrent `set_canonical` cannot 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.mdc` audit-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-lines` target flagged as superseded.** Round 2's `Seedhash` newtype + `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 commit `cb9dc0dd2`'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. - **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`](completed/RANDOMX_V2_PHASE2F_PLAN.md)). Six narrow refinements, each tightening a post-closure pin against a substrate observation. Per `21-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 on `PreparedCache`.** The original post-closure-pin disposition ("no accessor; `compute_hash` private-field-extracts internally") is replaced. The explicit accessor: ```rust impl PreparedCache { pub(crate) fn cache_ref(&self) -> &Cache; } ``` documents the established reach-through shape from `&PreparedCache` to `&Cache` for the dispatch loop's in-crate consumption, and prevents a future contributor from re-exposing `Cache`'s API on `PreparedCache` (e.g., adding `prepared.derive_item(...)` as a convenience). Per `05-system-thinking.mdc`'s "specification first, code second" discipline, the explicit accessor is the documented contract. Tests continue to use `pub(crate) Cache::from_raw` per 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.mdc` audit-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 ." 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 + `TraceSink` scope.** 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. **`TraceSink` trait 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 promote `TraceSink` to a public surface — a `pub trait TraceSink` exposed 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 explicit `pub(crate) fn cache_ref(&self) -> &Cache` accessor); 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 of [`docs/completed/RANDOMX_V2_PHASE2F_PLAN.md`](completed/RANDOMX_V2_PHASE2F_PLAN.md). Per `21-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 `Display` impl 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 seedhash` across `rust/shekyl-pow-randomx/src/`) found zero `tracing::` / `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 (matches `hex::encode` and the cryptographic-output convention); the framing is corrected to cite the convention directly rather than the unsupported "Phase 2c consistency" claim. The `Display` impl is for downstream consumers (FFI shim, daemon-side logging, test diagnostics) — the verifier crate itself does not log. **(2) §1.1 dispatch-loop / `Cache` visibility pin.** The Round 2 `Cache: pub → pub(crate)` transition does not affect the dispatch loop's `vm.rs::execute_one` signature (in-crate `cache: &Cache` — the visibility transition is crate-boundary-only). `compute_hash` extracts `&prepared.cache` via private-field access internally (both in the same crate); no `cache_ref()` accessor on `PreparedCache` is added. Tests that need direct `Cache` construction continue to use `pub(crate) Cache::from_raw` per the Phase 2c R0-D6 tests-use-the-actual-API discipline. **(3) §1.1 `PreparedCache` equality pin.** `PreparedCache` does not derive `PartialEq` / `Eq`. Two equality semantics are needed; each is served by a more specific primitive than `PartialEq` on `PreparedCache`: seedhash equality (`slot.seedhash() == lookup_key` via `Seedhash`'s derived `PartialEq`) for CacheStore slot indexing, and `Arc::ptr_eq` for identity comparisons in tests T-CS-5/7/9. Deriving `PartialEq` would either be structural value-equality (compare 256 MiB cache bytes; no caller wants this) or delegating equality (compare seedhash only; conflates "same seedhash" with "same `PreparedCache` instance"). 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`, `CacheStore`); the Phase 3a FFI shim owns the C-side opaque-handle shape, `Seedhash::from_bytes(*ptr)` construction at the boundary, `Arc` lifecycle across the boundary (daemon-facing API discourages long-lived holds per the caller hand-off Arc-lifetime discipline), and `VmStatePool::new(capacity)` runtime-parameter derivation from `dev`-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_trace` pre-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 for [`docs/completed/RANDOMX_V2_PHASE2F_PLAN.md`](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-off` always; `B-pool-on` when 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 a `pub(crate) const` at 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 if `cargo fmt --check` is 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 by `RwLock`-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 the `CacheStore` rustdoc (consumers should hold `Arc` only for the duration of the immediate hash computation; long-lived holds extend cache memory residency beyond `CacheStore`'s bound; daemon-side discipline, not a `CacheStore` enforcement). **§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** to `B-pool-off` / `B-pool-on` A/B per §3.3 Round 3; component-floor benches retained as cross-check; `BENCH_RESULTS.md` records the A/B delta + Branch disposition. **§8 commit table reshaped** to 6-commit Round-2-+-Round-3 shape: `Seedhash` + `PreparedCache` type 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 at `CacheStore` rustdoc** — 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 to `26-sub-pr-design-discipline.mdc` as 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 for [`docs/completed/RANDOMX_V2_PHASE2F_PLAN.md`](completed/RANDOMX_V2_PHASE2F_PLAN.md) Round 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. Derives `Copy / Clone / Debug / Eq / Hash / PartialEq` plus a `Display` impl (hex). Representation is private (accessor- mediated `from_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 PreparedCache` bundling `Cache + Seedhash`. The bundle is constructed via `pub fn PreparedCache::derive(Seedhash) -> PreparedCache`; `pub fn PreparedCache::seedhash(&self) -> &Seedhash`. Wrong-cache-for-seedhash is unrepresentable: there is no public path to construct a `PreparedCache` whose `cache` wasn't derived from its `seedhash`. - `compute_hash` signature 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 opaque `PreparedCache*` and never sees a seedhash on the compute path. - `Cache` transitions `pub → pub(crate)`. The `pub(crate)` `Cache::derive(&Seedhash) -> Cache` is the implementation primitive but not the public API. Test access is preserved via `src/*.rs#mod tests` discipline (Phase 2c R0-D6); `Cache` rustdoc carries a pointer to `PreparedCache` as the public construction path. - **Two-layer derivation discipline.** `Cache::derive(&Seedhash) -> Cache` is the pure transform (testable in isolation, preserves Phase 2c's T1 spec-vector test infrastructure); `PreparedCache::derive(Seedhash) -> PreparedCache` is the bundling wrapper (three lines of body; calls `Cache::derive` and pairs the result with the seedhash). Two-layer chosen over one-layer for test-infrastructure continuity (T1 spec vectors assert `Cache::derive` against canonical reference output; reusing those tests against the internal `Cache::derive` is cheaper than rewiring them through `PreparedCache.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` / `Seedhash` in 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) `PreparedCache` bundle [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 on `Arc`]; (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`. 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 many `Arc` 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. - **Frozen `CacheStore` public surface (Round 2 supersedes Round 1's code-block).** `pub fn new() -> CacheStore`, `pub fn lookup(&self, seedhash: &Seedhash) -> Option>`, `pub fn lookup_or_derive(&self, seedhash: &Seedhash) -> Arc`, `pub fn set_canonical(&self, prepared: Arc)`. The Round 1 `insert` method is **removed**; its function (publish a derived cache into the transient slot) is subsumed by `lookup_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 call `lookup` and treat `None` as 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>>` inside `CacheStore`. Concurrent `lookup_or_derive` calls 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-flight `HashMap` grows unboundedly under sustained novel-seedhash attack). Closes the thundering-herd attack surface that applies regardless of Axis 2/3 selection. The `Shared` is the `futures::future::Shared` adapter (or a sync alternative built on `std::sync::Arc>` + condvar; the choice is a Round 3 sub-detail per dependency discipline). - **Synchronization shape pinned at Round 2.** Per-slot `RwLock>>` 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` for in-flight (writes/reads balanced; short critical section; `RwLock` would 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 surfaces `RwLock`-not-helping or `Mutex`-contention. - **11-row state-transition table refreshed.** Pre/post states typed against `Arc` (rather than `(seedhash, Arc)` pairs); `insert→lookup_or_derive` substitution; 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 `CacheStore` lands 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 the `CacheStore`." Substrate-anchored event = consumer's call-site grep evidence + measurement showing the cost. - **Transparent-memo framing retired.** Parent-plan `RANDOMX_V2_PLAN.md` Decision #6 wording ("transparent memo with capacity-2 LRU and `pin()` API") belonged to the rejected Option (a). Option (b) is honest about the two-slot structure. Wording amendment queued as **precursor PR** `chore/randomx-v2-plan-decision6-amendment` that 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` 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-frozen `compute_hash(&Cache, &[u8; 32], &[u8])`). Phase 3a's FFI shim constructs `Seedhash::from_bytes(*ptr)` from the C-ABI's `*const [u8; 32]` and passes `Arc` 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 `PreparedCache` shape + §10.1 precursor PR queue + §10.2 PQC migration space note; §11 Round history gains Round 2 row. - **RandomX v2 Track A Phase 2f — Round 1 design closure** (`chore/randomx-v2-phase2f-plan`, 2026-05-23). Closes Round 1 of [`docs/completed/RANDOMX_V2_PHASE2F_PLAN.md`](completed/RANDOMX_V2_PHASE2F_PLAN.md) six 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 routine `lookup` ordering; 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 via `std::sync::Mutex` only — `lru` is not a workspace dependency (verified at `rust/Cargo.toml`), `parking_lot` is transitive-only via the existing `criterion` / `tokio` paths, and a 2-slot store does not justify pulling either into the direct dep set per `17-dependency-discipline.mdc`. Frozen API code-block pinned in `RANDOMX_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_canonical` advance promotes-from- transient + demotes-prior. Cold-start window (no `set_canonical` yet called) leaves both slots subject to attacker churn; bounded to daemon startup and handled by the FFI shim's discipline (no fallback policy in `CacheStore` itself). 11-row pre/post state- transition table covering `RANDOMX_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 `VmState` isolation):** picks option (b) Component method. Rejects (a) Diff method because the natural amortization shape requires either promoting `VmState` to `pub` or adding a `pub fn compute_hash_with_state` helper — both contradict `RANDOMX_V2_PHASE2F_PLAN.md` §1.1 (`VmState` is `pub(crate)`) and Decision #7 (no public `VmPool`). 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 production `VmState` newtype 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.md` line 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 inside `compute_hash`, no public `VmPool`, 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.mdc` against `src/cryptonote_core/blockchain.cpp`, `src/cryptonote_core/tx_pool.cpp`, `src/cryptonote_core/cryptonote_tx_utils.cpp`, `src/cryptonote_core/tx_pqc_verify.cpp`, and `src/common/threadpool.{h,cpp}` at `dev` tip = `fb21909ff`. Substrate correction vs. prompt: only **one** parallel `compute_hash` call site exists at HEAD — alt-chain branch validation's `block_longhash_worker` via `tools::threadpool::getInstanceForCompute()`, capped by `m_max_prepare_blocks_threads` (default 4). Mempool tx verification does **not** call `compute_hash` in parallel; the prompt's two-source assumption was incorrect. Pool capacity formula: `min(threadpool::getInstanceForCompute().get_max_concurrency(), m_max_prepare_blocks_threads) + 1` reserve. Reversion criteria: a future PR introduces `tools::threadpool` + `compute_hash` in `tx_pool.cpp` / `cryptonote_tx_utils.cpp` / `tx_pqc_verify.cpp`; `m_max_prepare_blocks_threads` default 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-0 `static` declarations (function-local statics are inside fn bodies and indented per rustfmt; `const` items are a different keyword and not matched). Pattern C bans `#[no_mangle]`, `#[unsafe(no_mangle)]`, `#[export_name`, `#[unsafe(export_name`, and `extern "C" fn` definition form (`extern "C" { fn foo(); }` import blocks consuming external FFI surfaces are not matched since they require `fn` *inside* the brace block, not after `"C"`). New script `scripts/ci/check_randomx_crate_invariants.sh` modeled on `scripts/ci/check_randomx_fpu_rounding.sh` from Phase 2d. CI integration: new `.github/workflows/build.yml` step `enforce RandomX crate-level isolation invariants` sibling to the FPU step. **Substrate finding at verification:** pattern C's first draft (anywhere-on-line match) collided with the existing `lib.rs` rustdoc 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 across `rust/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-impl` after 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 per `06-branching.mdc` — chore branch is short-lived; commit not pushed pending user authorization. - **RandomX v2 Track A Phase 2d implementation core landed** (`feat/randomx-v2-phase2d`, 2026-05-22). Implements [`docs/completed/RANDOMX_V2_PHASE2D_PLAN.md`](completed/RANDOMX_V2_PHASE2D_PLAN.md) §3.5/§3.7 R1–R6 decisions on [`rust/shekyl-pow-randomx`](../rust/shekyl-pow-randomx): - **F128 newtype + integer helpers** (`bd7cea464`). Promotes the Phase 2c `type F128 = [f64; 2]` alias to a `#[derive(Copy)]` `struct F128([f64; 2])` carrying `add/sub/mul/div/sqrt/swap` + FSCAL XOR-mask methods per §3.2 R1-D2. Adds private `sign_extend_i32_to_i64`, `load64`/`store64`, `rotr`/`rotl` helpers matching `instructions_portable.cpp` semantics. Quarantines the FPU rounding-mode write in a new [`fpu_rounding`](../rust/shekyl-pow-randomx/src/fpu_rounding.rs) module (x86_64 `_mm_setcsr`, aarch64 `mrs/msr fpcr` inline asm) per §3.1 R1-D1 / §3.7 R6-D1. Replaces the Phase 2c stub-NOP `dispatch_instruction` body with a dense `match` on `decode_instruction_type(opcode)` covering all 28 executable opcodes plus CFROUND + CBRANCH, driven by a PC loop with `Program.cbranch_table` static metadata populated during `init_program` (§3.6 R2-D1/R2-D2/R2-D3). Promotes `superscalar::{mulh, smulh_u64, randomx_reciprocal}` to `pub(crate)` for IMULH/IMUL_RCP dispatch. - **T16 real-dispatch hash vector + CI grep + FPU reset** (`26fc49d6c`). Adds [`tests/vectors/reference/vm/t16_vm_compute_hash_real.bin`](../rust/shekyl-pow-randomx/tests/vectors/reference/vm/t16_vm_compute_hash_real.bin) emitted by the pinned fork's interpreted-light VM under `RANDOMX_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. Adds [`scripts/ci/check_randomx_fpu_rounding.sh`](../scripts/ci/check_randomx_fpu_rounding.sh) wired into the existing Lint job to enforce the §9 FPU primitive scope (`_mm_setcsr` exactly once on x86_64, two `asm!(` calls on aarch64, no `fesetround`). Restores FPU rounding mode to round-to-nearest at `compute_hash` entry/exit and around the `execute_program` determinism tests so the process-wide MXCSR/FPCR mutation from CFROUND does not leak across tests. Phase 2d post-dispatch `compute_hash_alloc::per_call` measures 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/`](../rust/shekyl-pow-randomx/tests/vectors/reference/_generator/phase2d/) generator (gen.cpp + Makefile + README) driving the pinned fork's `randomx::BytecodeMachine::compileInstruction` + `executeInstruction` against fabricated single instructions over a canonical `NativeRegisterFile` + scratchpad fixture, and emits seven new `.bin` + `.meta.txt` reference vectors under [`tests/vectors/reference/vm/`](../rust/shekyl-pow-randomx/tests/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 with `rx_get_rounding_mode()` u32). Rust spec-vector tests in [`src/vm.rs#mod tests`](../rust/shekyl-pow-randomx/src/vm.rs) drive `dispatch_instruction` against 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 the `executeInstruction` dispatch surface against the v2 fork pin `aaafe71` byte-for-byte. - **Phase 2d post-gate fmt cleanup** (`4fc0606d1`). Six mechanical `cargo fmt --check` divergences accumulated across the four Phase 2d substantive commits surfaced together when the §9 Format gate re-ran post-T9-T15 land: four in `dispatch_instruction`'s integer arms (IAddRs / IMulRcp / IRorR / IRolR) from `bd7cea464`, plus `CANONICAL_E_MASK_PD` from `043076f18`. Addressed in a single fmt-only commit per `15-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 in [`docs/completed/RANDOMX_V2_PHASE2D_PLAN.md`](completed/RANDOMX_V2_PHASE2D_PLAN.md) §11's Implementation row with SHA → §8 mapping and §9 gate confirmation at HEAD `4fc0606d1`. - **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`](completed/RANDOMX_V2_PHASE2C_PLAN.md) / [`RANDOMX_V2_PHASE2C_AUDIT.md`](completed/RANDOMX_V2_PHASE2C_AUDIT.md) into [`.cursor/rules/26-sub-pr-design-discipline.mdc`](../.cursor/rules/26-sub-pr-design-discipline.mdc) (Option A; opt-in — cite when scoping multi-round per-trait PRs). Closes `docs/FOLLOWUPS.md` V3.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.md`](completed/RANDOMX_V2_PHASE2D_PLAN.md) through Round 6 after PR #66 on `dev` (`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 asm `mrs/msr fpcr` write — with substrate justification (no stable `core::arch::aarch64` FPCR-write intrinsic exists); **(R6-D2)** out-of-range opcode disposition changes from R1-D3's `debug_assert!`/no-op pair to `panic!` 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_pc` invariant- documentation note + sentinel reset for implementation-PR rustdoc (R6-D5). Implementation authorized on `feat/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 per [`docs/design/RANDOMX_V2_PLAN.md`](design/RANDOMX_V2_PLAN.md) §"Track A — Phase 2" and the design plan [`docs/completed/RANDOMX_V2_PHASE2C_PLAN.md`](completed/RANDOMX_V2_PHASE2C_PLAN.md). Eight-commit stack landing the cache + VM substrate end-to-end with byte-for-byte parity against the `randomx-v2` fork at pin `aaafe71` (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 — `Cache` type skeleton + size constants + `Drop`** (`39eda3164`). [`src/cache.rs`](../rust/shekyl-pow-randomx/src/cache.rs) `pub Cache` struct + `CACHE_SIZE` / `DATASET_ITEM_SIZE` / `DATASET_ITEM_COUNT` constants + empty `Drop` (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's `pub(crate) fill_cache`) + 8 × `Blake2Generator`-seeded `generateSuperscalar` programs (delegating to Phase 2b's `Blake2Generator` + `generateSuperscalar` from `src/superscalar.rs`) + `RANDOMX_CACHE_ACCESSES` constant + the cache-memory allocation unsafe carve-out (the only `#![deny(unsafe_code)]` exception this commit introduces, per the §1 covenant 7 enumeration) + cache-site `debug_assert!`s per §5.11.2. T1' (`Cache::derive` determinism property test, ~100 invocations) + the `programs` field landing on `Cache`. Plan-doc errata `86f058c3b`/`431a54b38`/`3e6bb2734` (impl-time pre-flight R0-D5/R0-D6/R0-D7: drop `Cache::from_raw`, relocate T1-T8 to unit tests, withdraw `randomx_reciprocal` `pub(crate)` promotion). - **Commit 3 — `Cache::derive_item` + `item_bytes` + T2' invariance** (`9ab584596`). `pub(crate) Cache::derive_item` (the per-iteration dataset-item read consumed by `VmState`'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)]` on `superscalar::execute_superscalar`. - **Commit 4 — `VmState` skeleton + scratchpad alloc + `Drop`** (`c63555a5e`, with `186a8cfdf` fix-up for the `PROGRAM_SIZE`/`PROGRAM_ITERATIONS` distinction caught at R0-D9 pre-flight). [`src/vm.rs`](../rust/shekyl-pow-randomx/src/vm.rs) `pub(crate) VmState` skeleton with the frozen §5.1.1 field set (per §5.5 F5 v2-only simplification), `pub(crate)` type definitions (`F128`, `Instruction`, `Program`), the `PROGRAM_SIZE` (384) / `PROGRAM_ITERATIONS` (2048) / `RANDOMX_SCRATCHPAD_L3` (2 MiB) spec constants, the `alloc_zeroed_scratchpad` carve-out (the second and final `#![deny(unsafe_code)]` exception this PR introduces per §1 covenant 7), the scratchpad-allocation `debug_assert!` per §5.11.2, the empty `Drop` (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_scratchpad` via `crate::aes::fill_aes_1r_x4`; `VmState::init_program` (stack- allocate the 3 200-byte program buffer per spec §4.5's `128 + 8 × PROGRAM_SIZE` budget, fill via `crate::aes::fill_aes_4r_x4`, parse `entropy[0..128]` into the register-init field set, parse `instructions[128..3200]` into `self.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`). - **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-NOP `dispatch_instruction` dispatches into per spec §4.6.5) + the private `dispatch_instruction` NOP-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 at [`tests/vectors/reference/_generator/phase2c/`](../rust/shekyl-pow-randomx/tests/vectors/reference/_generator/phase2c/) (Makefile + `gen.cpp` + README + `.gitignore`) compiled against the vendored fork at pin `aaafe71`. Eight reference vectors pre-computed and committed under [`tests/vectors/reference/cache/`](../rust/shekyl-pow-randomx/tests/vectors/reference/cache/) (T1: cache fingerprint Blake2b-256 over the entire derived cache + the 8 superscalar programs; T2: 8-item dataset batch) and [`tests/vectors/reference/vm/`](../rust/shekyl-pow-randomx/tests/vectors/reference/vm/) (T3: scratchpad init; T4: register init from entropy; T5: program parse from entropy; T6: `spAddr0`/`spAddr1` snapshot across 4 stub-NOP iterations; T7: post-AES-mix register snapshot across 4 stub-NOP iterations; T8: end-to-end `compute_hash` output under stub-NOP dispatch). Ten Rust spec- vector tests (T1-T8) inline in `src/cache.rs` and `src/vm.rs` pass byte-equality against the committed fixtures. Refactor: `VmState::execute_program` split into `execute_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** per [`16-architectural-inheritance.mdc`](../.cursor/rules/16-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 at `bytecode_machine.hpp:40`. The Rust port fuses `reg` + `nreg` into a single `VmState.r` per `30-cryptography.mdc` secret-locality framing; the per-chain reset that fell out of C's two-struct shape is re-asserted explicitly as `self.r = [0; 8];` at the top of `VmState::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::imm32` storage divergence (cross- language-port-storage-divergence discipline).** The C reference's `initCache` (`dataset.cpp:131-138`) post-processes the 8 `SuperscalarProgram`s after `generateSuperscalar` by replacing each `IMUL_RCP` instruction's `imm32` in-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 original `imm32` and computes the reciprocal on-the-fly in `execute_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 the `derive_item` output, not the program serialization) didn't. Resolution lives in the C++ generator (`emit_t1` re-runs `Blake2Generator` + `generateSuperscalar` directly rather than reading the C reference's `cache->programs` to 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 same `IMUL_RCP` arm at execution time via Rust's on-the-fly reciprocal calculation). Disposition under §14 Round 0 R0-D11. - **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`](../rust/shekyl-pow-randomx/benches/cache_derive.rs) + [`benches/compute_hash_alloc.rs`](../rust/shekyl-pow-randomx/benches/compute_hash_alloc.rs) landing the §5.8 PR-gate baseline measurement infrastructure. `tests/perf/per_hash_latency.rs` placeholder (`#[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 per [`21-reversion-clause-discipline.mdc`](../.cursor/rules/21-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.yml`](../.github/workflows/build.yml) Gate 2: `cargo test --release -p shekyl-pow-randomx` per §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 in [`src/lib.rs`](../rust/shekyl-pow-randomx/src/lib.rs) per §5.11.4 R4 (public-input-only scope with substrate-anchored reopening criterion). Baseline measurements recorded in [`BENCH_RESULTS.md`](../rust/shekyl-pow-randomx/BENCH_RESULTS.md) (i9-11950H, Debian 13, kernel 6.12.88): `Cache::derive` median 341.45 ms; `compute_hash` end-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 in `BENCH_RESULTS.md` §"Threshold reconciliation" with the diagnosis (single-thread Argon2d on this hardware class is fundamentally a ~300 ms operation; the `compute_hash_alloc` budget framing was internally inconsistent with what the bench measures) and named reopening criteria per `21-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. Phase 2c retains the Phase 2a/2b forward-compatibility posture: no `#[no_mangle]`, no `extern "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_ALLOC` and `vm::alloc_zeroed_scratchpad`). `cargo test -p shekyl-pow-randomx` succeeds without `external/randomx-v2/` initialized and without a C++ toolchain — the spec-vector reproducibility check (`make vectors` in 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 2c `dispatch_instruction` NOP stub via function-body replacement (no trait wiring, no impl swap, no signature change to `compute_hash` per §5.1.1's frozen surfaces 1-3); Phase 2f wraps `Cache::derive` in a `CacheStore` LRU + `VmState` pool; Phase 2g lands the C-side differential harness as a separate test-only artifact; Phase 3 then exposes the verifier through `shekyl-ffi` and rewires the C++ daemon to it; Phase 4 deletes the C++ verifier path. ### 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-body `dispatch_instruction`), the Phase 2d skeleton scaffold (function-body replacement of `dispatch_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::derive` moves from originally-scoped Phase 2e into 2c) lands as a parent-plan precursor commit. Per [`RANDOMX_V2_PHASE2C_PLAN.md`](completed/RANDOMX_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) `BytecodeDispatch` trait plus `StubNopDispatch` impl → `dispatch_instruction` free function with NOP body replaced in 2d, eliminating the mock-X anti-pattern; (R2-D2) `Vm<'a>` public type → `compute_hash` public transform with `VmState` private (module layout collapses 5 files → 2); (R2-D3) `Cache::from_raw` visibility correction (`pub` → `pub(crate)`; test-time only, not FFI surface). Parent-plan alignment commit follows (Decision #7 substrate-shift: `VmState` pooling becomes internal to `compute_hash`, not a public `VmPool` type). - **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_instruction` signature, frozen `Instruction` field set, and `VmState` field set populated empirically from an audit against `bytecode_machine.hpp`'s 29 opcode handlers + `vm_interpreted.cpp::execute()`. Audit produced one correction-from-prompted-list finding: `mp` is a v2-only local-variable alias for `mem.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 lands [`RANDOMX_V2_PHASE2D_PLAN.md`](completed/RANDOMX_V2_PHASE2D_PLAN.md) skeleton 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`](../.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::derive` determinism) + T2' (`derive_item` invariance) 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 unsafe `Box::new_zeroed_slice` sites (~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_LARGE` taxonomy + `RANDOMX_BLOCK_TEMPLATE_MAX_SIZE` pinned at 2 MiB; 2f CacheStore canonical-seedhash slot eviction-protection + `VmState` pool capacity sized against daemon parallel-verification fanout. **Discipline note:** §5.11.8 audit-against-actual-code validation (the discipline that produced R3-D1's `mp` correction 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`](completed/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-D1 `mp` correction 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`](../.cursor/rules/16-architectural-inheritance.mdc)'s "audits-are-clean-so-compress" anti-pattern names). - **(R5-D2)** [`RANDOMX_V2_PLAN.md`](design/RANDOMX_V2_PLAN.md) Phase 0 §5 FFI hardening refinements: C-side header form `const uint8_t (*seedhash)[32]` (not decayed `const 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_SIZE` rationale-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`](../.cursor/rules/21-reversion-clause-discipline.mdc)). - **(R5-D3)** [`RANDOMX_V2_PHASE2D_PLAN.md`](completed/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 the `RANDOMX_V2_PLAN.md` §7.7 **`shekyl-pow-randomx` never 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.md`](FOLLOWUPS.md) V3.0 entry: sibling PR (opens post-PR-#65 merge to `dev`; 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.mdc` with substantial prose, similar shape to `16-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.mdc`](../.cursor/rules/06-branching.mdc) rule 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`](../.cursor/rules/15-deletion-and-debt.mdc) "while we're here is the enemy." - **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`](completed/RANDOMX_V2_PHASE2C_PLAN.md) (new; Rounds 1–5); [`docs/completed/RANDOMX_V2_PHASE2D_PLAN.md`](completed/RANDOMX_V2_PHASE2D_PLAN.md) (new; Scaffold + Scaffold-R4 + Scaffold-R5); [`docs/design/RANDOMX_V2_PLAN.md`](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`](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 production `argon2d.rs` primitive 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`, and `m64_t3_p1_shekyl_test_key.meta.txt` all asserted that the `argon2_ref.c` reference Argon2 implementation "keeps the produced bytes architecture-independent." That is wrong on endianness: `gen.c`'s `write_raw` does a raw `fwrite` of `block { uint64_t v[128] }` memory, which serializes each `uint64_t` in the host's native byte order. The committed `.bin` files 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`'s `blocks_to_le_bytes` produces in Rust via `u64::to_le_bytes` (per `argon2d.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 a `htole64`-style serialization step in `write_raw`, not to redefine the on-disk format. Important asymmetry preserved: the **Rust** test side is already architecturally portable — `blocks_to_le_bytes` pins the LE convention on both sides regardless of `Block`'s in-memory layout, so the `cargo test` path 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.md` provenance check command is broken (1 site).** The "Reviewing the vectors" section documented `diff -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, so `diff -r` always reports "only in" entries instead of the intended check (do the regenerated `.bin` files match the committed bytes?). Replaced with `git diff --stat -- ../*.bin` issued from inside `_generator/`. `make vectors` overwrites the committed bytes in-place; `git diff --stat` then asks git whether the working tree has drifted from `HEAD` on the specific `.bin` paths. A clean exit (no output) is the affirmative attestation that the committed bytes match the named fork pin. Added a paragraph explaining why the prior `diff -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`, and `cargo doc -p shekyl-pow-randomx --no-deps` all clean. The Phase 2f forward-compatibility greps from `RANDOMX_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.mdc` scope-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 (add `htole64` portability shims to `gen.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 per [`16-architectural-inheritance.mdc`](../.cursor/rules/16-architectural-inheritance.mdc). The big-endian-host disposition is recorded in `gen.c`'s comment so a future maintainer who needs it knows the prescribed shape. - **Post-PR-4 `docs/FOLLOWUPS.md` cleanup** (`chore/post-pr4-followups-cleanup`, 2026-05-21). Two scope-respecting doc-only commits closing the cleanup work that became actionable once PR 4 (commit `fd6005e2a`, 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 under [`16-architectural-inheritance.mdc`](../.cursor/rules/16-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.md`](FOLLOWUPS.md) gains 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 to `dev` YYYY-MM-DD at SHA)" parenthetical the section uses, plus the dev-merge SHA `fd6005e2a` added for traceability: 1. "Stage 1 retroactive Mock-X cleanup: `MockLedger` → `LocalLedger::from_test_blocks(...)` + `FaultInjecting`" (was V3.0 queue L697; PR 4 §7.X commit C6β), 2. "Stage 1 retroactive Mock-X cleanup: `MockDaemon` → `TestDaemon` rename" (was V3.0 queue L744; PR 4 §7.X commit C6γ), 3. "Stage 1 PR 4 Phase 0d — `RefreshEngine` checkpoint 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 (referenced `ReorgAmplificationDetector` as "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). - **Commit 2 — Sharpen PR 3 engine-property test re-location entry** (`95ece3760`): [`docs/FOLLOWUPS.md`](FOLLOWUPS.md) V3.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: 1. 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 unified `KeyEngine` / `LedgerEngine` / `DaemonEngine` `pub(crate) → pub` visibility-promotion bundle explicitly, with an inline "unilateral `KeyEngine` widening 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-trait `pub(crate)` lock prevents per Stage 1's Trust-class A classification (PR 3 §2.1 trust-class table row for `KeyEngine`). 2. M3b D5 peer-test name removed (the `..._subaddress` peer 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 to `STAGE_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. *Gates.* Doc-only changes; no code touched. `git diff` shape 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 `fd6005e2a` will be cited at each focused PR's open date per `21-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 per [`docs/design/RANDOMX_V2_PLAN.md`](design/RANDOMX_V2_PLAN.md) §"Track A — Phase 2" and the design plan [`docs/completed/RANDOMX_V2_PHASE2B_PLAN.md`](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 into `superscalar.rs` between gate-runs) landing the remaining v2 primitives the verifier needs: - **Commit 1 — AES round primitives + Blake2Generator + MSRV bump.** `cipher_round` / `equiv_inv_cipher_round` at [`src/aes.rs`](../rust/shekyl-pow-randomx/src/aes.rs) wrapping `aes-0.9.0::hazmat` (`_mm_aesenc_si128` / `_mm_aesdec_si128` equivalent-inverse semantics, matching RandomX `soft_aesenc` / `soft_aesdec`). `Blake2Generator` PRNG at [`src/blake2_generator.rs`](../rust/shekyl-pow-randomx/src/blake2_generator.rs) per spec §3.5. Workspace `rust-version = "1.85"` pin for `aes-0.9.0`'s edition-2024 MSRV, verified against the Guix substrate before pinning. F1 convergence on `src/argon2d.rs`'s `#[allow(dead_code)]` from module-level to per-item attributes. - **Commit 2 — AES composites.** `AesGenerator1R` (state writeback), `AesGenerator4R` (no writeback), `AesHash1R` per spec §3.2–3.4. Initial state, generator keys, and extra- round keys ported as `const [u8; 16]` arrays via `pack_le_u32x4` reproducing `_mm_set_epi32(i3, i2, i1, i0)`'s little-endian memory layout exactly. - **Commit 3 — SuperscalarHash generator + executor.** [`src/superscalar.rs`](../rust/shekyl-pow-randomx/src/superscalar.rs) implementing 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/`](../rust/shekyl-pow-randomx/tests/vectors/reference/aes/) covering round primitives, the F6 chained-pair multi-round supplement, `AesGenerator1R` / `4R` outputs, and `AesHash1R` on uniform + empty inputs. Reviewer-runnable C++ generator at `_generator/` instantiates `` templates to keep emitted bytes SIMD-codegen-independent. - **Commit 5 — SuperscalarHash spec-vector parity tests.** 7 reference vectors at [`tests/vectors/reference/superscalar/`](../rust/shekyl-pow-randomx/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 fixed `r=[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 in `serialize_program` / `deserialize_program` helpers. - **Commit 6 — CHANGELOG + FOLLOWUPS entry** (this commit). F7 AES symbol-surface handoff to V3.0 / Phase 3c recorded at [`docs/FOLLOWUPS.md`](FOLLOWUPS.md); the live runnable check (`cargo build --release && nm shekyld | grep -iE '(aes|randomx)'`) and its expected disposition (no `randomx_*` matches per `RANDOMX_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]`, no `extern "C" fn`, no `#[export_name]`, no module-level runtime-mutable state. `#![deny(unsafe_code)]` at the crate level. `cargo test -p shekyl-pow-randomx` succeeds without `external/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 deliver `Vm`, bytecode dispatch, and `Cache::derive`; Phase 3 then wires the verifier through `shekyl-ffi`; Phase 4 deletes the C++ verifier path. - **RandomX v2 Track A Phase 2a — `shekyl-pow-randomx` crate scaffold + Argon2d primitive** (`feat/randomx-v2-phase2a`, 2026-05-21). First sub-PR of the Rust pure-software RandomX v2 verifier port per [`docs/design/RANDOMX_V2_PLAN.md`](design/RANDOMX_V2_PLAN.md) §"Track A — Phase 2" and [`docs/design/RANDOMX_V2_RUST.md`](design/RANDOMX_V2_RUST.md). - New workspace crate [`rust/shekyl-pow-randomx/`](../rust/shekyl-pow-randomx/) with crate-level rustdoc citing the Phase 0 decision substrate (spec-first per `RANDOMX_V2_RUST.md` §3; derived-first per §4; isolation invariants per §7). - `pub(crate) fn fill_cache(key: &[u8], blocks: &mut [argon2::Block])` at [`src/argon2d.rs`](../rust/shekyl-pow-randomx/src/argon2d.rs) implementing the Cache Argon2d "memory fill" per `external/randomx-v2/doc/specs.md` §7.1 + Table 7.1.1 (`parallelism = 1`, `memory = 262144` KiB = 256 MiB, `iterations = 3`, `Argon2d`, `salt = "RandomX\x03"`). Built on `argon2 = "0.5.3"`'s `Argon2::fill_memory` after verifying at source that the omit-finalizer path matches RandomX's spec-required surface (recorded in the module rustdoc). Constants and `Params` are compile-time validated. - Argon2d spec-vector parity tests at [`tests/vectors/reference/argon2d/`](../rust/shekyl-pow-randomx/tests/vectors/reference/argon2d/): two derived vectors (`m=8` boundary case; `m=64` multi-segment) from `argon2_ref.c` at fork pin `aaafe71` (v2.0.1), with per-file `.meta.txt` provenance headers and a reviewer-runnable `_generator/` reproducer (`gen.c`, `Makefile`, `README.md`). The Rust tests consume pre-committed bytes via `include_bytes!`; no `cargo test` dev-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]`, no `extern "C" fn`, no `#[export_name]`, no module-level runtime-mutable state (no `Mutex`/`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. - **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.md`](FOLLOWUPS.md) V3.0 queue and cross-refs [`V3_ENGINE_TRAIT_BOUNDARIES.md`](V3_ENGINE_TRAIT_BOUNDARIES.md) §8.1 / §1 banner plus [`WALLET_REWRITE_PLAN.md`](design/WALLET_REWRITE_PLAN.md). Dedicated audit markdown now landed: [`docs/completed/STAGE_1_COMPLETION_AUDIT.md`](completed/STAGE_1_COMPLETION_AUDIT.md). - **Stage 1 PR 5 — `PendingTxEngine` trait surface and Phase 1 substrate** (`feat/stage-1-pr5-pending-tx-engine`, 2026-05-27). Lands the Round-3-closed `PendingTxEngine` trait, the (γ) lean three-collection reservation model, secondary-engine trait seams, and `Engine` orchestration dispatch per [`docs/design/STAGE_1_PR_5_PENDING_TX_ENGINE.md`](completed/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 + 'static` at [`engine/traits/pending_tx.rs`](../rust/shekyl-engine-core/src/engine/traits/pending_tx.rs) with `build` / `submit` / `discard` / `outstanding` / optional `signal_mempool_evicted` (C5α/β). - `pub struct LocalPendingTx` at [`engine/local_pending_tx.rs`](../rust/shekyl-engine-core/src/engine/local_pending_tx.rs) as the V3.0 production `P` parameter default for `Engine` (C5β). - `pub struct SnapshotId([u8; 16])` and domain-separated `derive_snapshot_id(&LedgerSnapshot)` (C1). - Submit-path error vocabulary: `SubmitError`, `TerminalErrorKind`, `AmbiguousErrorKind`, `DiscardReason`, `ReservationExtension`, `PendingTxError` augmentations (C2). - `pub enum PendingTxDiagnostic` + `emit_pending_tx` helper on `DiagnosticSink` (C3); `AssertionSink` / `PanickingSink` pending-event recording for tests (C7). - Secondary-engine traits: `Signer` + `LocalSigner` (C4α), `OutputSelector` + `WalletGreedyOutputSelector` (C4β), `FeeEstimator` + `DaemonFeeEstimator` (C4γ). - `FaultInjecting` FIFO fault-injection wrapper under `#[cfg(any(test, feature = "test-helpers"))]` (C7); `Engine::replace_pending_tx` test hook (C6). - **Stage 1 PR 4 — `RefreshEngine` trait surface** (`feat/stage-1-pr4-refresh-engine`, 2026-05-15 → 2026-05-20). Lands the Phase-0a-binding `RefreshEngine` trait and the `ViewMaterial` adjacent type per [`docs/design/STAGE_1_PR_4_REFRESH_ENGINE.md`](completed/STAGE_1_PR_4_REFRESH_ENGINE.md) §4 Phase 0a + Phase 0c + Phase 0e and [`docs/V3_ENGINE_TRAIT_BOUNDARIES.md`](V3_ENGINE_TRAIT_BOUNDARIES.md) §2.3 (PR 4 C0 = `322677261`; C1 = `d3edc1abb`). - `pub trait RefreshEngine: Send + Sync + 'static` at [`engine/traits/refresh.rs`](../rust/shekyl-engine-core/src/engine/traits/refresh.rs) with one async method `produce_scan_result(snapshot: LedgerSnapshot, daemon: &D, opts: &RefreshOptions, cancel: &CancellationToken, progress: &watch::Sender, diagnostics: &dyn DiagnosticSink) -> Result` and `type Error: Into`. - 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::Error` is **unit-variant-only at the trait surface** per §5.4.7 R6 reframe: rich structured diagnostic information flows through the `&dyn DiagnosticSink` second channel; the synchronous return is a structural- branch signal only. Of `RefreshError`'s six variants, three are reachable from a `RefreshEngine` impl's `Self::Error` via `Into` (`Cancelled` unit, `Io(IoError)`, `InternalInvariantViolation { context: &'static str }`); three are orchestrator-constructed only (`MalformedScanResult` at the merge layer; `ConcurrentMutation` at the merge gate; `AlreadyRunning` at binary-layer single-flight). - `ScanResult` atomicity-under-cancellation contract: `produce_scan_result` returns **either** a `ScanResult` covering the full span it scanned **or** `RefreshError::Cancelled` — no partial-span result is ever returned (R7 disposition). - `LedgerSnapshot` is 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. - `&D` daemon-handle borrow with the §2.5 `Clone + Send + Sync + 'static` bound on `D`, so implementors can clone internally if they need an owned handle to spawn work (e.g., parallel block-fetch refinements); implementors MUST NOT borrow `&D` across a `tokio::spawn` boundary. - `pub struct ViewMaterial { spend_pub: EdwardsPoint; view_scalar: Zeroizing; x25519_sk: Zeroizing<[u8; 32]>; ml_kem_dk: Zeroizing>; spend_secret: Zeroizing<[u8; 32]> }` at [`engine/view_material.rs`](../rust/shekyl-engine-core/src/engine/view_material.rs) with `Zeroize + ZeroizeOnDrop` derived; capturing the view-and-spend material at `LocalRefresh::new` so the `Scanner` builds once and is held for the instance lifetime (no per-attempt scanner construction; no per-attempt secret duplication; R4 a-instance-scoped). - The `LocalRefresh` implementor at [`engine/local_refresh.rs`](../rust/shekyl-engine-core/src/engine/local_refresh.rs) (PR 4 C4 = `ac100e1ab`) is the V3.0 production `R` parameter for `Engine`; future implementors (Stage 4 actor-mesh `RefreshActor`; any future producer variant) implement the same trait surface. - **Stage 1 PR 4 — `RefreshDiagnostic` enum + `DiagnosticSink` trait + Stage 1 sink implementations** (PR 4 C2 = `8fc207051`; `SuppressedRateLimit` variant per Round 4 review pass F6 = same commit). Lands the second channel of the two-channel error / diagnostic actor-mesh seam per [`docs/design/STAGE_1_PR_4_REFRESH_ENGINE.md`](completed/STAGE_1_PR_4_REFRESH_ENGINE.md) §5.4.7 R6 reframe + §5.4.8 attack-surface dispositions. - `pub enum RefreshDiagnostic` at [`engine/diagnostics/refresh_events.rs`](../rust/shekyl-engine-core/src/engine/diagnostics/refresh_events.rs) with `#[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-added `SuppressedRateLimit { class: SuppressedClass }`. - Supporting bounded enums (`MalformedKind`, `DaemonOp`, `ProtocolErrorKind`, `SuppressedClass`), all `#[non_exhaustive]`; `SuppressedClass` carries one arm per rate-limited event class (`DaemonMalformed`, `DaemonTimeout`, `DaemonProtocolError`, `ReorgObserved`, `ScanProgress`). The `SuppressedRateLimit` variant carries *only* `class: 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 + 'static` with one method `fn emit(&self, event: RefreshDiagnostic)`. Trait-level contract pins (rustdoc): emission is **non-blocking** (extends to **non-blocking under concurrent emission**, foreclosing `Mutex>`- style implementations that re-introduce the producer- liveness hazard at scale); emission/return **coherence** (every non-`Cancelled` `Err` return is preceded by at least one corresponding `RefreshDiagnostic` emission before the error returns, with `AssertionSink`-driven property tests at C7 as the canonical reference per [`19-validation-surface-discipline.mdc`](../.cursor/rules/19-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-fidelity `RefreshDiagnostic` consumers 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 TracingDiagnosticSink` ship as the Stage 1 sink implementations; `TracingDiagnosticSink::emit` routes **per-class projections** to `tracing::event!` per the Round-4-review-pass F9 audit (variant tag only for `DaemonMalformed` / `DaemonProtocolError` / `SuppressedRateLimit`; bucketed `elapsed` for `DaemonTimeout`; bucketed `depth` for `ReorgObserved`; bucketed `candidates` for `ScanProgress` with `height` elided), not the full `RefreshDiagnostic` `Debug` impl. - All trait + enum surface re-exported flat at the `shekyl_engine_core` crate root per the R3 pattern. - **Stage 1 PR 4 — C6 no-Mock substrate pass (`RefreshEngine` / `LedgerEngine` failure-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 (commit `8484e669a`) and sub-pin extension (commit `29cb7e138`, F-Mock-1 through F-Mock-8). The pass closes the [`docs/FOLLOWUPS.md`](FOLLOWUPS.md) "Stage 1 retroactive Mock-X cleanup: `MockLedger` → `LocalLedger::from_test_blocks(...)` + `FaultInjecting`" entry and applies the no-Mock pattern PR 3 established (production-only implementors + composable trait-level `FaultInjecting` wrappers) to PR 2's inherited `MockLedger` parallel-implementation. *C6α — `FaultInjecting` wrapper + `test-helpers` feature* (commit `e9310542a`): - Adds `test-helpers = []` Cargo feature to [`rust/shekyl-engine-core/Cargo.toml`](../rust/shekyl-engine-core/Cargo.toml) (mirrors the `bench-internals` precedent) 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.rs`](../rust/shekyl-engine-core/src/engine/fault_injecting_refresh.rs) implementing `FaultInjecting` with the Option (i) wrapper API (`type Error = RefreshError`; FIFO `Mutex>` queue; `queue_failure(err)` general injector; `queued_failures()` drain inspector; `debug_assert!`-on-Drop queue-drain contract per F-Mock-2). - Adds `Engine::replace_refresh` test-only setter on [`engine/lifecycle/support.rs`](../rust/shekyl-engine-core/src/engine/lifecycle/support.rs) mirroring the existing `replace_daemon` / `replace_ledger` helpers. - 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` + `LocalLedger::from_test_blocks` + `MockLedger` retirement*: - Adds `engine/fault_injecting_ledger.rs` implementing `FaultInjecting` with the same Option (i) wrapper shape (queue-of-`RefreshError`, `queue_failure` / `queue_concurrent_mutation` / `queued_failures`, `debug_assert!`-on-Drop). Not `Clone` by design — the prior `MockLedger`'s `Arc>` aliasing shape (inherited from CryptoNote test patterns) does not survive the no-Mock transition. - Adds test-only `LocalLedger::from_test_blocks(Vec)` constructor at [`engine/local_ledger.rs`](../rust/shekyl-engine-core/src/engine/local_ledger.rs). The V3.0 substrate supports the empty-`Vec` case only (the sole shape every existing `MockLedger`-replaced caller needs); non-empty `Vec` panics with a forward-pointer to the V3.1 `TestLedgerBuilder` substrate-design FOLLOWUPS entry. The `Vec` 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` (in [`engine/refresh.rs`](../rust/shekyl-engine-core/src/engine/refresh.rs)) from `MockLedger::with_seed(...)` + `queue_concurrent_mutation()` to `FaultInjecting::new(LocalLedger::from_test_blocks(Vec::new()))` + `queue_concurrent_mutation()`. The wrapper's non-`Clone` posture required restructuring the assertion sites from a cloned handle to read-guard access through the engine's `Arc>>`; 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_ledger` test from [`engine/test_support.rs`](../rust/shekyl-engine-core/src/engine/test_support.rs) (`ROLE_LEDGER` becomes dead weight because `LocalLedger`'s `from_test_blocks` is deterministic and consumes no seed; the `ROLE_DAEMON` HKDF-derivation pinned-fixture test in the same module covers the underlying derivation mechanism). - Updates the stale `MockLedger` reference in [`docs/V3_ENGINE_TRAIT_BOUNDARIES.md`](V3_ENGINE_TRAIT_BOUNDARIES.md) §1.2 (the only active-doc factual claim that named `MockLedger` as the current substrate; the broader historical references in §§4+ and the PR 2 / PR 3 design docs remain as historical-record prose per the `15-deletion-and-debt.mdc` "while we're here" discipline). *C6γ — `MockDaemon` → `TestDaemon` rename*: - Mechanical rename of the test-substitute type and every call site across [`engine/test_support.rs`](../rust/shekyl-engine-core/src/engine/test_support.rs) (struct, `impl Rpc`, `impl DaemonEngine`, module docstrings), [`engine/refresh.rs`](../rust/shekyl-engine-core/src/engine/refresh.rs), [`engine/lifecycle/mod.rs`](../rust/shekyl-engine-core/src/engine/lifecycle/mod.rs), [`engine/mod.rs`](../rust/shekyl-engine-core/src/engine/mod.rs), [`benches/common/engine_fixture.rs`](../rust/shekyl-engine-core/benches/common/engine_fixture.rs) (forward-pointer comment), and [`Cargo.toml`](../rust/shekyl-engine-core/Cargo.toml) (`ChaCha20Rng` rationale 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: `TestDaemon` signals the role correctly per the no-Mock substrate- inheritance discipline. - Active-doc trajectory updates in [`docs/V3_ENGINE_TRAIT_BOUNDARIES.md`](V3_ENGINE_TRAIT_BOUNDARIES.md) §1.2 (Generic `DaemonClient` trajectory row), §1.4 rename-chain note, §6.1 hybrid-test discussion, §6.2 RNG-seed pin, §3.5 `Rpc`-impl rationale, and the §"Linked file paths" inventory entry (rename chain extended: `MockRpc` → `MockDaemon` → `TestDaemon`). *Test gates (post-C6).* `cargo fmt --all -- --check` clean; `cargo clippy -p shekyl-engine-core --all-targets --features test-helpers -- -D warnings` clean; `cargo clippy -p shekyl-engine-core --all-targets -- -D warnings` clean (default features); `cargo test -p shekyl-engine-core --lib` 152/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`)* (commit `c9e65bbc6`): - Refactors `Engine::replace_refresh` at [`engine/mod.rs`](../rust/shekyl-engine-core/src/engine/mod.rs) from a `&mut self` setter into a consume-and-rebuild constructor (`fn replace_refresh(self, refresh: R2) -> Engine`) mirroring the existing `replace_daemon` / `replace_ledger` shape at [`engine/lifecycle/support.rs`](../rust/shekyl-engine-core/src/engine/lifecycle/support.rs). The refactor lets the generic `R` type parameter change between construction and replacement so test orchestration can build an `Engine<…, LocalRefresh>` at assemble time and rewire it to `Engine<…, FaultInjecting>` for failure-injection scenarios without going through a `dyn`-erased trait object. - Adds `AssertionSink`, `PanickingSink`, and the `PanickingSinkTrigger` configuration enum to [`engine/diagnostics/sink.rs`](../rust/shekyl-engine-core/src/engine/diagnostics/sink.rs), all gated `#[cfg(any(test, feature = "test-helpers"))]` per the F-Mock-1 cfg-symmetry pin. `AssertionSink` records emitted `RefreshDiagnostic` events for post-hoc coherence assertions; `PanickingSink` panics on configured trigger events to exercise producer panic-safety. - Adds `proptest = "1"` as a `dev-dependency` in [`rust/shekyl-engine-core/Cargo.toml`](../rust/shekyl-engine-core/Cargo.toml) powering the new producer property tests below. - Adds the hybrid retry test `hybrid_refresh_engine_orchestrator_cancellation_retries` at [`engine/refresh.rs`](../rust/shekyl-engine-core/src/engine/refresh.rs) that exercises the producer-trait / orchestrator cancellation-checkpoint split end-to-end against the fully-composed `Engine, FaultInjecting>` stack, verifying the orchestrator retries on `ConcurrentMutation` (driven by `FaultInjecting::queue_concurrent_mutation`) and surfaces cancellation cleanly when `FaultInjecting` injects `RefreshError::Cancelled`. - Adds the `producer_property_tests` module at [`engine/local_refresh.rs`](../rust/shekyl-engine-core/src/engine/local_refresh.rs) with five parametric coherence tests, one `proptest!`-driven fuzz test (`coherence_proptest_fuzz_chain_and_injection`) exercising randomized chain length + failure-injection scenarios, four panic-safety tests verifying clean unwind through `PanickingSink` panics across `DaemonMalformed` / `DaemonProtocolError` / `ScanProgress` / `Any` triggers plus a recovery test, and a classifier sanity test. The coherence tests exercise the §5.4.6 emission/return coherence pin: every non-`Cancelled` `RefreshError` is preceded by a corresponding `RefreshDiagnostic` emission. The panic-safety tests verify the §5.4.6 producer-side robustness property: `Scanner` zeroizes cleanly via `Drop` across a panicking `emit`, cancellation-token state remains well-defined, and the refresh attempt fails predictably without corrupting interior state. Tests are deterministic via a compile-time-generated `PROPERTY_TEST_MASTER_SEED` and `#[tokio::test(start_paused = true)]` for fake-time async scheduling. *Test gates (post-C7).* `cargo fmt --all -- --check` clean; `cargo clippy -p shekyl-engine-core --all-targets --features test-helpers -- -D warnings` clean; default-feature clippy clean; `cargo test -p shekyl-engine-core --features test-helpers --lib` 170/170 pass (152 → 170: +18 C7 tests); `cargo doc -p shekyl-engine-core --features test-helpers --no-deps` green 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.md`](completed/STAGE_1_PR_4_REFRESH_ENGINE.md) gains the Phase-1-landed Status-banner closure paragraph enumerating C0–C8 landing SHAs; §7.X gains per-`Commit Cn` `Landed:` lines anchoring each commit's SHA inline next to the design-time prose. - [`docs/V3_ENGINE_TRAIT_BOUNDARIES.md`](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.md`](FOLLOWUPS.md) gains 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::from_test_blocks` and `MockDaemon` → `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`](../.cursor/rules/91-documentation-after-plans.mdc)'s stale-doc detection discipline and [`.cursor/rules/15-deletion-and-debt.mdc`](../.cursor/rules/15-deletion-and-debt.mdc)'s "deferred without a named home is the failure mode" framing). Surfaced during a post-C8 review of `docs/FOLLOWUPS.md` against the actual code state in `engine/local_ledger.rs:356–367` (trait-method `apply_scan_result` discards `Vec` and short- circuits `populate_engine_handle_fields`) and `engine/merge.rs:181–215` (inherent `Engine::apply_scan_result` runs the post-pass against the captured `inserted` indices) — the two paths diverge by construction in the post-Phase-1 substrate. - [`docs/FOLLOWUPS.md`](FOLLOWUPS.md) P1 / P2 / P3 entries rewritten with **Post-PR-4-Phase-1 substrate** subsections + substrate-anchored reopening criteria per [`.cursor/rules/21-reversion-clause-discipline.mdc`](../.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 the `LedgerEngine::apply_scan_result` trait surface, absorbing P1 / P2 / P3 as a side effect. Phase 1 settled on α (preserved current shape; trait surface unchanged) per `STAGE_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 integrates `RefreshHandle`") survives intact and is restated as "P1 closes before any binary integrates `RefreshHandle`". 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) `RefreshEngine` owns the merge post-pass is newly available because PR 4 landed the `RefreshEngine` trait at C1 / C4); P2 → `refresh/p2-wallet-birthday-plumbing` (substrate well-defined: `LocalRefresh::new` is 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). - [`docs/design/STAGE_1_PR_4_REFRESH_ENGINE.md`](completed/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 per `15-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`, and `cargo doc --no-deps` all inherit C8's results unchanged (170 / 170 lib tests pass; fmt clean; clippy clean under both default and `test-helpers` features; 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`](../.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 in [`rust/shekyl-scanner/src/scan.rs`](../rust/shekyl-scanner/src/scan.rs). Six sites updated from pre-C4 `scan_transaction` to C4-landed `scan_transaction_with_cancel`, plus the gate-test rustdoc return-type updated from `Ok(Timelocked::empty())` to `Ok(ScanOutcome::Completed(Timelocked(empty)))` to match the actual `ScanOutcome` variant the gate returns. Closes Copilot finding IDs 3278232594 / 3278232649 / 3278232666 / 3278232686 plus two same-class adjacent sites discovered during the audit. - **C11** `949e42bd8` — `bench_fixtures` rustdoc fact-fix in [`rust/shekyl-scanner/src/bench_fixtures.rs`](../rust/shekyl-scanner/src/bench_fixtures.rs). The `make_bench_wallet` spend-secret comment cited the on-chain spend point as the basepoint when `fake_spend_key_bytes()` actually returns `2 * G`. The `fake_spend_key_bytes()` rustdoc opening was internally contradictory and is rewritten as a clean three-property justification (torsion-free; non-default; distinct from `G`). 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 in [`rust/shekyl-engine-core/src/engine/traits/refresh.rs`](../rust/shekyl-engine-core/src/engine/traits/refresh.rs). The `RefreshEngine` trait 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`](../.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 in [`rust/shekyl-scanner/benches/scan_transaction.rs`](../rust/shekyl-scanner/benches/scan_transaction.rs). Both warm-cache benchmark variants used `iter_batched_ref` with an in-routine `mem::replace(b, block.clone())`, placing `ScannableBlock::clone` inside the timed region. Switched to `iter_batched(|| block.clone(), |block| scanner.scan(block), ..)` so the clone is in the setup closure and only `Scanner::scan` is measured. **F11-S audit-trail impact: ZERO** — the F11-S binding measurement (per [`docs/design/STAGE_1_PR_4_REFRESH_ENGINE.md`](completed/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 at `a4da2212a` and 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-scanner` for C10 / C11 / C13; `shekyl-engine-core` for 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 ran `cargo check --benches` to confirm the bench targets compile under the new `iter_batched` shape. *C14 — `[Unreleased]` doc-after-plans propagation for C10 – C13* (this commit): - Doc-only follow-up commit per [`.cursor/rules/91-documentation-after-plans.mdc`](../.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 SHA `839c4bbfd`. 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`, and `cargo doc --no- deps` gates 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 touch `engine/`-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`](../.cursor/rules/90-commits.mdc). - **C15** `bafb9c548` — refresh-trait `[`LocalRefresh`]` rustdoc link target fix in [`rust/shekyl-engine-core/src/engine/traits/refresh.rs`](../rust/shekyl-engine-core/src/engine/traits/refresh.rs). Two `[`LocalRefresh`]` reference-link aliases (lines 204 + 258 of the `RefreshEngine` trait file) pointed at `super::super::Engine` instead of `super::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 the `Engine` struct rather than `LocalRefresh`. Correct target verified at source: `LocalRefresh` lives at `engine/local_refresh.rs:250`, re-exported at `engine/mod.rs:187`; from `engine::traits::refresh`, `super::super::LocalRefresh` resolves through the re-export (matching the working precedent at `engine/fault_injecting_refresh.rs:105`). Closes Copilot finding IDs 3278391428, 3278391456. - **C16** `376e1e821` — `FaultInjecting` + `FaultInjecting` Drop-time `debug_assert!` message fix in [`rust/shekyl-engine-core/src/engine/fault_injecting_refresh.rs`](../rust/shekyl-engine-core/src/engine/fault_injecting_refresh.rs) and `rust/shekyl-engine-core/src/engine/fault_injecting_ledger.rs`. Both Drop messages told test authors to "drain via `queued_failures()` and `consume_or_inject`". Neither instruction was usable: `consume_or_inject` does 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), and `queued_failures()` is a `usize` inspector, 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 — with `queued_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-side `should_panic` had also been pinned on the older "FaultInjecting" (without ``) 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-scope `should_panic` re-pin. Test counts unchanged at 170 / 170 lib tests pass; doc warnings unchanged at 49 (C9 baseline). The two re-pinned `should_panic` tests 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`](../.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`, and `cargo doc --no-deps` gates 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`](../.cursor/rules/90-commits.mdc). - **C18** `6cc22965f` — `Scanner::scan_with_cancel` per-tx safe-point cancellation check in [`rust/shekyl-scanner/src/scan.rs`](../rust/shekyl-scanner/src/scan.rs). The F11-S binding's between-tx safe-point per `RefreshEngine` trait rustdoc checkpoint 5 was delivered only via the inner per-output iter-0 check inside `scan_transaction_with_cancel`. For transactions whose per-output loop never runs (zero-output txs; `tx.version() != 2`; malformed `extra`; 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 of `N` such transactions deferred cancellation by `N × O(1)-per-tx-skip` cost rather than bounded at a single tx-entry's cost. Fix adds `if 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 the `outer_per_tx_loop_cancellation_fires_for_zero_output_tx` regression test (V2 miner-only block via `Input::Gen(0)` + empty outputs/extra). The `cancel_tests` module 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 across `N_outputs` per tx and well below the F11-S worst-case per-output cost. Closes Copilot finding ID 3278452877. - **C19** `5749f444c` — dead `ScanOutcome::Cancelled` arm `debug_assert!` in `InternalScanner::scan` in [`rust/shekyl-scanner/src/scan.rs`](../rust/shekyl-scanner/src/scan.rs). The function delegates to `scan_with_cancel` with a never-cancelling closure (`|| false`); under the closure-invariant, the `Cancelled` variant is unreachable. The previous code mapped the unreachable variant to `Ok(Timelocked(Vec::new()))` for production-panic-free behavior — but the empty-result fallback would silently mask future logic-dispatch regressions. Fix adds `debug_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 (preferring `debug_assert!` over `unreachable!()`) named in the same arm's comment so a future refactor preserves the rationale. Closes Copilot finding ID 3278452893. - **C20** `3331fb82e` — `ViewMaterial::try_from_keys` view_scalar canonical-bytes decoding in [`rust/shekyl-engine-core/src/engine/view_material.rs`](../rust/shekyl-engine-core/src/engine/view_material.rs). The previous reconstruction via `Scalar::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) validates `keys.spend_pk` with explicit `IoError::Scanner` on non-canonical bytes; the asymmetric treatment of view-scalar vs. spend-public-key was not justified by the threat model. Fix switches to `Option::::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 returns `None` and maps to `RefreshError::Io(IoError::Scanner)` with an operator-actionable detail string. The rustdoc's field-derivation summary and `# Errors` block were both updated to describe the new shape and cite `30-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 downstream `shekyl-engine-core` regression 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`](../.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`, and `cargo doc --no-deps` gates 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 (stale `expect()` 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`](../.cursor/rules/90-commits.mdc). - **C22** `168ff0e22` — stale `scan_transaction_with_cancel` `expect()` strings in [`rust/shekyl-scanner/benches/scan_transaction.rs`](../rust/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 surface `Scanner::scan(..)`. The mismatch is the same class as the C10 commit (`60f401e77`) that rewrote six rustdoc fn-name references in `scan.rs` post 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 on `Err`, and the bench fixtures' `Scanner::scan` invocations never produce `Err` by 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 structural `CryptoError::DecapsulationFailed` match in [`rust/shekyl-scanner/src/bench_fixtures.rs`](../rust/shekyl-scanner/src/bench_fixtures.rs). The `typical_case_first_output_exits_via_view_tag_mismatch` sanity-check test asserted `format!("{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 confirms `scan_output_recover` constructs multiple `DecapsulationFailed(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 inner `String` field followed by a separate inner-message `assert!` — the two-class pinning (variant + reason within variant) is preserved; only the FORM changes (binding the inner `String` directly via pattern-match rather than going through `format!("{err:?}")`). Comment rewritten to enumerate the two drift classes the new shape catches explicitly. `CryptoError` imported via the existing `shekyl_crypto_pq::error` public 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 targeted `cargo test ... typical_case_first_output_exits_via_view_tag_mismatch -- --nocapture` to 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`](../.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`, and `cargo doc --no-deps` gates 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`](../.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`](../.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` — stale `build_scanner_from_keys` rustdoc / comment references in [`rust/shekyl-engine-core/src/engine/mod.rs`](../rust/shekyl-engine-core/src/engine/mod.rs) (comment above `pub(crate) fn keys()`) and [`rust/shekyl-engine-core/src/engine/view_material.rs`](../rust/shekyl-engine-core/src/engine/view_material.rs) (module rustdoc § "Field shape"). The free function `build_scanner_from_keys` was deleted in C5β (`b6a1274de` — legacy producer-scaffolding deletion in `engine/refresh.rs`) and replaced by `ViewMaterial::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 of `Engine::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.mdc`](../.cursor/rules/21-reversion-clause-discipline.mdc) naming Phase 2's `sign_transfer` / `tx_proof` / `reserve_proof` surfaces 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_refresh` is `pub(crate)`, link from `pub` `view_material` module'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)]` on `pub(crate) use refresh::RefreshEngine` re-export in [`rust/shekyl-engine-core/src/engine/traits/mod.rs`](../rust/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` 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`](../.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 of `Engine::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` — reword `test-helpers` Cargo feature description in [`rust/shekyl-engine-core/Cargo.toml`](../rust/shekyl-engine-core/Cargo.toml) to 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`, `FaultInjecting`, `Engine::replace_refresh`, `LocalLedger::from_test_blocks`) remain `pub(crate)` with the feature enabled — no `__test_helpers` re-export module exists at the crate root (verified at source vs. the sibling `bench-internals` feature which DOES have a `__bench_internals` re-export at `lib.rs:46-56`). Per [`.cursor/rules/21-reversion-clause-discipline.mdc`](../.cursor/rules/21-reversion-clause-discipline.mdc) chose 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 four `pub(crate)` surfaces); what it does NOT do (no public re-exports; compare-and- contrast with `bench-internals` makes the asymmetry explicit); why no re-exports yet (pre-genesis no-consumer state); reopening criteria (when the first downstream consumer emerges, add `__test_helpers` module under the `__bench_internals` precedent + V3.0-targeted FOLLOWUPS item + `AUDIT_SCOPE.md` amendment 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 to [`docs/V3_ENGINE_TRAIT_BOUNDARIES.md`](../docs/V3_ENGINE_TRAIT_BOUNDARIES.md) §6 "Test boundary" / §6.1 "Pinned commitments for Stage 1". The §6 framing still asserted a "fully- mocked `Engine`" 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: `MockKey` in PR 3 (per `STAGE_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); `MockLedger` in PR 4 C6β (replaced by `FaultInjecting`); `MockRefresh` in PR 4 C6α (replaced by `FaultInjecting`). 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 to `FaultInjecting<...>` 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) retain `MockLedger::with_seed(...)` literal example text as a deliberate scope decision per `15-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-core` Rust 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 touched `Cargo.toml` only (comment-only change inside `[features]`); fmt / clippy / test all clean; no `.rs` files touched. C28 touched a `docs/` 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`](../.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`, and `cargo doc --no-deps` gates 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 `### Added` and `### Changed` entries below for the trait- surface and `Engine` four-parameter additions PR 4 ships, per the C8 spec at `STAGE_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 commit `c0c4a11e5`, 2026-05-19). Adds `external/randomx-v2` submodule pinned to Shekyl-Foundation/RandomX SHA `aaafe71322df6602c21a5c72937ac284724ae561` (v2.0.1 release; identical to `tevador/RandomX:master` at pin time, per the dependency-discipline verification in `docs/completed/RANDOMX_V2_PHASE1_PLAN.md` §1.3). Adds `BUILD_RANDOMX_V2_MINER_LIB` CMake option (default `OFF`). When `ON` on a single-config generator (Ninja, Make), an `ExternalProject_Add` block in `external/CMakeLists.txt` builds the v2 fork out-of-tree under `${CMAKE_BINARY_DIR}/external/randomx-v2-build/` and exposes the `shekyl_randomx_v2` `IMPORTED` static-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-safe `LIST_SEPARATOR`-based forwarding pattern. On multi-config generators (MSVC, Xcode, Ninja Multi-Config) the option fails with a `FATAL_ERROR` directing the developer to `-G Ninja` plus an explicit `-DCMAKE_BUILD_TYPE`; per-`CONFIG` wiring is the V3.x Phase 2 FOLLOWUPS item alongside the first real consumer. The out-of-tree build pattern avoids the target-name collision with `external/randomx` (v1.2.1), which declares the same `project(RandomX)` and `add_library(randomx ...)` symbols; see `RANDOMX_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 crate `rust/shekyl-pow-randomx/`) and Phase 3's miner cutover. The existing `external/randomx` (v1.2.1 at `102f8acf`) is unchanged; the v1 fallback path per `docs/design/RANDOMX_V1_FALLBACK.md` §1 remains reachable. See `docs/completed/RANDOMX_V2_PHASE1_PLAN.md` for the full scope, the `ExternalProject_Add` configuration rationale, the build-smoke test results, the §10 implementation-time dispositions (D1 `check_submodule` omission, 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 invoking `07-consensus-atomic-cutovers.mdc`. The PR contains eleven commits that respect single-purpose scope per `90-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 of `docs/design/DAA_LWMA1_PLAN.md` Phase 4 and the V3.0 DAA item in `docs/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 from [`zawy12/difficulty-algorithms#3`](https://github.com/zawy12/difficulty-algorithms/issues/3) with `N=90`, `T=120s`, `GENESIS_DIFFICULTY=100`. The FFI surface (`shekyl_difficulty_lwma1_next`) is wrapped at the three `Blockchain` call sites (`get_difficulty_for_next_block`, `recalculate_difficulties`, `get_next_difficulty_for_alternative_chain`) by the `lwma1_next_difficulty` helper in `blockchain.cpp`, which throws `cryptonote::difficulty_computation_error` (declared in `src/cryptonote_core/difficulty_engine_error.h`) on non-zero FFI return codes. - **FTL**: `CRYPTONOTE_BLOCK_FUTURE_TIME_LIMIT` (`60*60*2` = 7200s) becomes `SHEKYL_DAA_FTL_SECONDS` = 540s (zawy12-required `N*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 becomes `SHEKYL_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 to `SHEKYL_DAA_TARGET_SECONDS` (also 120s). 8 production sites and 5 test sites; verified by the consensus-invariants gate (`scripts/ci/check_consensus_invariants.sh` invariant 3). - `CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_SECONDS_V2` is preserved with its RHS rewired from `DIFFICULTY_TARGET_V2` to `SHEKYL_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 by `SHEKYL_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 `#define`s removed from `src/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-delta `CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_SECONDS_V1` is removed (pre-genesis Monero behavior, dead under `60-no-monero-legacy.mdc`). - `next_difficulty` and `next_difficulty_64` deleted from `src/cryptonote_basic/difficulty.{h,cpp}` (surgical, per the pre-flight's drift-F6 amendment: the `check_hash` PoW 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; the `lwma1-cross-check` harness (Phase 2 vintage) is retained in `tests/difficulty/CMakeLists.txt`. - `lift_up_difficulty` helper plus `gen_block_invalid_nonce` and `gen_block_invalid_binary_format` test classes removed from `block_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 for `mining_status.block_target` and `get_info.target` at `120`. Both gtests plus the `static_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.yml` plus `scripts/ci/check_consensus_invariants.sh` — three source-level grep invariants (no live consumers of the deleted DAA functions; no C-ABI in `rust/shekyl-difficulty`; no orphaned references to the deleted `#define`s). Shared landing pad for the upcoming RandomX v2 Phase 2f symbol-isolation checks. Binary-level `nm`-on-`shekyld` verification 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/`; the `lwma1-cross-check` harness stays. - **F2** — `CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_SECONDS_V2` preserved with rewired RHS (option B); `_V1` deleted. - **F3** — V1 `next_difficulty(...)` fixtures in `block_validation.cpp` deleted along with the helper that drove them. - **F4** — `DIFFICULTY_TARGET_V2` consumer 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_V2` consumer **undercount in `blockchain.cpp`**: the plan's §9.7 enumeration missed two sites at lines 4239 / 4243 (an MTP-window correction and a `timestamps.back() + DIFFICULTY_TARGET_V2` adjustment inside `check_block_timestamp`); both rewired to `SHEKYL_DAA_TARGET_SECONDS`. `wallet2.cpp`'s lines 181, 182, 5975, 11548 were never drift — the earlier text mis-attributed F5 to `wallet2.cpp`; corrected 2026-05-18 per PR #53 Copilot review C-6. - **F6** — surgical (not wholesale) deletion of `src/cryptonote_basic/difficulty.{h,cpp}`: the `check_hash` PoW-validation family is retained; only the `next_difficulty` family is deleted. - **F7** — `check_difficulty_checkpoints()` is NOT a deletion target. Pre-flight §14 (and `DAA_LWMA1.md` §7.1) erroneously enumerated it as a symbol-isolation deletion candidate. The function in `blockchain.cpp:1066` is 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.mdc` sub-clause 4.3): - **A. Consensus-affecting changes** (priority attention): `blockchain.cpp` DAA rewires (commit 3), FTL rewires (commit 4), MTP rewires (commit 5), `cryptonote_config.h` deletions (commit 7), `difficulty.cpp` deletions (commit 8). - **B. Mechanical rewires** (value-unchanged): `DIFFICULTY_TARGET_V2` → `SHEKYL_DAA_TARGET_SECONDS` (commit 6), `DIFFICULTY_BLOCKS_ESTIMATE_TIMESPAN` rewires 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 per `06-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 of `blockchain.cpp`'s three `next_difficulty` call sites, the FTL/MTP consumer surfaces, and the deleted `#define`s are all captured in the pre-cutover `dev` SHA 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 in `docs/FOLLOWUPS.md`. - **LWMA-1 difficulty-adjustment migration — Phase 0 design docs** (`feat/daa-lwma1-phase0-design`, 2026-05-17). Adds two Phase 0 design documents under `docs/design/`: [`DAA_LWMA1.md`](./completed/DAA_LWMA1.md) (the primary design) and [`DAA_LWMA1_PLAN.md`](./completed/DAA_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=15` with literal `// !!!` warning, `DIFFICULTY_CUT=60`) with LWMA-1 from zawy12's canonical reference at [`zawy12/difficulty-algorithms#3`](https://github.com/zawy12/difficulty-algorithms/issues/3), implemented as a Rust crate `shekyl-difficulty` per `20-rust-vs-cpp-policy.mdc` rule 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 per `16-architectural-inheritance.mdc` pre-genesis discount and `60-no-monero-legacy.mdc` no-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-design `rust/shekyl-difficulty/src/lwma1.rs` sketch is explicitly documented as **not** canonical (different formula, missing `6*T` solvetime clamp, missing `N*N*T/20` minimum-L floor, missing `99/200` bias factor) and was **deleted** during Phase 0 so Phase 1 starts from an empty crate directory; the divergence catalogue is retained in `DAA_LWMA1.md` §2.4 as the design record of why each non-canonical shape is rejected. Reversion clauses per `21-reversion-clause-discipline.mdc` cover LWMA-2/3/4 and ASERT reopening criteria. *Round 2 review update (2026-05-17):* (a) reframes `shekyl-difficulty` as a **leaf crate** with zero internal workspace dependencies per `18-type-placement.mdc`, with FFI exposure routed through `shekyl-ffi` (`DAA_LWMA1.md` §2.1); (b) records the explicit "DAA is a primitive, not an actor" disposition (`DAA_LWMA1.md` §2.7) — `lwma1_next` is a free function plus typed constants plus the FTL/MTP predicates, no `DifficultyEngine` actor wrapper; (c) pivots the consensus-constants source-of-truth from a `cbindgen` handwave to the existing `config/consensus_constants.json` JSON-authority pattern documented in `docs/FOLLOWUPS.md` and 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++ `Blockchain` through 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 `#define` drift 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 is `CRYPTONOTE_BLOCK_FUTURE_TIME_LIMIT` (not `BLOCK_FUTURE_TIME_LIMIT`; there is no `_V2` variant), and `BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW` is currently `60` (Monero-era widening from the CryptoNote-original `11`), 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_*`, not `SHEKYL_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 the `core_rpc_server.cpp:1452 res.block_target` RPC-contract preservation property; (e) acknowledges Phase 4 atomicity as a deliberate exception to `06-branching.mdc` (FTL/MTP value changes cannot stage behind alias `#define`s without weakening consensus in the intermediate state); (f) resolves the bias-factor location drift — `99` and `200` (plus `6` and `1/20`) appear as bare integer literals inside `src/lwma1.rs` to match canonical zawy12 verbatim, **not** as named `pub(crate) const` in `consts.rs`; (g) mechanizes Phase 5's conditional cross-reference to `24-reviewer-discipline.mdc` so 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 a `solvetime[1]` `-T` offset regression vector to §8.1's required-vector list; (j) adds explicit MIT attribution to the Phase 2 vendored `tests/difficulty/zawy12_lwma1_reference.h`; (k) moves long reviewer-note prose out of the long-lived `Cargo.toml` into a Phase 1 review-checklist section; (l) flags `is_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_t` to canonical little-endian `[u8; 16]` byte arrays (`DAA_LWMA1.md` §6.1 and plan Phase 3). Rationale: Rust's `u128` C 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 native `uint128_t` and 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 produces `next_D == avg_D` (within rounding)" was mathematically wrong: with `solvetime[i] == T` for all `i`, the formula yields `next_D == avg_D * 99 / 100` — a deliberate 1 % downward bias, which is the point of the `99/200` factor 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's `height < N` short-circuit covers N blocks, not N+1; the Shekyl FFI `chain_height < N` translation puts blocks `1..=N` in the short-circuit per the new §5.6 validator consumer contract). (c) Adds `DAA_LWMA1.md` §5.6 "Validator consumer contract: `chain_height → header.difficulty`" specifying the off-by-one mapping between the DAA function's `chain_height` parameter (predecessor's height) and the block-being-validated's height, plus the per-block disposition: block 0 (genesis) is exempt; blocks `1..=N` carry `GENESIS_DIFFICULTY`; blocks `≥ N+1` are algorithm-computed. Pre-empts the Phase 4 reviewer's first question. (d) **Closes all Phase 0 open questions.** `GENESIS_DIFFICULTY = 100` and `N = 90` are 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 to `DAA_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) Reframes `T = 120 s` as Shekyl's chosen target block time (zawy12 LWMA-1 recommends 60–120 s for CPU-mineable chains) rather than "inherited from CryptoNote `DIFFICULTY_TARGET_V2`." The numerical value matches; the source-of-truth is the JSON authority `daa_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 the `u128` ABI unsoundness as a Tier 1 blocker but stopped short of proposing the specific wire representation. Round 5 closes this. `ShekylU128` decomposes the 128-bit value into two `u64` fields whose ABI is universally stable on every Shekyl- supported target — no `improper_ctypes` exposure, no MSRV-pin-to-1.78 constraint, no per-target ABI verification matrix. The struct-with-named-fields shape preserves explicit `lo`/`hi` semantics (debugger-friendly, unambiguous, survives any future endianness disposition because the field meaning is carried by the field name). Endianness is consensus-locked in `DAA_LWMA1.md` §6.1: `ShekylU128` is little-endian by field semantics — `lo` is the low 64 bits, `hi` is the high 64 bits, reconstruction is `value = (hi as u128) << 64 | (lo as u128)`. Cost: one struct definition and four lines of `From` impls per direction. Benefit: the consensus-critical surface is immune to `u128`-ABI target-portability issues permanently, not just on rustc ≥ 1.77. (b) **MTP 60 → 11 trade-off framing.** The `BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW = 60` → `SHEKYL_DAA_MTP_WINDOW = 11` change 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 calling `get_info` against the post-Phase-4 daemon receives a `block_target` field 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 in `DAA_LWMA1_PLAN.md`** Phase 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 PR `feat/consensus-atomic-cutovers-rule` ratifies the criteria as `.cursor/rules/07-consensus-atomic-cutovers.mdc` and 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 expect `next_D == avg_D * 99/100` on the §8.1 perfectly-stable hashrate input (deliberate downward bias from the `99/200` factor). The Round 4 fix did not synchronously update §5.3 step 7's derivation prose, which still described the `99/100` factor as "compensating for a ~1 % upward bias" — leaving the doc internally contradictory: one section described the factor as canceling drift (stable input → `avg_D` exactly), 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*T` clamp truncation, jump-rule amplification from downstream LWMA-2+ variants) present under realistic chain operation; on §8.1's *deterministic* unit-test vectors (all solvetimes exactly `T`, 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 observes `next_D == 990_000` on 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 to `DAA_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 constant `T`*, which §9.7's enumeration surfaced as a wallet consumer at `wallet2.cpp:181, 182, 5975, 11548` and `wallet_rpc_server.cpp:163` (unlock-time defaults, recent-spend-window math, `seconds_per_block` consumers, `suggested_confirmations_threshold` math — five wallet-side sites). §11 now reads accurately: the algorithm is not consumed by the wallet, but `T` is, with a value-preserving rewire from `DIFFICULTY_TARGET_V2` to `SHEKYL_DAA_TARGET_SECONDS` across 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 the `is_above_mtp` and `is_timestamp_below_ftl` predicates 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 in `tests/core_tests/block_validation.cpp:137` shrinks 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 the `cryptonote_core.cpp:1817, 1829, 1838` Poisson 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.md` Phase 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 across `src/` and `tests/`. (vi) `DAA_LWMA1.md` status 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](https://github.com/zawy12/difficulty-algorithms/issues/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*T` clamp, 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.md` Phase 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 canonical `LWMA1_()` and `LWMA3_()` 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 against `time(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 for `time_offset|TimeOffset|GetAdjustedTime|GetTimeOffset|MAX_PEER_DELTA|MAX_TIME_DELTA|MEDIAN_TIME|TIMESTAMPS_FOR_TIME_SYNC` against consensus-relevant surface. Lowering FTL from 7200 s to 540 s is therefore safe against the [zcash/zcash#4021](https://github.com/zcash/zcash/issues/4021) attack class. Disposition recorded in `DAA_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, the `FTL / 2` revert-threshold relationship per zawy12 issue #24 item 14 becomes load-bearing at that point and `daa_peer_time_revert_threshold_seconds` MUST 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_template` at `blockchain.cpp:1650–1656` (the canonical pattern: set `b.timestamp = time(NULL)`, then if `check_block_timestamp` fails, raise to `median_ts`). The MTP window change from 60 to 11 preserves the patch's effectiveness; no Phase 4 work required. Disposition recorded in `DAA_LWMA1.md` §5.5 with code citation. A minor doc-vs-code drift at `blockchain.cpp:1540`'s cached-template path is recorded as a `FOLLOWUPS.md` candidate, 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 to `T = 60 s` chains; the recommendation scales inversely with `T` and for `T = 120 s` the 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 `±7xT` rule, consistent with zawy12 issue #24 item 9's post-FTL deprecation of `±7xT`. `DAA_LWMA1_PLAN.md` gains 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.md` status 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)` returns `time_t`, which on 64-bit platforms (the only Shekyl-supported platforms per the 32-bit retirement chore landed at commit `e06ee37d96af`, recorded in `docs/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 per `75-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 `±7xT` disposition (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 in `DAA_LWMA1.md` body, 2 sites in `DAA_LWMA1_PLAN.md` body, 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 `.body` of [`zawy12/difficulty-algorithms#24`](https://github.com/zawy12/difficulty-algorithms/issues/24) via `docs/design/refs/zawy12_issue_24_history.md` at Phase 2 PR time, using the same `gh api` + `jq -r .body` mechanism 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.md` Phase 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 that `zawy12_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), and `zawy12_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.md` Phase 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 commit `e06ee37d96af` ("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 to `DAA_LWMA1_PLAN.md` Phase 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 in `DAA_LWMA1_PLAN.md` previously read "the **nine** MTP consumers ... (seven in `blockchain.cpp`, two in `block_validation.{h,cpp}`)"; it now reads "the **thirteen** MTP consumers ... (eight in `blockchain.cpp`, five in `block_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.cpp` is 3 files, not 4; the prior "4" likely double-counted `cryptonote_config.h` where the `#define` lives, but that's already counted in the adjacent "1 MTP `#define` removed" item). - **`DIFFICULTY_*` count (§9.2 in `DAA_LWMA1.md` and Phase 4 work item 3 + YAML phase4-cpp-cutover todo in `DAA_LWMA1_PLAN.md`).** Copilot flagged the plan's Phase 4 work item 3 ("six constants" but enumerating seven names); the same drift exists in `DAA_LWMA1.md` §9.2 line 1973 ("all five inherited `DIFFICULTY_*` `#define`s and the two timestamp-validation `#define`s") and in the plan's YAML todo block (line 18: "Delete the 6 inherited DIFFICULTY_*"). The §9.2 enumeration has always listed seven `DIFFICULTY_*` defines plus two timestamp-validation defines, and the §9.3 cross-reference at line 2022 ("the seven `DIFFICULTY_*` defines plus FTL plus MTP") and the plan's breakdown at line 789 ("7 `DIFFICULTY_*` 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.md` line 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`), executes `prev_max = max(timestamps[0] - T, timestamps[0])`, evaluating to `timestamps[0]` since `T > 0`. This overwrites the `-T` anchor the surrounding prose claims is preserved, producing `solvetime[1] = timestamps[1] - timestamps[0]` rather than the intended `solvetime[1] = timestamps[1] - (timestamps[0] - T) = T + T = 2T` on 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 evaluates `solvetime[1] = timestamps[1] - (t0 - T) = 2T` (using the `-T` anchor), then updates `prev_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 zawy12 `LWMA1_()` 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 from `docs/design/refs/zawy12_issue_3_lwma1.md` (lines 77–119 of the pinned `.body`), compiled with `g++ -std=c++17 -O2`, and ran against the §8.1 "perfectly stable hashrate" input vector with `avg_D = 1_000_000`, `N = 90`, `T = 120`, and `timestamps[i] = 1_700_000_000 + i*T` for `i ∈ 0..=N`. Result: canonical output `990_000` (matches §8.1 expected value). An initial harness run with `timestamps[i] = i*T` produced `10_000_000` due to `uint64_t(0) - uint64_t(120)` underflow at `timestamps[0] - T`; corrected to realistic Unix epoch timestamps and re-ran with the expected result. The Shekyl-corrected algorithm (transcribed from `docs/design/refs/shekyl_lwma1_running_max_symmetric_clamp.md`) was also compiled and run against the same stable input, producing byte-identical `990_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 with `timestamps[2] = timestamps[1] - 5*T`) produced canonical `990_000` (attack neutralized to `+1` via canonical's `previous_timestamp+1` floor; no penalty) versus Shekyl-corrected `992_000` (attacker's negative-solvetime contribution to `L` produces higher `next_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 the `docs/design/refs/zawy12_issue_3_lwma3.md` extraction 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 to `shekyl_lwma1_running_max_symmetric_clamp.md` to 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 in `DAA_LWMA1.md` §3 and `DAA_LWMA1_PLAN.md` are updated to the new name. The `zawy12_issue_3_lwma3.md` convenience extraction (verbatim LWMA-3 reference, *not* a pin) remains a Phase 2 work item per `DAA_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-256 `14c68aee9780ca1b1fb8ca28ac43f7956996859f5281ef166cc0634b2cc50df9`, captured-at 2026-05-18T05:25:21Z), `docs/design/refs/zawy12_issue_24_history.md` (LWMA history issue pin, SHA-256 `94a6fc8f10b57cf7d0731f62d07c0b4bbdf65d969d7c8679755b22eace76891d`, same capture timestamp), and `docs/design/refs/shekyl_lwma1_running_max_symmetric_clamp.md` (Shekyl hybrid reference, SHA-256 `f16f62695ae74b2ca47d15227b79035cdc349609d9fc73db2b7a3c57c0dfcc4a`, same capture timestamp). `DAA_LWMA1.md` §3's pin records embed the SHA-256s and timestamps; the `LWMA1_()` byte-offset anchors and the LWMA3_() convenience extraction remain Phase 2 work per `DAA_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-difficulty` crate scaffold per `DAA_LWMA1_PLAN.md`) opens against ratified spec. The pre-flight harness source (transcribed from the pinned `zawy12_issue_3_lwma1.md` LWMA1_() function) is available at this commit and is reproducible via `g++ -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) * r` rounding-to-3-significant-decimal- digits step from canonical `LWMA1_()` (`zawy12_issue_3_lwma1.md` lines 116–119 of the pinned `.body`). The §8.1 expected values all depend on this step; without it, the raw outputs are `989_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)` with `B = 1_700_000_000` (Unix epoch base). The pre-Round-13 specification used `i*T` or `(i-1)*T` formulas with `B` implicit; the latter produced `timestamps[0] = -T`, unrepresentable as `u64` (wraps to `~1.8e19`) and the cause of the pre-flight harness's initial `10_000_000` mis-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^7` instead of `97_297_200_000_000`; quotient `1_035_521_504` instead of step-9-rounded `1_040_000`). Round 13 rederives `L = T*(N-1)*(N-2)/2 = 469_920`, computes raw `next_D = 1_035_252`, applies step 9 to round to `1_040_000`, and cross-checks against the harness output. The minimum-L floor vector's expected output drops from `10_010_000` (analytic, missing step 9) to `10_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`, Shekyl `1_040_000`. Canonical's `911_000` is *below* the `990_000` stable 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, with `README.md` explaining build/run/license. The MIT SPDX identifier covers the canonical `LWMA1_()` transcription; the Shekyl variant header documents Shekyl Foundation origin. The `DAA_LWMA1.md` §3 reference list and §8.1 vector-derivation footer point at the harness directory; the Phase 1 implementer reproduces the pinned values via `g++ -std=c++17 -O2 preflight_outofseq.cpp -o p && ./p` before opening Phase 1's first commit. Round 13 leaves the `RATIFIED — Phase 0 close (2026-05-18 UTC)` line on `DAA_LWMA1.md` line 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. - **`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.md` Phase 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: 1. **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. 2. **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.mdc` no-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). 3. **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. 4. **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 under `docs/design/`: [`RANDOMX_V2_RUST.md`](./design/RANDOMX_V2_RUST.md) (the primary design), [`RANDOMX_V1_FALLBACK.md`](./design/RANDOMX_V1_FALLBACK.md) (the contingency design), and [`RANDOMX_V2_PLAN.md`](./design/RANDOMX_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 under `18-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 in [`RANDOMX_V2_RUST.md`](./design/RANDOMX_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 (`102f8acf` pin plus verifier toggle) for any time between Phase 0 and genesis release if the algorithm-review gate fails per `RANDOMX_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 per `docs/design/DAA_LWMA1_PLAN.md` Phase 2, and lands the `shekyl_difficulty_lwma1_next` FFI 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 is `shekyl-ffi` (hosting in `shekyl-difficulty` itself 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_unwind` panic-safety wrapper dropped.** The original Phase 3 prescription wrapped the FFI body in `std::panic::catch_unwind`. The workspace runs `panic = "abort"` in both `dev` and `release` profiles (`rust/Cargo.toml` lines 103, 106); under `panic = "abort"`, `catch_unwind` is a no-op because panics terminate the process before any catch can engage. The Rust algorithm body is panic-free by construction (returns `Result` for every spec error path; uses explicit `checked_*` / `try_from` overflow guards), and the §8.1 corpus exercises both branches. The FFI shim calls `lwma1_next` directly. `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 in `DAA_LWMA1.md` §3 as the pin record. - `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 (SPDX `BSD-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 (incomplete `next_D =` assignment and an unbalanced `)` in the jump-rule branch); the extraction preserves the malformation as-is, documented in both the file header and `DAA_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 exposing `shekyl_difficulty_lwma1_next` as a `pub unsafe extern "C" fn` with the `ShekylU128` two-u64 decomposition ABI per `DAA_LWMA1.md` §6.1 (Round 5's disposition against target-defined Rust `u128` C ABI). Error codes wire-stable at `0` / `-1` / `-2` / `-3` / `-4`; null-input pointers permitted iff `count == 0` (genesis short-circuit). Five unit tests cover the `ShekylU128` round-trip, the genesis path, the null-pointer rejection paths (both `out` and inputs-with-nonzero-count), and the `ERR_INVALID_COUNT` mapping. **C header (`src/shekyl/shekyl_ffi.h`).** Adds the `shekyl_difficulty_lwma1_next` declaration, the `struct shekyl_u128` definition, and the `SHEKYL_DIFFICULTY_OK` / `SHEKYL_DIFFICULTY_ERR_NULL_PTR` / `_ERR_INVALID_COUNT` / `_ERR_OVERFLOW` / `_ERR_INTERNAL` macros. The struct lives inside the existing top-of-file `extern "C"` block; macros sit at file scope below the block per the C++ rule that `extern "C"` applies to linkage of declarations, not to preprocessor symbols. **Cross-check harness (`tests/difficulty/lwma1_cross_check.cpp` + `tests/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_000` for vector 6, `911_000` for 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 from `shekyl/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.txt` with the `lwma1-cross-check` target (linked against `${SHEKYL_FFI_LINK_LIBS}`) and the `lwma1_cross_check` ctest 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 crate `rust/shekyl-difficulty` per `docs/design/DAA_LWMA1.md` and `DAA_LWMA1_PLAN.md` Phase 1. Pure- arithmetic `#![no_std]` + `#![deny(unsafe_code)]` leaf crate with zero internal workspace deps; the FFI export (`shekyl_difficulty_lwma1_next` with the `ShekylU128` ABI per `DAA_LWMA1.md` §6.1) is deferred to Phase 3 in `shekyl-ffi`. **Public surface.** `lwma1_next(chain_height, ×tamps, &cumulative_difficulties) -> Result` 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-corrected `99/200` formula 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 predicates `is_timestamp_below_ftl` and `is_above_mtp` co-located in the same crate per `DAA_LWMA1.md` §2.5. Window-shape constants `N`, `T_SECONDS`, `FTL_SECONDS`, `MTP_WINDOW`, `GENESIS_DIFFICULTY` flow through the existing `config/consensus_constants.json` JSON authority (extended with five `daa_*` keys); the bias factor `99/200`, the solvetime clamp `6`, and the min-L floor divisor `20` deliberately stay as bare integer literals inside `src/lwma1.rs` per 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.json` adds `daa_window_n=90`, `daa_target_seconds=120`, `daa_ftl_seconds=540`, `daa_mtp_window=11`, `daa_genesis_difficulty=100`. `cmake/generate_consensus_constants.py` extends `KEYS_INTEGER` and the emitted header with five `SHEKYL_DAA_*` macros; until Phase 4 lands, these macros are emitted but have no C++ consumer (the Phase 4 cutover replaces inherited `DIFFICULTY_TARGET_V2`, `CRYPTONOTE_BLOCK_FUTURE_TIME_LIMIT`, and `BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW`). `rust/shekyl-difficulty/build.rs` reads the same JSON and emits the Rust mirrors to `OUT_DIR` (Round 3's Option A; extending `shekyl-engine-core/build.rs` would have broken the leaf-crate property). The build script also emits `usize` mirrors of `N` and `MTP_WINDOW` as plain `usize` literals rather than via `usize::try_from(u64)` in a const block, because `TryFrom::try_from` is not yet const-trait-stable in rustc 1.95.0 (issue #143874); this keeps the workspace's `cast_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's `1_010_000`), `1_040_000` (selfish-mine attack regression, Shekyl ≠ canonical's `911_000`). Edge cases: genesis short-circuit across `chain_height ∈ 0..N` returns `GENESIS_DIFFICULTY`, the §5.3 step-1 boundary surfaces `Error::InvalidCount` on length mismatch, a non- monotonic cumulative-difficulty input surfaces `Error::Overflow`, both branches of the §5.3 step-8 overflow guard execute cleanly, the `solvetime[1] = -T` regression 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`, and `cargo fmt --package shekyl-difficulty -- --check` all pass. `cargo check --workspace` passes (the JSON authority extension does not affect existing consumers; `shekyl-engine-core/build.rs` continues to read only the FCMP/RCT keys it already consumed). ### Changed - **Stage 1 PR 5 — `Engine` parameterized over `P: PendingTxEngine` (fifth type parameter)** (`feat/stage-1-pr5-pending-tx-engine`, C6 = `0713591bf`; default `P = LocalPendingTx`). Orchestrator methods `build_pending_tx` / `submit_pending_tx` / `discard_pending_tx` / `outstanding_reservations` dispatch through `self.pending` rather than reading `Engine`'s former inline reservation map. `Engine::replace_pending_tx` (test-helpers) mirrors PR 4's `replace_refresh` pattern. - **Stage 1 PR 5 — `Engine::discard_pending_tx` reason-parameter drop at orchestrator boundary** (C6). The orchestrator-facing `discard_pending_tx(id)` no longer accepts `DiscardReason`; the trait surface retains `discard(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γ). `Reservation` gains `snapshot_id`, `extensions`, and collection-membership encoding (no `ReservationState` enum under segment 2h); `PendingTx` gains `snapshot_id`. Submit snapshot staleness returns `SubmitError::SnapshotInvalidated` with rich ids; terminal daemon failures emit `Discarded { DaemonRejectedTerminal }`; ambiguous failures emit `SubmitPendingResolution` and keep the reservation `in_flight`. - **Stage 1 PR 5 — `engine/pending.rs` free-function extraction** (C5β). Production paths live on `LocalPendingTx`; legacy `build_pending_tx_in_state` / `submit_pending_tx_in_state` / `discard_pending_tx_in_state` remain under `#[cfg(test)]` for migrated unit tests. - **Stage 1 PR 4 — `Engine` parameterized over `R: RefreshEngine` (fourth type parameter)** (`feat/stage-1-pr4-refresh-engine`, PR 4 C5a = `553d70139`; default `R = LocalRefresh` per the Round 4 turnkey-default discipline). `Engine` becomes `Engine` at [`engine/mod.rs`](../rust/shekyl-engine-core/src/engine/mod.rs). The orchestrator retry loop in [`engine/refresh.rs`](../rust/shekyl-engine-core/src/engine/refresh.rs) migrates from a free-function `produce_scan_result(...)` to trait dispatch on `R` via `self.refresh.produce_scan_result(...)` per PR 4 C5 = `7140f726a`; the legacy producer scaffolding (`produce_scan_result` free function + `ProduceError` + `ProgressEmitter` + duplicated helpers + constants) is deleted from `engine/refresh.rs` per PR 4 C5β = `b6a1274de`. The new `Engine::replace_refresh` test-only constructor (consume-and- rebuild; refactored at PR 4 C7 = `c9e65bbc6` from its initial `&mut self` setter form per PR 4 C6α = `e9310542a`) lets the `R` type parameter change between construction and replacement so test orchestration can build the engine with `LocalRefresh` at assemble time and rewire to `FaultInjecting` for failure-injection scenarios. `ViewMaterial::try_from_keys` at [`engine/view_material.rs`](../rust/shekyl-engine-core/src/engine/view_material.rs) derives the trait-required view-and-spend material from the `KeyEngine` at engine-assemble time, populating the `LocalRefresh` constructor argument. Crate-level public APIs consuming the engine type alias (`Wallet`, `WalletWithLedger` test helpers, `RefreshHandle`, the benchmark fixtures) thread the additional type parameters forward with appropriate defaults; no consumer outside the `shekyl-engine-core` crate is required to name `R` explicitly under the default-parameter discipline. - **Stage 1 PR 4 — `RefreshError::InternalInvariantViolation { context: &'static str }` variant addition** (PR 4 C3 = `c45894ffe`; Phase 0c amendment per [`docs/design/STAGE_1_PR_4_REFRESH_ENGINE.md`](completed/STAGE_1_PR_4_REFRESH_ENGINE.md) §5.4.7 R6 close-out). Resolves the Round 2 R6 "(a) extend `ConcurrentMutation` or (b) introduce `InternalInvariantViolation`" cleanup pin at the design layer. Disposition (b): conflating "wallet under sustained merge contention" and "wallet hit an internal bug" into `ConcurrentMutation` would deny downstream consumers (`PeerReputationActor`, telemetry, user-facing error surface) the structural distinction they need to respond correctly. The retry-loop call sites in [`engine/refresh.rs`](../rust/shekyl-engine-core/src/engine/refresh.rs) (per PR 4 C5 = `7140f726a`) and the `RefreshHandle::join` dropped-sender site surface state-machine invariant violations as `InternalInvariantViolation { context }` with compile-time-fixed developer content; `&'static str` is 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 three `RefreshError` variants reachable from a `RefreshEngine` impl's `Self::Error` via `Into` (alongside `Cancelled` unit and `Io(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 per `docs/design/ELECTRUM_WORDS_REMOVAL_PLAN.md` Phase 2 and `docs/design/ELECTRUM_WORDS_REMOVAL.md` substrate §2.4. Closes Phase 0 Mission Audit Lens B finding B-1 at the RPC layer (FFI + `wallet2` core 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 in `src/wallet/wallet_rpc_server*` plus `tests/`: - `restore_deterministic_wallet` JSON-RPC method + handler + `COMMAND_RPC_RESTORE_DETERMINISTIC_WALLET` request/response structs deleted. The method took a 25-word Electrum seed + optional `seed_offset` + `language` and reconstructed an account; Shekyl wallets restore from raw seeds via the `shekyl_account_generate_from_raw_seed` FFI surface (testnet/fakechain only per `rust/shekyl-crypto-pq/src/account.rs`'s permitted network/seed-format matrix), not from word lists. - `get_languages` JSON-RPC method + handler + `COMMAND_RPC_GET_LANGUAGES` request/response structs deleted. The method enumerated Electrum-word language packs that have no analogue in the Shekyl seed flow. - `language` request field + `is_valid_language(req.language)` validation branch + `wal->set_seed_language(req.language)` call removed from `COMMAND_RPC_CREATE_WALLET` and `COMMAND_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 send `language="English"` do not see a parse error — the value is dropped on the floor and `wallet2::generate()` runs with the wallet2 default seed language. The §4.3 hard-error discipline is enforced at the **FFI surface** (Phase 1 `wallet2_ffi_create_wallet` / `wallet2_ffi_generate_from_keys` reject non-empty `language` per `src/wallet/wallet2_ffi.cpp:309–320, 485–495`); the wallet-RPC handler reaches `wallet2::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 underlying `wallet2::set_seed_language` and `crypto::ElectrumWords::is_valid_language` symbols still exist (called from `wallet2_ffi.cpp` and `wallet2` internals); their full removal lands with the mnemonics module in Phase 5. - `seed` and `seed_offset` request fields + the entire seed-recovery branch (the `if (!req.seed.empty()) { words_to_bytes / decrypt_key / account.generate(...) / spend-key match check }` block at `wallet_rpc_server.cpp:2316–2366`) removed from `COMMAND_RPC_STOP_BACKGROUND_SYNC`. The branch was P0-broken on mainnet/stagenet under the legacy 3-arg `account.generate()` overload (constant-drift audit `docs/audit_trail/2026-05-ffi-constant-drift-audit.md`); password-only `stop_background_sync` survives unchanged. A BIP39 / raw-seed replacement is a `docs/FOLLOWUPS.md` V3.2 item, not Phase 2 scope. - `#include "mnemonics/electrum-words.h"` removed from `wallet_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_wallet` and 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 invokes `monerod` / `monero-wallet-rpc` binaries that don't exist in the Shekyl tree; (b) `functional_tests_rpc` and `check_missing_rpc_methods` were silently skipped in CI because the build environment lacked the `requests` / `psutil` / `monotonic` / `deepdiff` Python deps at `cmake` configure time — inherited dead code with no live caller; (c) `shekyl-wallet-rpc` lacks a `--regtest` / `--fakechain` flag 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. Per `15-deletion-and-debt.mdc`'s default-delete posture, deletion is the disposition. `add_subdirectory(functional_tests)` removed from `tests/CMakeLists.txt`; the Functional tests section of `tests/README.md` is rewritten to record the deletion + the planning posture for a Shekyl-native replacement. Build verification: `wallet_rpc_server`, `wallet`, `shekyld`, and `unit_tests` targets all build clean; `unit_tests` ctest pass (306s, 0 failures). - **Vestigial CLSAG-era `ring_size` field (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-side `ring_size` removal recorded above ("Decoy and `ring_size` removal 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`).** Deleted `ring_size` field + 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 via `KV_SERIALIZE_OPT(..., (uint64_t)0)` with zero readers in the post-FCMP++ codepath) and from one response struct (`transfer_description` inside `COMMAND_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–1505 `min(cd.sources[s].outputs.size())` walk that populated `desc.ring_size`; under FCMP++, `cd.sources[s].outputs.size()` does not represent a CLSAG ring and the computed value has no consensus meaning. Adjusted the `res.desc.push_back({...})` brace-init at L1471 to drop the corresponding `std::numeric_limits::max()` third element. **Blockchain logging (`src/cryptonote_core/blockchain.cpp`).** Removed the `ring_size` local at L3192 and reformatted the `MINFO` log line from `I/M/O` to `I/O` (inputs/outputs). Under FCMP++ the "M" (mixin / ring-member count) field pulled from `txin_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 the `ring_size` field from the `reference` struct, the corresponding constructor parameter (`uint64_t rs`), and updated the sole call site at L216 from `reference(height, txin.key_offsets.size(), n)` to `reference(height, n)`. The field was write-only across the utility's lifetime; the per-output frequency accounting at the loop's tail (L222–236) counts `out.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+ Legacy `wallet_rpc_server` Rust cutover) was selected because folding means vestigial `ring_size` ships 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 under `RCTTypeFcmpPlusPlusPqc`. ### 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: 1. **Error-taxonomy ambiguity** (RUST.md:776, PLAN.md:290). `ERR_CACHE_DERIVE_FAILED` and `ERR_INTERNAL` both claimed coverage of "Rust panic caught at FFI shim," making the taxonomy ambiguous for implementers. Resolved by assigning panics uniformly to `ERR_INTERNAL (-4)` via `catch_unwind` at the shim, while `ERR_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. 2. **Reviewer-rule misattribution** (RUST.md:934, FOLLOWUPS reviewer-discipline rules-queue entry). Both entries cited `.cursor/rules/06-branching.mdc` as the source of an "at least one reviewer who is not the author" rule. Verified against the file: `06-branching.mdc` governs 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.1 `24-reviewer-discipline.mdc` rules-queue entry is the *introducing* rule rather than a promotion of an existing one. 3. **`cncrypto` PUBLIC-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 misnamed `monero_fcmp_pp_crypto`. Re-ran the survey against the pinned tree: corrected `monero_fcmp_pp_crypto` → `fcmp_basic` (with `fcmp` as the second `src/fcmp/` target); added `src/blockchain_db/`, `src/checkpoints/`, `src/device/`, and `src/wallet/` (wallet_rpc_server) to the production-`src/` direct-consumer list; added `tests/wallet_bench/`, `tests/daemon_tests/`, `tests/functional_tests/` (two targets), `tests/hash/`, and `tests/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. 4. **Phase 3c / Phase 4 ordering hazard** (PLAN.md:338). Phase 3c deletes `slow-hash.c` / `rx-slow-hash.c` / `pow_cryptonight.cpp` together, but `src/cryptonote_basic/miner.cpp` still declares `slow_hash_allocate_state` / `slow_hash_free_state` `extern "C"` and `src/cryptonote_basic/cryptonote_format_utils.cpp` still calls `crypto::rx_slow_hash` and `crypto::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 the `miner.cpp` and `cryptonote_format_utils.cpp` call 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 that `cryptonote_format_utils.cpp`'s `cn_slow_hash` calls at lines 1465/1473 are non-PoW KDFs that need a Rust-side replacement before 3c — a Phase 4 deliverable. 5. **RPC-payments §15.4 incompleteness** (RUST.md:642). The deletion checklist omitted three surfaces: `src/rpc/core_rpc_ffi.cpp` (registers the six `rpc_access_*` JSON-RPC dispatch entries), `src/rpc/core_rpc_server_commands_defs.h` (defines the `COMMAND_RPC_ACCESS_*` request/response structs), and `tests/functional_tests/functional_tests_rpc.py` (includes `'rpc_payment'` in `DEFAULT_TESTS` at line 13). All three added to the checklist. 6. **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. 7. **Smaller items.** (a) `#[export_name]` CI grep extended in PLAN.md §2f to cover both bare and `#[unsafe(export_name = "...")]` forms, mirroring the existing `no_mangle` pattern; 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) -> u64` discretionary export reshaped to the same `i32`-return + out-parameter discipline as the committed hash export, per `40-ffi-discipline.mdc`. (f) FALLBACK.md §2's "`external/randomx-v2` is 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 via `python3 -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 from `docs/design/`. Verified each rewritten link resolves to a real path (29 OK; 2 intentional forward references: `external/randomx-v2` is added by Phase 1, `RANDOMX_V2_PHASE_3B_DELETED_CALL_AUDIT.md` by Phase 3b). - **PLAN.md Phase 2f C-ABI exports invariant**: the existing `extern\s*"C"\s*\{` grep matches only foreign import blocks, not the `pub extern "C" fn` shape 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 to `ERR_CACHE_DERIVE_FAILED`. The plan now records that OOM at cache derivation aborts and that a fallible-allocation path with an `ERR_CACHE_ALLOC_FAILED` taxonomy 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 `$HOME` path 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 `common` link (which sits below most subsystems and transitively re-exports `randomx_*` to everything depending on `common`) plus `cryptonote_basic` (two targets), `cryptonote_core`, `daemon`, and `fcmp` (two targets). Also recorded that `tests/crypto/CMakeLists.txt`'s `cncrypto-tests` does **not** link `cncrypto` directly (it links `common` and 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) to `RANDOMX_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_error` per Rust's default allocator; this is consistent with PLAN.md §2e's infallible-allocation choice. A future `ERR_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 in [`RANDOMX_V2_RUST.md`](./design/RANDOMX_V2_RUST.md) §1.4, but [`RANDOMX_V2_PLAN.md`](./design/RANDOMX_V2_PLAN.md) still carried the old Phase-2-gate framing in eight places: frontmatter `algorithm-review-gate` todo, frontmatter `overview` text, frontmatter `phase5-docs` todo, 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 with `MonAudit`/`MonDeploy` as 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`](./design/RANDOMX_V2_RUST.md) §16 gains a `const _: () = assert!(...)` compile-time assertion that `SEEDHASH_EPOCH_BLOCKS.is_power_of_two()`, because the `& !(SEEDHASH_EPOCH_BLOCKS - 1)` mask in the `seedheight()` 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 the `data` / `data_len` pairing so the `data == NULL && data_len == 0` empty-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 in [`FOLLOWUPS.md`](./FOLLOWUPS.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 the `rx-slow-hash.c` stateful 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). Rewrites [`RANDOMX_V2_RUST.md`](./design/RANDOMX_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`](./design/RANDOMX_V1_FALLBACK.md) §1 reframes the fallback as **late-binding** (any time between Phase 0 and release), unpin-to-`102f8acf` rather than stop-and-restart, with explicit Production-deployment-failure and Inheritance- failure trigger classes added to the existing list. Plan todo `algorithm-review-gate` rewritten 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). Rewrites [`RANDOMX_V2_RUST.md`](./design/RANDOMX_V2_RUST.md) §1 from a forward-looking "Shekyl-controlled divergence" framing to the empirical picture: the `Shekyl-Foundation/RandomX` fork tracks upstream `tevador/RandomX` without divergence; RandomX v2 is the upstream tevador algorithm landed in PR #317 (commit `bb6ed2c`); and the fork's pinned commit is `aaafe71` ("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's `doc/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's `audits/` 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 current `external/randomx` submodule is at v1-era `102f8acf` and Phase 1 adds `external/randomx-v2` at `aaafe71` as a **new** submodule alongside it (not a repoint) so the v1→v2 swap is a single reviewable commit later. [`RANDOMX_V1_FALLBACK.md`](./design/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's `audits/` directory at the pinned v1 commit. - **RandomX v2 Phase 0 — RPC-payments disposition resolved to delete** (`feat/randomx-v2-phase0-design`, 2026-05-16). Rewrites [`RANDOMX_V2_RUST.md`](./design/RANDOMX_V2_RUST.md) §15 from the open "rewrite or delete" question to an explicit **delete** decision with the rationale recorded (no users pre-genesis per `60-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 — five `rpc_payment*` files plus `wallet_rpc_payments.cpp` plus a functional test deleted whole, surgical hook removal across `core_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`](./design/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). Expands [`RANDOMX_V2_RUST.md`](./design/RANDOMX_V2_RUST.md) with new sections §16 (genesis-block seedhash handling, including the `rx_seedheight` early-block branch and a canonical Rust `seedheight()` form), §17 (FFI error-code taxonomy with stable negative codes), §18 (thread- safety contract for `shekyl_pow_randomx_v2_hash`), §19 (`block.major_version` field 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/` vs `tests/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 to `wallet_rpc_payments.cpp:156/158/163`. [`RANDOMX_V1_FALLBACK.md`](./design/RANDOMX_V1_FALLBACK.md) §2 records the upstream-`tevador/RandomX`-vs-Shekyl-fork v1 source choice and the `BUILD_RANDOMX_V2_MINER_LIB` rename, §4 fixes the cross-reference to `RANDOMX_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_address` pair** (`chore/stage-1-pr3-closeout`, 2026-05-12). Introduces the criterion + iai-callgrind sibling pair for the `KeyEngine::account_public_address` trait method, classified under the `engine_trait_bench_*` threshold class via `compare.py`'s `classify()` function-name routing. The fixture is `Box` rather than the canonical `(Box>, TempDir)` shape per the substrate-forced divergence documented in [`STAGE_1_PR_3_CLOSEOUT_PREFLIGHT.md`](./completed/STAGE_1_PR_3_CLOSEOUT_PREFLIGHT.md) §1.2 — `Engine` does not yet hold a `LocalKeys` field; orchestrator integration is `KeyEngine` PR-5 territory per [`STAGE_1_PR_3_KEY_ENGINE.md`](./completed/STAGE_1_PR_3_KEY_ENGINE.md) §2.1.1 (Round 4a workflow-shape pivot). Workload class is **trivial pure-read** (cached `AccountPublicAddress` borrow); iai-callgrind is the load-bearing signal because criterion `median_ns` reflects optimizer amortization across the iteration loop (§4.4 hoisting caveat). The bench-internals visibility expansion adds only `LocalKeys` to the `pub` surface (following the exact precedent of `LocalLedger` at Stage 1 PR 2 — name-only expansion; fields stay private). `LocalKeys::from_test_seed` becomes `pub` under `#[cfg(any(test, feature = "bench-internals"))]` matching `LocalLedger::populate_for_bench`'s gating. `AccountPublicAddress` stays `pub(crate)` — the bench helper returns a primitive `usize` summary (sum of address-field byte-lengths) rather than the natural `&AccountPublicAddress` return type, sidestepping the API-widening Copilot's PR review flagged. Closes two of four deferred-bench slots from [`FOLLOWUPS.md`](./FOLLOWUPS.md)'s Stage-1-performance-baseline entry (`ledger_balance` previously satisfied at Stage 1 PR 2, `key_account_public_address` here); 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's `Zeroizing<…>`-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's `AssertionSink` / 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 like `SnapshotId`, `ReservationId` + version, `BlockHeight`) and at the lint level (V3.1+: scope-extending the FOLLOWUPS F5 entry to a unified `diagnostic_consumer_discipline` lint covering both recursive-trust-boundary and cross-emitter-ordering misuse sub-scopes). PR 5 §5.0.3 carries the parallel amendment. **F13 (`SuppressedRateLimit` field-shape pin; meta-review of F6).** F6 added the `SuppressedRateLimit` variant 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 carries `class: SuppressedClass` only, where `SuppressedClass` is 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's `SuppressedClass` enum 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 under [`16-architectural-inheritance.mdc`](../.cursor/rules/16-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 (`SuppressedRateLimit` emission-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 one `SuppressedRateLimit { class }` per class per attempt" — the producer's per-attempt `emit_state` carries a per-class `notice_emitted: bool` latch, 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 via `ConcurrentMutation`-driven retries) is bounded at the orchestrator's existing retry-loop policy layer; no producer-side state survives across attempts (the zeroization scope for `ViewMaterial` and `Scanner` forecloses 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 if `recover_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_discipline` lint 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 or `clippy` lint 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. - **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 FOLLOWUPS `PersistenceConsumerActor` entry 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 pins `AssertionSink` / `PanickingSink` as 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 a `RefreshDiagnostic::SuppressedRateLimit` variant); 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 for `TracingDiagnosticSink` and 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 the `SuppressedRateLimit` variant + per-class projections + 7th contract pin; C4 carries the per-transaction inner cancellation check + producer-side per-class emission rate budget enforcement; C7 carries `AssertionSink` / `PanickingSink` as 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 under [`16-architectural-inheritance.mdc`](../.cursor/rules/16-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 on [`STAGE_1_PR_4_REFRESH_ENGINE.md`](./completed/STAGE_1_PR_4_REFRESH_ENGINE.md) per 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 confirms `DaemonOp` two-variant and `ProtocolErrorKind` five-variant refresh-reachable subset against the producer's actual call sites. §6 review checklist fills in following PR 5's shape (binding-check matrix against `V3_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 + `ViewMaterial` type; C2 `RefreshDiagnostic` + `DiagnosticSink` + Stage 1 sink impls; C3 `RefreshError::InternalInvariantViolation` variant addition; C4 `LocalRefresh` aggregate + producer-body migration; C5 `Engine` parameterization + retry-loop call-site migration + `RpcError` classification; C6 `MockRefresh` test substrate + `replace_refresh`; C7 hybrid retry test + `AssertionSink` / `PanickingSink` property 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 per [`STAGE_1_PR_5_PENDING_TX_ENGINE.md`](./completed/STAGE_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 on [`STAGE_1_PR_4_REFRESH_ENGINE.md`](./completed/STAGE_1_PR_4_REFRESH_ENGINE.md). PR 5 Round 1's disposition under the actor-mesh framing (per [`STAGE_1_PR_5_PENDING_TX_ENGINE.md`](./completed/STAGE_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) and `LocalSigner` (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)'s `Signer` trait substrate as the V3.x migration target — the R4 (c) migration becomes *"`Scanner` stops holding spend material; delegates key-image generation via the existing `Signer` trait"* rather than designing the split from scratch, shrinking the V3.x cost to a producer-side shape change (no architectural change); (3) the `REFRESH_DIAGNOSTIC_STREAM.md` → `DIAGNOSTIC_STREAM.md` rename housekeeping was already covered by PR 5 segment 2g — no PR 4 doc references remain to sweep (confirmed by `rg`); (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 in [`16-architectural-inheritance.mdc`](../.cursor/rules/16-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. - **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 per [`STAGE_1_PR_3_CLOSEOUT_PREFLIGHT.md`](./completed/STAGE_1_PR_3_CLOSEOUT_PREFLIGHT.md): - **A1 commit (mechanical past-tensing sweep)**: reconciled 17-reference enumeration across [`STAGE_0_HARNESS.md`](./completed/STAGE_0_HARNESS.md), [`STAGE_1_PR_1_DAEMON_ENGINE.md`](./completed/STAGE_1_PR_1_DAEMON_ENGINE.md), and [`STAGE_1_PR_2_LEDGER_ENGINE.md`](./completed/STAGE_1_PR_2_LEDGER_ENGINE.md) to 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)**: `TransferDetails` field removal structurally complete; M3-series naming sweep complete (preserved-as-history or false-positive); `42-serialization-policy.mdc` stale globs closed in M3e. Three clean-as-found invariants from PR #40's audit pass. - **Stage 1 PR 3 — M3e: documentation realignment to post-M3d architecture** (`feat/stage-1-pr3-m3e`; six commits cut off `dev` post-M3d, landing the four logical units planned at amendment-cycle time per [`STAGE_1_PR_3_M3E_PREFLIGHT.md`](./completed/STAGE_1_PR_3_M3E_PREFLIGHT.md) §4: the "preflight + review-response + amendment" logical unit landed across three actual commits — original preflight at `82693bab7`, forward-templates capture at `4b931b1b5`, amendment at `1f9a7ad59` — followed by three substantive commits at `8e6780062` / `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 of `TransferDetails` per [`docs/completed/STAGE_1_PR_3_MIGRATION_PLAN.md`](./completed/STAGE_1_PR_3_MIGRATION_PLAN.md) §3.5 (M3e — documentation realignment-of-the-whole) and [`docs/completed/STAGE_1_PR_3_M3E_PREFLIGHT.md`](./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.md`](./completed/STAGE_1_PR_3_KEY_ENGINE.md) carries 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; see ]` or `[Remains open / Forward-looking record]` while preserving original framing as historical record. [`V3_ENGINE_TRAIT_BOUNDARIES.md`](./V3_ENGINE_TRAIT_BOUNDARIES.md) replaces the pre-migration `KeyEngine` trait block with the post-M3 4-method shape (`account_public_address`, `derive_subaddress`, `try_claim_output`, `sign_transaction` per `rust/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 to `sign_with_spend` → `sign_transaction` while preserving the "Round 2 dispositions" section's original Q9.1/Q9.2/Q9.3 framings as historical record. [`MIGRATION_AUDIT.md`](./completed/STAGE_1_PR_3_MIGRATION_AUDIT.md) gains a post-M3 status banner clarifying that the audit's commit hashes (`ffcaa62e9` and `e6efaf5b5`) 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 old `KeyEngine` method 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 (per [`STAGE_1_PR_3_M3E_PREFLIGHT.md`](./completed/STAGE_1_PR_3_M3E_PREFLIGHT.md) §11.1) and swept inline rather than deferred. - **Rules realignment.** [`.cursor/rules/42-serialization-policy.mdc`](../.cursor/rules/42-serialization-policy.mdc) underwent a mechanical rename of all 11 stale crate-path references (`shekyl-wallet-state` → `shekyl-engine-state`; `shekyl-wallet-file` → `shekyl-engine-file`) across the `globs` frontmatter, intro paragraph, pairing table, mechanical enforcement subsections, and procedure section. The stale `globs` field 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 in [`FOLLOWUPS.md`](./FOLLOWUPS.md)). - **FOLLOWUPS structuring.** [`FOLLOWUPS.md`](./FOLLOWUPS.md) gains 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 2 `KeyEngine`-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 in `15-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`); the `V3_WALLET_DECISION_LOG.md` intro paragraph only (dated entries preserved per append-only discipline); and the `WALLET_REWRITE_PLAN.md` current-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`](./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`](./FOLLOWUPS.md) (8 occurrences across historical audit-trail entries plus the new M3e entries that reference the rename event); [`V3_WALLET_DECISION_LOG.md`](./V3_WALLET_DECISION_LOG.md) (16 occurrences across dated decision-log entries protected by the file's append-only discipline); [`WALLET_REWRITE_PLAN.md`](./design/WALLET_REWRITE_PLAN.md) (6 occurrences inside PR-0.X historical descriptions); [`shekyl_rust_v0.json`](./benchmarks/shekyl_rust_v0.json) (10 occurrences; captured baseline pinned to `git_rev` anchor `a2bf417e4b7985ed2097dc5d3fb53affef306d1a`); and [`shekyl_rust_v0.iai.snapshot`](./benchmarks/shekyl_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 `rg` invocation and by surface during the M3e preflight's D5 audit. ### 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 off `dev` post-M3c). Activates the **"secrets confined to engine" property** for the orchestrator/engine boundary per [`docs/completed/STAGE_1_PR_3_MIGRATION_PLAN.md`](./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`](./completed/STAGE_1_PR_3_M3D_PREFLIGHT.md) §3.3, and the audit migration table at [`docs/completed/STAGE_1_PR_3_MIGRATION_AUDIT.md`](./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>` fields deleted from `shekyl_engine_state::TransferDetails`: `combined_shared_secret` (64 bytes), `ho`, `y`, `z`, `k_amount` (32 bytes each). Corresponding entries in the `TransferDetailsSchema` mirror struct, the `impl Zeroize for TransferDetails` block, and the `rust/shekyl-engine-state/.zeroize-allowlist` schema-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. The `wallet_ledger.rs` docstring is the authoritative in-source statement of the pairing rule ("Each per-block bump implies a `WALLET_LEDGER_FORMAT_VERSION` bump") — the workspace-wide rule `.cursor/rules/42-serialization-policy.mdc` still carries pre-rename `shekyl-wallet-state` / `shekyl-wallet-file` path references (tracked as a focused FOLLOWUP for path-rename realignment). Per the workspace's `15-deletion-and-debt.mdc` "no in-Shekyl migration code" rule, v4 stores refuse v3 loads rather than migrate; pre-genesis users `rm -rf ~/.shekyl` and re-sync. - **Property activated:** orchestrator-side `TransferDetails` no longer carries derived per-output secrets. The engine re-derives them inside its signing-session boundary from `(view_secret, source_ciphertext)` via `LocalKeys::derive_source_secrets_bundle` (per `STAGE_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 `.snap` files that transitively serialize `TransferDetails` (`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. = Some(Zeroizing::new(...))` write lines at `shekyl-scanner/src/ledger_ext.rs::process_scanned_outputs` deleted; the M3b deterministic-handle pathway (`source_ciphertext`, `output_handle` populated by `engine::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`, four `benches/*.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 populates `source_ciphertext` (via direct `HybridCiphertext` construction) and `output_handle` (via `shekyl_crypto_pq::handle::derive_output_handle`) so `Option`-valued roundtrip / snapshot benches continue exercising non-default payloads representative of post-M3d transfers. The `postcard_roundtrip_with_secrets` test was renamed to `postcard_roundtrip_with_handle_fields`. - **Documentation cleanup (carve-out per `91-documentation-after-plans.mdc`):** the past-tensing edits to `STAGE_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), and `docs/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_output` cleanup 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 two `shekyl_crypto_pq` imports under `bench-internals` in the engine-core common bench fixture after `cargo clippy --all-targets` surfaced them as unused when included from the default-feature `synced_height` bench 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 off `dev` at `ea1df2539`). Lands the validation milestone per [`docs/completed/STAGE_1_PR_3_MIGRATION_PLAN.md`](./completed/STAGE_1_PR_3_MIGRATION_PLAN.md) §3.3 (with §3.3.1 cross-reference to the implementation disposition) and the pre-flight in [`docs/completed/STAGE_1_PR_3_M3C_PREFLIGHT.md`](./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 the `tx_builder::sign_transaction` surface** — the precondition M3d depends on for removing the legacy `TransferDetails`-secret-bearing-fields fallback. - **New unit test:** `engine_derived_bundle_signs_through_tx_builder_end_to_end`, inline in `rust/shekyl-engine-core/src/engine/local_keys.rs`'s `mod tests` as a peer to M3b D5. Constructs a `LocalKeys` from `TEST_SEED`; for each of 9 fixtures (3 input counts {1, 2, 3} × 3 subaddress indices {`PRIMARY`, `SubaddressIndex::new(1)`, `SubaddressIndex::new(42)`} — `SubaddressIndex` is a flat `u32`, not a `(major, minor)` pair) synthesizes *n_in* outputs paid to `subaddress_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 via `scan_output_recover` to compose a hand-derived legacy bundle; derives the engine bundle via `LocalKeys::derive_source_secrets_bundle`; asserts engine and legacy `SpendInput`s are byte-identical field-by-field at the input layer (12 fields per input including per-`leaf_chunk`- entry equality); calls `tx_builder::sign_transaction(...)` *once* on the engine path; asserts BP+ deserializes via `Bulletproof::read_plus` and verifies via `Bulletproof::verify` against un-cofactored output commitment points; asserts FCMP++ verifies via `shekyl_fcmp::proof::verify` against engine-derived key images, the proof's pseudo-outputs, the synthetic h_pqc Selene scalars, the synthetic single-leaf-chunk tree root, and the same `signable_tx_hash` passed to the prover; asserts `reference_block` and `tree_depth` echo unchanged. - **Inline cryptographic tree-fixture helpers.** Replicates the recipe from `shekyl-fcmp::proof::tests::prove_verify_roundtrip` inline in `local_keys.rs::tests` (single-leaf chunk; depth = 1; `tree_root = SELENE_HASH_INIT + multiexp_vartime` over Selene generators × leaf scalars; h_pqc derived deterministically via `dalek_ff_group::FieldElement::wide_reduce` for reproducibility; recipient `output_index` offset by `n_in + 100` to avoid the input/output commitment-mask collision that collapses FCMP++'s rerandomization scalar to zero in single-input/single-output sweeps with shared `combined_ss`). Helpers: `build_synthetic_single_chunk_tree_root`, `make_synthetic_h_pqc_bytes`, `make_recipient_output_info`, `compute_test_key_image`. New `[dev-dependencies]` on `shekyl-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_core` per `17-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_transaction` trait method (PR-5+ scope; today returns `KeyEngineError::SignTransactionTraitSurfaceIncomplete` because `TxToSign`'s `outputs` and `fcmp_plus_plus_context` are 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 `SpendInput`s for `commitments` / `enc_amounts` byte-equality at the signer-output layer. Pre-flight review surfaced that `SpendInput` byte-equality at the input layer is strictly stronger (subsumes the original property by signer determinism, and additionally guards regressions in `SpendInput` fields 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 of `tx_builder::sign_transaction` end-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.rs` only validation-error paths; `transfer_e2e[_iai].rs` benches explicitly elide full sign pending a checked-in tree-fixture; `shekyl-fcmp::proof::tests::prove_verify_roundtrip` exercises 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 of `tx_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 same `KeyEngine`-widens-to-`pub` trigger (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). - **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 off `dev` at `647f82d59` on 2026-05-09). Lands the `KeyEngine`-mediated source-secrets derivation path per [`docs/completed/STAGE_1_PR_3_MIGRATION_PLAN.md`](./completed/STAGE_1_PR_3_MIGRATION_PLAN.md) §3.2 and the pre-flight dispositions in [`docs/completed/STAGE_1_PR_3_M3B_PREFLIGHT.md`](./completed/STAGE_1_PR_3_M3B_PREFLIGHT.md) §2 / §3 / §5. Property delivery: **partial** — every output the scanner ingests now carries a deterministic `OutputHandle` and the `HybridCiphertext` it was decapsulated from on its `TransferDetails`; the legacy secret-bearing `TransferDetails` fields 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` (Layer 1, transform-shaped, in `shekyl-crypto-pq`) extracts the X25519 + ML-KEM-768 + HKDF-SHA-512 re-decap chain from `scan_output_recover`'s prefix; `LocalKeys::derive_source_secrets_bundle( source_ciphertext, output_index, subaddress_idx) -> Result` (Layer 2, state-shaped, in `shekyl-engine-core::engine::local_keys`) composes Layer 1's output with the engine-owned `b` (spend secret) and `m_i` (subaddress derivation scalar). Placement per `18-type-placement.mdc`: transform-shaped lives with its function; state-shaped lives with its owner. - **`TransferDetails` schema extension (D3).** Two `Option<…>` fields — `source_ciphertext: Option` (the on-chain hybrid X25519 + ML-KEM-768 ciphertext the scanner detected) and `output_handle: Option` (the deterministic 16-byte handle from cSHAKE256 keyed by the view secret). `Zeroize` impl skips the new non-secret fields per `35-secure-memory.mdc`'s redaction discipline. Both fields land behind an `Option` so the bridge-impl fallback is feature-detected (presence of `source_ciphertext` ↔ primary path; `None` ↔ legacy field path). `LEDGER_BLOCK_VERSION` and `WALLET_LEDGER_FORMAT_VERSION` bumped 2 → 3; both schema snapshots regenerated. The new fields' wire stability is locked by extending the `postcard` round-trip test; `postcard-schema = "0.2"` added as a direct dep on `shekyl-crypto-pq` per `17-dependency-discipline.mdc` (matches the existing `shekyl-engine-state` direct-dep pin). - **`TxInputSigningContext` field swap (D2).** Drops `source_secrets: SourceSecretsBundle` (the by-value secret carrier that contradicted the engine-confined-secrets property) in favor of `source_ciphertext: HybridCiphertext` + `output_index: u64`. The trait-surface input is now the public on-chain ciphertext; the engine derives the secrets internally via `LocalKeys::derive_source_secrets_bundle`. `Debug` impl simplified; redaction tests updated. - **Engine post-pass at the orchestrator layer (Q2 δ disposition).** `Engine::apply_scan_result` becomes a three-step body inside one `LocalLedger` write guard: `collect_detection_residue` (pre-collects a `HashMap<(tx_hash, internal_output_index), HybridCiphertext>` from the `ScanResult`'s new transfers) → `apply_scan_result_to_state` (the existing sync bookkeeping merge, unchanged) → `populate_engine_handle_fields` (walks the freshly-merged `TransferDetails` and binds each to its `source_ciphertext` + deterministic `output_handle` from 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 via `shekyl_crypto_pq::handle::derive_output_handle` (a synchronous pure function that requires only `(view_secret, tx_hash, output_index)`); M3c+ wires `LocalKeys` onto `Engine` and re-routes the helper through `KeyEngine::try_claim_output`, at which point the helper signature becomes `async fn` and `Engine::apply_scan_result` takes 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.** `RecoveredWalletOutput` extended 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 revealed `RecoveredWalletOutput` was discarding the on-chain residue at construction time. Reordered to land before the engine post-pass commit so each commit leaves the workspace `cargo 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 inner `CryptoError` so 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_chain` asserts field-by-field byte-equality between the new Layer 2 chain (`derive_source_secrets_bundle`) and a hand-rolled bundle from `scan_output_recover`'s `RecoveredOutput` across 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_seeds` exercises 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 in `local_keys.rs::tests` rather than the pre-flight's planned `tests/byte_identical_derivation.rs` integration test, due to the M3a Round 4a `pub(crate)` lock on `LocalKeys`, `SourceSecretsBundle`, and `KeyEngineError`; tracked for re-location in `docs/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 `TransferDetails` schema bumps `WALLET_LEDGER_FORMAT_VERSION` from 2 to 3, which is a wallet-state schema change handled by the pre-V3-launch `rm -rf ~/.shekyl` migration path per `15-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). - **Stage 1 PR 3 — Phase 0: `AllKeysBlob` zeroize-discipline realignment** (`chore/allkeysblob-zeroize-realignment`; closes [`docs/completed/STAGE_1_PR_3_KEY_ENGINE.md`](./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 new `MlKem768DecapKey` typed newtype in [`rust/shekyl-crypto-pq/src/keys.rs`](../rust/shekyl-crypto-pq/src/keys.rs) that mirrors the established `ViewSecret` / `SpendSecret` shape (`#[repr(transparent)]`, `Clone + Zeroize + ZeroizeOnDrop`, no `Copy`, no `Debug`, `pub(crate)` constructor, public `as_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 by `account_ffi::tests::struct_layout_matches`. The producer [`crate::account::ml_kem_keypair_from_d_z`] returns the typed `MlKem768DecapKey` directly (constructed via `from_zeroizing` consuming a `Zeroizing<[u8; N]>` source) — the secret travels through the type system from producer to consumer without any call site materialising an untracked stack `Copy` of the 2400-byte buffer between them. - **F2 / `35-secure-memory.mdc:23–25`.** `AllKeysBlob` migrated from a hand-written `Drop` impl (which the design doc itself characterized as "documenting the lie" — the spec asserted `AllKeysBlob: ZeroizeOnDrop` while the trait was not implemented) to `#[derive(Zeroize, ZeroizeOnDrop)]`. With every field now `Zeroize`-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 derived `Drop::drop` calls `self.zeroize()` once on every field; field-drop-glue then re-invokes each `ZeroizeOnDrop` field's destructor independently — an idempotent double-wipe documented in the struct rust-doc so future `ZeroizeOnDrop`-grep audits do not mistake the pattern for a discipline violation. - **F3 / `KEY_ENGINE.md` §7.5.** `AllKeysBlob: Clone` derive deleted. Workspace audit (`rg 'AllKeysBlob.*\.clone\(\)'` + per-call-site read; `cargo build --workspace --all-targets` is the locking gate that compiles every `#[cfg(test)]` block) surfaced zero callers in production *or* test code; per `30-cryptography.mdc` and `35-secure-memory.mdc:26-28`, `Clone` on a secret-bearing struct requires explicit justification, and none surfaced. The `traits/key.rs:581` doc-comment ("Not Clone — implementors wrap `AllKeysBlob`") becomes literally enforced. **`ml_kem_ek` deliberately stays raw `[u8; ML_KEM_768_EK_LEN]`.** Public encap key, broadcast in the address; outside `35-secure-memory.mdc:21–22`'s reach as public material. Wrapping it would be uniformity-driven completionism without rule grounding (per `15-deletion-and-debt.mdc`'s "while we're here is the enemy") and would create a permanent type-system signal collision (`Zeroize` semantics on a public type as a distractor for any future grep-for-secrets audit). Five-reason disposition recorded inline at [`docs/completed/STAGE_1_PR_3_KEY_ENGINE.md`](./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 `AllKeysBlob` carried raw `[u8; N]` fields (where `derive(ZeroizeOnDrop)` would have been a literal one-line addition). The intervening `chore/allkeysblob-typed-wrappers-monero-sweep` (which closed the inheritance audit's `spend_sk` / `view_sk` secret-flow finding) left `ml_kem_dk` as 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: ZeroizeOnDrop` literally 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 `AllKeysBlob` zeroize discipline, restoring the precondition that Q9.3 / Phase 0d's cross-reference language now resolves cleanly against. M3b cuts off the post-merge `dev` tip with the precondition true. - **Single-source-of-truth JSON authority for the consensus-affecting constant subset: `config/consensus_constants.json`.** Mirrors the existing `config/economics_params.json` pattern. The JSON is the authority; `cmake/generate_consensus_constants.py` emits `shekyl/consensus_constants_generated.h` for the C++ build, and `rust/shekyl-engine-core/build.rs` reads the same file and emits a `consensus_constants_generated.rs` module that `rust/shekyl-engine-core/src/multisig/v31/intent.rs` consumes via `include!()`. 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 as `5` in `src/cryptonote_config.h` and as `10` in `rust/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 RCTType` in C++; `ProofType::FcmpPlusPlusPqc => 7` in `shekyl-oxide`); both sides now stamped against the JSON via `static_assert` (C++ in `src/fcmp/rctTypes.cpp`) and a runtime test (Rust, in `intent.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 a `const _: () = 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_fresh` changed from `tip = 905` (age = 5, the boundary value `age < 5` evaluates false under the post-fix `MIN_AGE = 5`) to `tip = 903` (age = 3, unambiguously rejected). The test exercises the rejection branch (`age < MIN_AGE`) and stays correct as long as `MIN_AGE > 3` — i.e. it survives any tightening (`MIN_AGE` increasing above 5) and any loosening down to and including `MIN_AGE = 4`. Only a loosening to `MIN_AGE = 3` or lower would invalidate the fixture, which itself would warrant the consensus re-review the sentinel demands. **Out of scope:** `ADDRESS_VERSION_V1` is single-source in Rust with no C++ duplicate, so there's nothing to align. The full-migration follow-up for the remaining `SHEKYL_*` 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.x `TimeoutResolverActor` correlation contract. - **Findings A + E + H — `SnapshotId` hash 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 pinned `SnapshotId` to SHA-256 via `sha2 = "0.10"`, citing `rust/shekyl-engine-core/Cargo.toml` line 115 as the workspace-state-reuse anchor. That citation was a dependency-discipline error: [`Cargo.toml`](../rust/shekyl-engine-core/Cargo.toml) line 115 is in `[dev-dependencies]` (test-only), and the production `sha2` at line 33 is `optional = true` (gated behind a feature flag). The Copilot-fix follow-up switches the primitive to `shekyl-crypto-hash::cn_fast_hash` (Keccak-256, original padding) — `shekyl-crypto-hash` is an unconditional `[dependencies]` entry per Cargo.toml line 28, the consensus-audited Keccak primitive Shekyl already uses throughout its codebase. Strictly better disposition against the [`17-dependency-discipline.mdc`](../.cursor/rules/17-dependency-discipline.mdc) workspace-state reuse rule against the actual production-dependency graph. The §5.4 R2 sketch also still showed a prior `blake3::hash` form from segment-2d's open-shape-not-primitive disposition; the sketch is updated to the binding `cn_fast_hash` form. 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 — `SnapshotId` is 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 `LedgerSnapshot` merely makes the wallet submit a tx valid against the prior snapshot; the daemon could have rejected the tx anyway via `DoubleSpend` if 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. *(Since superseded by SA-3c, 2026-08-11: the digest is now cSHAKE256 with the domain as the customization, `b"shekyl/snapshot-id-v1"` — see the V3_WALLET_DECISION_LOG SA-3c entry.)* 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-checklist `SnapshotId` item, 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) — rename `REFRESH_DIAGNOSTIC_STREAM.md` → `DIAGNOSTIC_STREAM.md`**. The cross-reference is updated to the chosen doc name with the closure rationale. - **Finding F — `&dyn DiagnosticSink` vs `Arc` inconsistency.** §5.0.2.1 (the segment-2f sink-binding-closure section) used the earlier `&dyn DiagnosticSink` form when the closure section itself pins `Arc`; this is corrected for self-consistency. The Round-1-close CHANGELOG entry's `&dyn DiagnosticSink` description receives a forward-pointer noting that segment 2f tightened the form to `Arc` for reference-shape ergonomics during R11 closure (the earlier wording remains historically accurate at Round 1 close). - **Findings C + D — V3.x FOLLOWUPS `SubmitFailureAnalyzer` variant-name alignment.** The `SubmitFailureAnalyzer` FOLLOWUPS entry referenced `SnapshotInvalidated` in two places and `SubmitFailed { kind: Timeout }` in one place; the binding variant names per segment 2f / Phase 0a / Phase 0f are `SubmitSnapshotInvalidated` and `SubmitFailed { 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 — `TimeoutResolverActor` chain-observation correlation contract architectural mismatch.** The `TimeoutResolverActor` FOLLOWUPS entry described subscribing to `LedgerDiagnostic::SnapshotMerged` to observe whether the timed-out `tx_hash` landed on chain — but `SnapshotMerged` is pinned by Phase 0g as `{ new: SnapshotId, prior: SnapshotId, height: BlockHeight }` and carries no `tx_hash` field, 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 additive `LedgerDiagnostic` variant carrying tx-confirmation payloads, or (ii) an additive `LedgerEngine` chain-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; the `LedgerEngine` and `LedgerDiagnostic` surfaces have their own additive- extension discipline that the consumer-actor PR composes against. Wallet-correctness is preserved by R8's `ReservationTTLActor` safety net regardless of when `TimeoutResolverActor` lands. - **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 `ReservationTTLActor` subscription contract incomplete (raised against 2f177a0c3, line 987 of design doc).** Segment 2e's R8 closure named only `PendingTxDiagnostic::BuildSucceeded` as the actor's subscription, with no terminal events. This would leak closed reservations into the actor's in-memory age-tracking map indefinitely, producing stale `ReservationOutstanding` warnings on already-terminated reservations and spurious `AutoDiscardMessage` round-trips to `PendingTxActor`. **Fix**: §5.4 R8 prose expanded with a full subscription contract section pinning `BuildSucceeded` (insert), `SubmitSucceeded` (remove — terminal success), and `Discarded` (remove regardless of `reason` — covers all four `DiscardReason` variants including the segment-2f `DaemonRejectedTerminal` and the segment-2e `TTLAutoDiscard` self-cleanup). Explicit "what `SubmitFailed` does *not* close" note per segment-2f R9's two-stage submit-flow + Finding-2 daemon-side authority disposition: `SubmitFailed` on `DaemonTimeout` / `DaemonUnavailable` keeps the reservation in `SubmitPendingDaemonAck` and the actor keeps tracking. Memory-bound property pinned: actor's map size is bounded by `PendingTxActor::outstanding()`, not by cumulative reservation count. - **Finding 3 — FOLLOWUPS `ReservationTTLActor` entry 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 (`ReservationTTLActor` entry subscription- contract subsection); docs/CHANGELOG.md (this entry + forward-pointer note on the Round 1 close entry). No code changes; no test impact. - **Stage 1 PR 5 — Round 2 segment 2g (Round 2 close-out: §4 Phase 0 binding-form enumeration; `SnapshotId` hash primitive pin; §5.0.3 diagnostic-stream-doc generalization closure; §6 review checklist filled).** Doc-only commit on `feat/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 (`SubmitError` and `SubmitErrorKind` enums per segment 2f); Phase 0b (`SnapshotId` opaque type with binding hash primitive — see below); Phase 0c (REMOVED at the trait surface per segment 2d's R12 (a) closure); Phase 0d (`Reservation` struct shape with `extensions: Vec` per segment 2b R14); Phase 0e (reservation-lifecycle prose with R5 / R9 segment-2f / R10 closure cross-references); Phase 0f (`PendingTxDiagnostic` enum + constructor-bound `DiagnosticSink` per segment-2f §5.0.2.1); Phase 0g (`LedgerDiagnostic::SnapshotMerged` deferred to consumer-PR per segment-2g introduction-PR disposition — avoids speculative-introduction-without-consumer violation of the [`15-deletion-and-debt.mdc`](../.cursor/rules/15-deletion-and-debt.mdc) no-live-caller rule); **four new Phase 0 candidates** from segment-2b / segment-2c residual closures: Phase 0h (`Signer` trait surface per R11 (b) segment-2b closure); Phase 0i (`OutputSelector` trait surface per R13 segment-2c closure); Phase 0j (`FeeEstimator` trait surface + `FeePriority` enum per R16 segment-2c closure with segment-2d V3.0-lift evaluation); Phase 0k (`SubmissionStrategyActor` topology slot per R15 segment-2c closure — V3.x introduction; no V3.0 trait amendment). **`SnapshotId` hash primitive pinned** as Keccak-256 via `shekyl-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"`). *(Since superseded by SA-3c, 2026-08-11 → cSHAKE256 with the domain as the customization; see the V3_WALLET_DECISION_LOG SA-3c entry.)* *(Forward-pointer: the Copilot-fix follow-up entry below revised this binding from segment-2g's prior `sha2`-based form to the Keccak-based form. The prior `sha2` citation referenced `rust/shekyl-engine-core/Cargo.toml` line 115, which is in `[dev-dependencies]` and therefore unavailable to production code; the production `sha2` at line 33 is `optional = true`. `shekyl-crypto-hash` is the consensus-audited Keccak primitive already unconditional in `shekyl-engine-core` production deps at line 28 — the strictly better dependency-discipline disposition.)* Selection rationale (revised form): `shekyl-crypto-hash` is an unconditional `[dependencies]` entry per [`17-dependency-discipline.mdc`](../.cursor/rules/17-dependency-discipline.mdc) workspace-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 because `SnapshotId` is a wallet-internal token that does not cross the wire. **§5.0.3 diagnostic-stream-doc generalization closure**: option (a) — rename `REFRESH_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 for `RefreshDiagnostic` / `PendingTxDiagnostic` + `DiscardReason` / `LedgerDiagnostic` pending the consumer-actor PR). Option (b) — parent `DIAGNOSTIC_STREAM_CONTRACTS.md` factoring — 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 additions `S: Signer`, `O: OutputSelector`, `F: FeeEstimator`); test-substrate preservation list (`AssertionSink` / `PanickingSink` property-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` + `SubmitErrorKind` enum pins; sink-binding constructor-bound closure for Finding 4).** Doc-only commit on `feat/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 internal `ReservationState` machine (`Active | SubmitPendingDaemonAck | Resolved`); trait surface unchanged (`outstanding()` counts `Active + SubmitPendingDaemonAck`). Self-continuation message pattern pinned: `PendingTxActor` defers reply until `SubmitCompleted` self-message arrives, preserving mailbox throughput. Per-error-class disposition table pins state-transition + diagnostic-event-sequence + trait-return tuples for `Accepted` / `AlreadyInMempool` / `DoubleSpend` / `FeeTooLow` / `Malformed` / `Timeout` / `NetworkError`. **Finding 2 closes as (B) — daemon-side authority**: on `Timeout` or `DaemonUnavailable`, reservation stays in `SubmitPendingDaemonAck`; consumer-explicit `discard(id, ConsumerExplicit)` is the resolution path; R8's `ReservationTTLActor` (per-state TTL with shorter TTL on `SubmitPendingDaemonAck`) 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` + `SubmitErrorKind` enums** 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 the `DiscardReason`/`Discarded` event infrastructure. **No new `PendingTxDiagnostic` variants 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 pins `LocalPendingTx::new(..., sink: Arc, ...)` 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. Existing `SubmitFailureAnalyzer` FOLLOWUPS entry amended with segment-2f closure status; new `TimeoutResolverActor` FOLLOWUPS entry added naming the V3.x ergonomic-complement surface for Finding 2's daemon-side authority disposition. Updates §5.0.2 (`SubmitError` + `SubmitErrorKind` enum 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 `ReservationTTLActor` composition closure; `DiscardReason::TTLAutoDiscard` variant pin).** Doc-only commit on `feat/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's `ReservationTTLActor` introduction is additive-only — no V3.x trait revision, no V3.x enum revision, no V3.x consumer-side breaking change per the [`16-architectural-inheritance.mdc`](../.cursor/rules/16-architectural-inheritance.mdc) continuous-discipline corollary. The Round 1 reframe already named `ReservationTTLActor` as the consumer-actor composition shape (same pattern as PR 4's `PeerReputationActor` / `RecoveryActor`); segment 2e pins the V3.0 deliverables: (1) `PendingTxDiagnostic::BuildSucceeded` emitted at the `build`-success path in `LocalPendingTx::build` / `PendingTxActor::handle_build` (Phase 1 call-site review confirms); (2) `PendingTxDiagnostic::Discarded { reason: SnapshotRotationAutoDiscard }` emitted at `submit`'s snapshot-mismatch path (R5's lazy-discard semantics); (3) `PendingTxDiagnostic::ReservationOutstanding` variant exists in the `#[non_exhaustive]` enum (no V3.0 emitter; V3.x `ReservationTTLActor` is the first emitter); (4) **new in segment 2e:** `DiscardReason::TTLAutoDiscard` variant added to the `#[non_exhaustive] DiscardReason` set so V3.x's `ReservationTTLActor` can trigger `PendingTxActor` to emit `Discarded { reason: TTLAutoDiscard }` events without a V3.x enum revision. **R5 ↔ R8 coherence verified** — R5's `SnapshotRotationAutoDiscard` is the reactive cleanup path (cleanup-on-use); R8's `TTLAutoDiscard` is the proactive complement (age-based policy on never-used reservations); both share the `DiscardReason`/`Discarded` event 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. Existing `ReservationTTLActor` FOLLOWUPS entry amended with segment-2e closure-status confirmation and the new `DiscardReason::TTLAutoDiscard` variant 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.2 `DiscardReason` enum sketch (adds `TTLAutoDiscard` variant); §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; `SnapshotId` opacity closed as 16-byte content-addressed digest).** Doc-only commit on `feat/stage-1-pr5-pending-tx-engine-design`. Segment 2d closes the two remaining `SnapshotId`-adjacent residuals against the actual shape of the `LedgerSnapshot` substrate landed in PR 2. **R12 closes as (a)** — content-derived `SnapshotId` from existing `LedgerSnapshot` data; substrate inspection confirmed `LedgerSnapshot` carries `synced_height: u64` + `reorg_blocks: ReorgBlocks` (deterministic by construction; sufficient for content- addressed derivation). Stage 1's `LocalPendingTx` derives `SnapshotId` from `LedgerEngine::snapshot()` (existing trait method); Stage 4's `PendingTxActor` receives identical values via `LedgerDiagnostic::SnapshotMerged` events using the same digest function. No `LedgerEngine` trait amendment; Phase 0c truly collapses. **R2 closes as opaque 16-byte content-addressed digest** (`pub struct SnapshotId([u8; 16])`); domain-separated hash over `LedgerSnapshot`'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-boundary `SnapshotId` or `SnapshotMerged` consumer; 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): `LedgerBlock` carries 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 coordinated `LedgerEngine` + `FeeEstimator` PR. 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 the `SnapshotId`-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 `OutputSelector` trait-parameter seam (`LocalPendingTx`); V3.x lands `RandomizedSelector` / `EntropyMaximizingSelector` alternatives. **§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.md` adversary); options enumerated; disposition closed as V3.0 ships `SubmissionStrategyActor` seam with `DirectStrategy` default; V3.x lands `JitteredSubmissionStrategy` / `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 `FeeEstimator` trait seam; V3.x lands `WalletSideEstimator` analyzing `LedgerEngine` historical block fee distribution. **Conditional V3.0 lift** noted: if segment-2d Phase 0 review confirms bounded `LedgerEngine`- 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 `OutputSelector` trait seam; submission-strategy actors under `SubmissionStrategyActor` seam; wallet-side fee estimator under `FeeEstimator` trait seam; encrypted-persistence `PersistenceConsumerActor` for 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.mdc` amendment (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.mdc` or a new `discipline.mdc` rule 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. - **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) — separate `LocalSigner` / `SigningActor` from 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) — `PendingTxActor` holds spend material, "matches PR 4 R4's instance-scoped pattern" — with shape (b) (separate `SigningActor`) 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; `LocalPendingTx` does 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, per `00-mission.mdc` §1; designing the trait surface so spend material never enters `PendingTxActor` is 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 add `signer: Arc` (Stage 1) and `signer: ActorRef` (Stage 4) fields plus prose pinning the spend-material-locality discipline. - **R14 reservation extensibility seam.** New §5.4 R14 entry. `Reservation` shape gains an `extensions: Vec` field; `ReservationExtension` is `#[non_exhaustive]` with empty V3.0 variant set; same extensibility pattern as `RefreshDiagnostic` / `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 in [`FOLLOWUPS.md`](./FOLLOWUPS.md) is replaced by a V3.x entry tracking HW-wallet integration as a `Signer`-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)'s `SigningActor` infrastructure: the spend-key- isolated actor R4 (c) needs has a precedent in PR 5's `SigningActor`; 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.mdc` recurred 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` / `AttemptCompleted` events push-driving a `refresh_in_flight: bool` rather than a synchronous query) and explains why it still fails: the daemon controls when `AttemptCompleted` fires, the bool stays `true` indefinitely 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.md`](./ANONYMITY_NETWORKS.md) plus 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-reviewer` surfaced 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_snapshot` acquisition mechanism) added; §5.5 ground-1 prose softened against implicit overclaim.** Doc-only follow-up commit on `feat/stage-1-pr5-pending-tx-engine-design`. Round 1 review surfaced one R1-adjacent finding the closure commit implicitly overclaimed: §5.0.1's `LocalPendingTx` sketch holds `ledger: L` "for `current_snapshot` reads 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's `SnapshotId` opacity 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 `SnapshotId` from existing `LedgerSnapshot` data (working hypothesis).** Stage 1 reads snapshot identity via existing `LedgerEngine` / `LedgerSnapshot` surface; computes content-addressed ID locally. **Phase 0c truly collapses** in this disposition; no new trait surface. - **(b) Stage 1 subscribes to the `LedgerDiagnostic` stream.** Stage 1 implementation symmetric with Stage 4; modest implementation-symmetry cost in `LocalPendingTx`. Phase 0c still collapses at the trait surface. - **(c) `LedgerEngine` grows 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`](./completed/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.mdc`](../.cursor/rules/16-architectural-inheritance.mdc) applied 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. - **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` (off `dev` at PR-#42 merge `6de8335d5`). Closes the load-bearing open question [`docs/design/STAGE_1_PR_5_PENDING_TX_ENGINE.md`](./completed/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 (`DiagnosticSink` parameter; 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 per [`16-architectural-inheritance.mdc`](../.cursor/rules/16-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, `LedgerEngine` had to grow `current_snapshot_id() -> SnapshotId` so `PendingTxEngine::build` could read it inline. Under the actor framing, snapshot identity flows through the diagnostic-stream surface as `LedgerDiagnostic::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, `submit` is a mailbox message; the actor processes one message at a time; "check `reservation.snapshot_id` against `current_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, `PendingTxActor` is decoupled from `RefreshActor`'s liveness by mailbox. Hostile daemon stalling refresh keeps `RefreshActor` busy in `produce_scan_result`; `PendingTxActor`'s mailbox continues processing build/submit/discard against the most-recently-merged snapshot regardless. **Shapes (2) and (3) require `PendingTxActor` to query `RefreshActor`'s state**, which is what creates the DoS surface; shape (1) has no such query. Per [`00-mission.mdc`](../.cursor/rules/00-mission.mdc) §1 (security as precondition) and [`ANONYMITY_NETWORKS.md`](./ANONYMITY_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, `PendingTxActor` already 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 defines `PendingTxDiagnostic` (`#[non_exhaustive]`) carrying `BuildSucceeded` / `BuildFailed` / `SubmitAttempted` / `SubmitSucceeded` / `SubmitFailed` / `SubmitSnapshotInvalidated` / `Discarded` / `ReservationOutstanding` plus the `DiscardReason` enum (`#[non_exhaustive]`: `ConsumerExplicit` / `SnapshotRotationAutoDiscard` (R5 lazy-discard) / `DaemonRejectedTerminal` (R9 terminal disposition)). The trait surface adds a `&dyn DiagnosticSink` parameter on `LocalPendingTx::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 DiagnosticSink` to `Arc` 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-cutting `DiagnosticSink` contracts 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_snapshot` acquisition 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 (`SnapshotId` opacity / projection types; recursive trust boundary), R8 (reservation TTL / leak prevention — reframed as `ReservationTTLActor` composition + V3.x FOLLOWUPS), R9 (daemon-side submit failure — reframed as two-stage submit flow with intermediate `submitted-pending-daemon-ack` state 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) `SigningActor` isolation, 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 + 'static` on `P`). **Phase 0 net change (§4).** One amendment removed: 0c (load-bearing cross-trait synchronous query → `LedgerEngine`). Two added: 0f (`PendingTxDiagnostic` enum + `DiagnosticSink` parameter on `LocalPendingTx`); 0g (`LedgerDiagnostic::SnapshotMerged` variant addition — cross-trait but additive only, lives in the diagnostic-stream surface not in `LedgerEngine`'s trait surface). **Net effect: load-bearing surface coupling collapses to additive-only event-surface coupling**, which is exactly the kind of structural cleanup [`16-architectural-inheritance.mdc`](../.cursor/rules/16-architectural-inheritance.mdc)'s continuous-discipline corollary predicts. **V3.x FOLLOWUPS landed in this commit.** - `ReservationTTLActor` consumer actor (closes R8 by composition; subscribes to `BuildSucceeded` / `Discarded` events; restart-amnesia constraint per PR 4 §5.4.8 #1). - `SubmitFailureAnalyzer` consumer actor (subscribes to `SubmitFailed` / `SubmitSnapshotInvalidated`; pattern detection — many `SnapshotInvalidated` in a row → adversarial reorg-churn; recurring `FeeTooLow` → fee estimator drift; recursive trust boundary applies). - `ReservationAuditActor` consumer actor (subscribes to all `PendingTxDiagnostic` events; in-memory wallet-action audit log; falls under recursive trust boundary discipline if it persists or exports — projections only). - `SigningActor` migration entry (R11 option (b); Stage 4 spend-secret isolation; same HW-wallet-trigger language as PR 4 R4 deferred-(c)). **Cross-cutting `DiagnosticSink` contract-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 named `docs/design/REFRESH_DIAGNOSTIC_STREAM.md` as the spec doc; Round 2 disposes whether to rename to `DIAGNOSTIC_STREAM.md` (general) or factor a parent `DIAGNOSTIC_STREAM_CONTRACTS.md` that 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`](./completed/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`](./completed/STAGE_1_PR_4_REFRESH_ENGINE.md) §5.4.6 / §5.4.7 R6 reframe / §5.4.8 (the cross-cutting `DiagnosticSink` contracts inherited by PR 5); [`V3_ENGINE_TRAIT_BOUNDARIES.md`](./V3_ENGINE_TRAIT_BOUNDARIES.md) §2.4 (PR 5's binding trait surface — unchanged by Round 1). - **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-reviewer` on 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` → `Foreclosing` in [`STAGE_1_PR_4_REFRESH_ENGINE.md`](./completed/STAGE_1_PR_4_REFRESH_ENGINE.md) §5.4.6 (R6 reframe, concurrent-emit pin discussion). - Typo `dispositon` → `disposition` in [`REFRESH_DESIGN_LANDSCAPE.md`](./design/REFRESH_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.md`](./completed/STAGE_1_PR_4_REFRESH_ENGINE.md) over to the actual [`engine/refresh.rs`](../rust/shekyl-engine-core/src/engine/refresh.rs) source. 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. - **Stage 1 PR 4 — §5.5 work-list hygiene: P3 `apply_scan_result_to_state` `Vec`-discard row added.** Single-row addition to the [`STAGE_1_PR_4_REFRESH_ENGINE.md`](./completed/STAGE_1_PR_4_REFRESH_ENGINE.md) §5.5 work-list against the dev-side FOLLOWUPS entry ("P3: `apply_scan_result_to_state` allocates `Vec` even for trait-impl callers that discard it") that landed via PR #37 (commit `0a0d46b38`, 2026-05-10) during the design branch's pre-M3-tail window. The design branch was cut at `9e53c82fa` (pre-PR-#37); PR #37 reshaped the merge pipeline (`LedgerIndexes::ingest_block`, `process_scanned_outputs`, `apply_scan_result_to_state` carry 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 onto `dev`. P3's disposition under α (Round 1) plus (a-instance-scoped) view-material (Round 2 R4) remains Round 3 / Round 4 trait-surface enumeration: either `LedgerEngine::apply_scan_result` grows to surface the insertion-range carryout (Vec consumed, optimization dead code) or `RefreshEngine` owns 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 `RefreshEngine` producer-redesign question.** Doc-only commit on `feat/stage-1-pr4-refresh-engine-design` (off `dev` at `9e53c82fa`). Closes the load-bearing open question [`docs/design/STAGE_1_PR_4_REFRESH_ENGINE.md`](./completed/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 per [`19-validation-surface-discipline.mdc`](../.cursor/rules/19-validation-surface-discipline.mdc) (named on `dev` 2026-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`](./completed/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`](./design/REFRESH_DESIGN_LANDSCAPE.md): refresh-design-space substrate covering the privacy-by-default precondition (§2), the operational view-tag pre-filter from [`STAGE_1_PR_3_KEY_ENGINE.md`](./completed/STAGE_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.md`](./FOLLOWUPS.md) entry ("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-design` stays on `dev`-rooted doc-only commits until M3e closes per [`STAGE_1_PR_4_REFRESH_ENGINE.md`](./completed/STAGE_1_PR_4_REFRESH_ENGINE.md)'s branch policy. - **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`](./completed/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`](../rust/shekyl-engine-core/src/engine/refresh.rs)) builds a `Scanner` carrying 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::new` constructor 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::ConcurrentMutation` boundary**. Pinned as orchestrator-internal translation of `LedgerEngine` errors; **excluded** from `RefreshEngine::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 — `ScanResult` atomicity-under-cancellation contract**. Confirmed against the existing implementation (cancel checks at lines 980 / 1140 / 1186 return `Cancelled` immediately; partial state drops via the function frame). Pinned in the trait contract per [`V3_ENGINE_TRAIT_BOUNDARIES.md`](./V3_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 `DaemonEngine` contract), snapshot poisoning via `LedgerSnapshot` (confirmed value-typed at lines 147–156), and `ScannerContractViolation.evidence` as memory-amplifier vector (bounded shape required). **§5.4.6 trait-surface contract pins.** `Send + Sync + 'static` bound on `R: RefreshEngine` (Stage 4 `kameo` actor 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. - **Stage 1 PR 4 — Round 2 close-out: Phase 0c `InternalInvariantViolation` + Phase 0e `DaemonOp` / `ProtocolErrorKind` seed 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-side `RefreshError` enum.** Resolves the §5.4.7 R6 "(a) extend `ConcurrentMutation` or (b) introduce `InternalInvariantViolation`" cleanup pin at the design layer, not Round 4 commit-decomposition. The retry-loop call sites at [`engine/refresh.rs:1672–1680`](../rust/shekyl-engine-core/src/engine/refresh.rs) and `:2055–2065` are **state-machine invariant violations** ("loop body itself is broken" per the existing site comments), not retry-budget exhaustion. Conflating both into `ConcurrentMutation` would 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 str` for `context` is 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 from `MalformedScanResult { reason: "..." }` to `InternalInvariantViolation { context: "..." }`; existing reason strings become `context` values. **Phase 0e seed enums — `DaemonOp` and `ProtocolErrorKind` initial variant sets, audited against the producer's actual call-site surface.** Two ground-truth findings: - `DaemonOp` narrows to two variants per the [`engine/refresh.rs`](../rust/shekyl-engine-core/src/engine/refresh.rs) audit. The producer issues exactly two daemon RPCs: `daemon.get_height()` (tip fetch; lines 1480 / 1958) and `rpc.get_scannable_block_by_number(...)` (per-block fetch; line 1190). Under FCMP++ with view-tag pre-filtering, `get_scannable_block_by_number` returns the full per-block payload; no separate `GetBlocks` / `GetTransactions` / `GetOutputs` / `GetChainHashes` are issued. `GetFeeEstimates` and `SubmitTransaction` are `PendingTxEngine`-issued (PR 5), not refresh-issued. - `ProtocolErrorKind` is **fresh-defined**, not a re-export of upstream `shekyl_rpc::RpcError`. Upstream `RpcError` is a flat enum carrying `String` payloads 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 the `RefreshDiagnostic`-emission boundary; the `String` payload 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 + `DiagnosticSink` docstring).** The `Send + Sync` bound permits concurrent `emit` from multiple tasks; the non-blocking contract **holds under concurrent emission**, not merely per call. Serializing internal synchronization that admits unbounded contention — `Mutex>`, `RwLock`-wrapped state, any shared mutable structure without bounded-wait guarantees — violates the contract even when each `emit` call returns promptly in isolation. Conforming implementations use lock-free queueing (`crossbeam::queue::ArrayQueue`, `flume` non-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 multiple `LocalRefresh` instances share a sink. **Producer-panic-safety property and Round 4 `PanickingSink` test deliverable (§5.4.6).** The non-blocking pin closes the producer-liveness hazard from a *blocking* `emit`. It does not close the adjacent hazard from a *panicking* `emit` — 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 the `Scanner` (holding spend material) is live across the `emit` call. **Pinning "MUST NOT panic" on `emit` as 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 of `emit` results in a predictable refresh-attempt failure with `Scanner` cleanly zeroized via `Drop`, no leaked half-state, and the cancellation token consistently in either fired-or-not state. **Phase 1 test deliverable:** the `AssertionSink` coherence property test grows a `PanickingSink` variant that panics on configured event variants; the test asserts (a) `Scanner` is dropped before the panic crosses the producer frame (visible via a `Zeroize` observer wrapper in the test harness), (b) no inconsistent producer state remains observable after the unwind, and (c) the panic propagates without `Drop`-chain corruption or double-panic. Round 4 commit-decomposition pass records this alongside the `AssertionSink` coherence test as a Phase 1 deliverable. **Test-as-canonical-reference pin on the coherence contract (§5.4.6 + `DiagnosticSink` docstring).** When the `AssertionSink` coherence 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 a `ScanProgress` emission count toward coherence for a `MalformedScanResult` return?" 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. Per [`19-validation-surface-discipline.mdc`](../.cursor/rules/19-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 paired `AssertionSink` (coherence) and `PanickingSink` (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: `DiagnosticSink` contract 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 `emit` contract.** `DiagnosticSink::emit` MUST NOT block. Implementations use `try_send`-shaped semantics; on a full bounded channel, unavailable consumer, or any other back-pressure condition, `emit` drops 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.** `RefreshEngine` implementations MUST emit at least one corresponding `RefreshDiagnostic` event to the sink for every non-`Cancelled` `RefreshError` returned, 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: an `AssertionSink` wraps `LocalRefresh` and 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 `PeerReputationActor` and `ViewTagAnomalyDetector` design. **§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 — `MalformedKind` initial variants recorded.** Six daemon-attributable variants (`NonEmptyForEmptyRange`, `RangeLengthMismatch`, `RangeMembershipViolation`, `DuplicateHeight`, `MissingHeightEntry`, `ResidualAfterApply`) covering the current `MalformedScanResult { reason: &'static str }` call sites in `engine/merge.rs` and `engine/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 in `engine/refresh.rs:1678–1680` and `:2061–2064`) are flagged for Round 4 cleanup — they don't belong on `MalformedScanResult`'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 `ViewTagAnomalyDetector` V3.x entry with the explicit producer-side dependency: before the detector lands, the producer must grow a `ViewTagFalsePositive { 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. - **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](./completed/STAGE_1_PR_4_REFRESH_ENGINE.md) §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 about `RefreshEngine` in 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 per [`16-architectural-inheritance.mdc`](../.cursor/rules/16-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: `RefreshDiagnostic` event stream emitted via `DiagnosticSink`.** Rich structured events fan out to specialized consumer actors with per-consumer trust posture and sanitization rules. `produce_scan_result` gains a `diagnostics: &dyn DiagnosticSink` parameter (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 are `NoopDiagnosticSink` / `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-aware `DaemonEngine` surface 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 `ReorgAmplificationDetector` consumer actor that subscribes to `RefreshDiagnostic::ReorgObserved` events, maintains a windowed count, and signals cancellation back through the existing `CancellationToken` checkpoint-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 via `RecoveryActor` (Byzantine-fault-tolerance-flavored N-of-M agreement on contested data); reorg-amplification detection via `ReorgAmplificationDetector` (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. - **`PeerId` stability under Tor/I2P** → `PeerId` is a transport-defined opaque token; decay calibrated to circuit-rotation cadence; Stage 1 variants omit peer attribution entirely until PR 1's peer-aware `DaemonEngine` surface 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 + `ViewMaterial` type 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-side `RefreshError` retained 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**): `RefreshDiagnostic` enum + `DiagnosticSink` trait + `produce_scan_result` signature change (`diagnostics: &dyn DiagnosticSink` parameter). Stage 1 sinks: `NoopDiagnosticSink`, `TracingDiagnosticSink`. **FOLLOWUPS amended.** The previous Round 2 "extend checkpoint 3" V3.x FOLLOWUPS entry is **withdrawn** and replaced by the `ReorgAmplificationDetector` entry. Three new V3.x FOLLOWUPS entries added: `PeerReputationActor` (with §5.4.8 #1 / #2 / #3 mitigation pins binding on the implementation), `RecoveryActor`, and `docs/design/REFRESH_DIAGNOSTIC_STREAM.md` spec 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`](../.cursor/rules/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 what [`16-architectural-inheritance.mdc`](../.cursor/rules/16-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. - **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::build` during 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 against `current_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` / `RefreshProgress` public-module promotion.** Confirmation, not discovery: `RefreshOptions`, `RefreshProgress`, `RefreshSummary`, `RefreshHandle`, `RefreshReorgEvent`, `RefreshPhase` are already crate-publicly re-exported from [`shekyl_engine_core/src/lib.rs:25–30`](../rust/shekyl-engine-core/src/lib.rs) at the flat crate root, matching the `DaemonEngine` / `LedgerEngine` convention. Stage 4's `kameo` actor 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 public `Zeroize + ZeroizeOnDrop` type carrying `{ spend_pub, view_scalar, x25519_sk, ml_kem_dk, spend_secret }` — exactly the fields `build_scanner_from_keys` extracts from `&AllKeysBlob` today. One `Scanner` held for `LocalRefresh`'s lifetime; per-attempt cost drops to snapshot+daemon RPC (no scanner construction). Stage 4 actor mailbox carries no secrets. Wallet-lock semantics drop `LocalRefresh` and zeroize via the existing `ZeroizeOnDrop` chain. **(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_height` RPC 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's `DaemonEngine` peer-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::ConcurrentMutation` boundary + variant set.** Promote the existing crate-internal `ProduceError` ([`engine/refresh.rs:202`](../rust/shekyl-engine-core/src/engine/refresh.rs)) to public `RefreshEngineError`; use it as `RefreshEngine::Error: Into`. Variant set: `Cancelled`, `Io(IoError)`, `MalformedScanResult { reason: &'static str }` — the existing name and bounded payload are kept; the user-proposed `ScannerContractViolation { kind, evidence }` rename declined for V3.0 since `&'static str` is 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 — `ScanResult` atomicity-under-cancellation contract.** Pinned in [`V3_ENGINE_TRAIT_BOUNDARIES.md`](./V3_ENGINE_TRAIT_BOUNDARIES.md) §2.3 / §7 prose: a `produce_scan_result` call returns either a `ScanResult` covering the full span scanned, or `RefreshError::Cancelled`; no partial-span `ScanResult`. Already true in the existing implementation per the cancel checks at [`engine/refresh.rs:980 / :1140 / :1186`](../rust/shekyl-engine-core/src/engine/refresh.rs); the contract pin prevents future drift. **§4 Phase 0 finalized.** Phase 0a: trait-surface contract pins (`Send + Sync + 'static` on `R`; Progress-channel trust boundary; `ScanResult` atomicity per R7; `LedgerSnapshot` value-typed contract; `ViewMaterial` type definition per R4). Phase 0b: `LocalRefresh::new(view_material: ViewMaterial)` constructor + flat-crate-root export of `ViewMaterial`. Phase 0c: `RefreshEngineError` promotion per R6. Phase 0d: retired (R5 deferred). **Two new V3.x FOLLOWUPS entries** in [`docs/FOLLOWUPS.md`](./FOLLOWUPS.md): R5 mid-scan reorg-abort deferral; R4 (c) split-producer/recoverer deferral. Both have named triggers per [`15-deletion-and-debt.mdc`](../.cursor/rules/15-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. ### Fixed - **CI bench gate no longer false-fails on `baseline=0` capture anomalies; the anomaly is surfaced as informational rather than silenced.** Discovered on PR #34: the `bench-baseline` branch's most-recent refresh (from dev-tip `647f82d5`) recorded `instructions=0` for six `hot_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 in `baseline.iai.snapshot` reporting `6 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 on `chore/investigate-bench-baseline-flake-2026-05-09`. [`scripts/bench/compare.py`](../scripts/bench/compare.py) now routes `(base_val == 0 && pr_val != 0)` into a distinct `baseline_zero` bucket — informational, not gating — that preserves the PR-side measurement for diagnosis. [`scripts/bench/post_comment.py`](../scripts/bench/post_comment.py) renders the bucket under its own header line ("Baseline anomaly (informational, not gated)") and table rows with a `_baseline=0_` verdict badge distinct from `ok` / `FAIL` / `added` / `missing`, so the anomaly surfaces to reviewers rather than being silently masked under the "new in PR" label. The post-merge `update-baseline` job re-captures from the next push to `dev`; 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 trip `fail` (validated with a +39% hot_path fixture); the `(base=0, pr=0)` edge case is preserved as a 0% delta `ok` rather than getting routed away. Lock-down: [`scripts/bench/test_compare.py`](../scripts/bench/test_compare.py) pins 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 via `python3 scripts/bench/test_compare.py`. - **Bench-capture producer guard rejects `instructions=0` rows at source so the anomaly cannot reach `bench-baseline` again.** Paired defense-in-depth with the consumer-side `baseline_zero` bucket (above): the consumer routes around already-corrupted baseline data; the producer prevents new corruption from being written. Implemented in [`scripts/bench/capture_rust_baseline.sh`](../scripts/bench/capture_rust_baseline.sh) inside the JSON-assembly heredoc, post-parse / pre-write: any iai entry with `metrics.instructions == 0` causes the script to exit `2` with a structured error that lists the offending `(crate, bench_target, group, function, run_id)` tuples and points operators at `docs/investigation/2026-05-09-bench-baseline-flake.md`. The canonical `shekyl_rust_v0.json` is **not** written when the guard trips, so the prior good `bench-baseline` content is preserved across both pipeline arms — `update-baseline` (push to `dev`) and `capture-pr` (per-PR baseline). The raw stdout snapshot at `shekyl_rust_v0.iai.snapshot` is still written unconditionally as bisection evidence, and a diagnostic side-file at `shekyl_rust_v0.json.flake.json` carries the parsed envelope plus a `flake` block enumerating the zero entries — investigators can `gh run download`-style fetch it without re-running the harness. Bypass: `SHEKYL_BENCH_ALLOW_ZERO=1` skips the check with a loud `WARNING` line 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 hardcodes `FAKECHAIN`; the legacy 3-arg overload is deleted entirely and every caller spells its network out explicitly.** Pre-fix, the 3-arg `account_base::generate(recovery_key, recover, two_random)` overload (with default args `secret_key{} / false / false`) hardcoded `DerivationNetwork::Fakechain` as the raw-seed derivation salt regardless of the wallet's actual `network_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`), and `wallet_rpc_server::on_stop_background_sync`'s seed-recovery path. On TESTNET, every from-seed wallet creation produced a FAKECHAIN-salted account that failed `wallet2::load`'s rederive (which uses `m_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 through `generate_from_raw_seed`, and is now the **only** `generate(...)` overload — the legacy 3-arg form is deleted entirely. `wallet2::generate(...)` and `wallet_rpc_server::on_stop_background_sync` migrated to pass `m_nettype` / `m_wallet->nettype()`. The 0-change dummy- destination caller in `wallet2::transfer_selected_rct` migrated to the same 4-arg form with `cryptonote::FAKECHAIN` hardcoded — 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 across `tests/{unit_tests,core_tests,performance_tests,trezor, functional_tests,wallet_bench}` migrated to pass `cryptonote::FAKECHAIN` explicitly. The structural deletion eliminates the "one omitted argument away from FAKECHAIN" footgun class entirely — there is no longer a `generate(...)` 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's `permitted_seed_format` check 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) and `wallet2_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 via `wallet2` simply 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 through `wallet2::load`. **New regression test:** `tests/unit_tests/account.cpp` :: `generate_uses_explicit_nettype_argument` pins (a) `generate(..., TESTNET)` matches `generate_from_raw_seed(..., TESTNET)`, (b) `generate(..., FAKECHAIN)` produces a distinct account (different HKDF salt), and (c) `generate(..., MAINNET / STAGENET)` throws for **both** `recover=true` (recovery) and `recover=false` (fresh CSPRNG seed). See `docs/audit_trail/2026-05-ffi-constant-drift-audit.md` Bug 4-adjacent. - **`FCMP_REFERENCE_BLOCK_MIN_AGE` aligned to consensus authority (5).** `rust/shekyl-engine-core/src/multisig/v31/intent.rs` defined `FCMP_REFERENCE_BLOCK_MIN_AGE = 10` while `src/cryptonote_config.h` defines it as `5` (locked by Decision 14 in commit `6561278d9`, asserted by `tests/unit_tests/fcmp.cpp:668`, documented in `docs/FCMP_PLUS_PLUS.md:432`). The Rust multisig `SpendIntent` was added in `744ab6407` 23 days after Decision 14 and copied the pre-Decision-14 value `10`. Bug 3 of the 2026-05-05 FFI constant-drift audit. Failure mode: a multisig wallet would reject reference blocks at heights `tip-9..tip-5` that 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 to `5`, with a doc-comment that cross-references the C++ authority and the audit. Test `validate_temporal_rejects_ref_block_too_fresh` updated to use `tip = 903` (age = 3) instead of `tip = 905` (age = 5, which was the boundary value that masked the regression — age = 5 is not `< 5`). `docs/SHEKYL_MULTISIG_WIRE_FORMAT.md` aligned. The `chore/cbindgen-consensus-constants` follow-up generates this value from the Rust authority into the C++ build to prevent recurrence. See `docs/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.h` defined `SHEKYL_CLASSICAL_ADDRESS_BYTES = 64` while authoritative `rust/shekyl-crypto-pq/src/account.rs::CLASSICAL_ADDRESS_BYTES = 1 + 32 + 32 = 65`. Because `ShekylAllKeysBlob` is `#[repr(C)]` with byte-aligned `[u8; N]` arrays, the 1-byte deficit shifted every later field's offset by one. C++ `populate_account_from_blob` read `spend_sk` and `view_sk` from the wrong bytes; the resulting non-canonical Ed25519 scalars failed `sc_check` inside `secret_key_to_public_key`, so `verify_keys` returned false and every `wallet2::load` threw `error::wallet_files_doesnt_correspond`. Header constant set to `65`. **Bug 1 of 2 surfaced by `wallet_storage.{store_to_mem2file, change_password_mem2file}`.** See `docs/audit_trail/2026-05-ffi-constant-drift-audit.md`. - **C++/Rust FFI constant disagreement caused every RAW32 wallet to silently mis-encode its `seed_format` byte.** `src/shekyl/shekyl_ffi.h` defined `SHEKYL_SEED_FORMAT_BIP39 = 0` / `_RAW32 = 1` while authoritative `rust/shekyl-crypto-pq/src/account.rs` defines `SEED_FORMAT_BIP39 = 0x01` / `SEED_FORMAT_RAW32 = 0x02` (with `0` reserved for "unset"). C++ wrote `m_seed_format = 1` to disk meaning RAW32; on `wallet2::load`, the FFI received `seed_format = 1` and Rust decoded it as `Bip39`; `permitted_seed_format(Fakechain, Bip39)` returned `false`; the rederive returned `false` with `"(network, seed_format) pair disallowed or derivation inconsistent"`. The BIP-39 path was equally broken (both sides held `0`, 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 to `1` / `2`. **Bug 2 of 2.** Pre-V3 launch: no on-disk wallets exist, so no migration code is required. See `docs/audit_trail/2026-05-ffi-constant-drift-audit.md`. - **`wallet_storage` round-trip tests now construct `wallet2` with `cryptonote::FAKECHAIN`.** `wallet2::generate(name, password)` routes through the legacy `account_base::generate()` test wrapper, which hardcodes `FAKECHAIN` for raw-seed derivation regardless of the wallet's `m_nettype`. The default-constructed `wallet2` inherited `MAINNET`, so the rederive on `load` passed `MAINNET`, which doesn't permit `RAW32`. Tests now use `tools::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 in `account_base::generate()`'s callers (the `wallet2::generate("", password)` test path and `wallet_rpc_server::stop_background_sync`) is the **Bug 4-adjacent** finding in `docs/audit_trail/2026-05-ffi-constant-drift-audit.md`, slated for the sibling branch `fix/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_fields` previously scanned the full `ledger.transfers` Vec on every `Engine::apply_scan_result` invocation, even though only `result.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 against `residue` that found nothing — ~5 s of refresh-time wallclock. The merge pipeline now threads the inserted-index list out of `LedgerIndexes::ingest_block` (now `Range`), through `LedgerIndexesExt::process_scanned_outputs` (now `Range`) and `apply_scan_result_to_state` (now `Result, 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_fields` O(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_storage` tests deleted.** The `tests/data/wallet_00fd416a*` and `tests/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 behind `GTEST_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_authority` and `ffi_seed_format_constants_match_rust_authority` pin the FFI re-exports to the authoritative `rust/shekyl-crypto-pq/src/account.rs` constants. **Scope (honest):** these tests compare two Rust-side values; they do not read `src/shekyl/shekyl_ffi.h`. A hand-edit to the C++ `#define` alone — 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 branch `chore/cbindgen-consensus-constants`, which generates a header from the Rust constants for `RCTTypeFcmpPlusPlusPqc`, `FCMP_REFERENCE_BLOCK_*_AGE`, and `ADDRESS_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 via `account_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`, and `rederive_from_raw_seed_reproduces_account_testnet` (consensus-level `(network, format)` matrix invariants). The `wallet2`-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_bip39` absence (`tests/unit_tests/wallet_storage.cpp`).** Three SFINAE detectors + one combined `static_assert` that fires at build time if a future contributor adds `wallet2::generate_from_bip39` with 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 with `wallet2.cpp` at Phase 5 of the Rust rewrite. Architectural decision recorded in `docs/FOLLOWUPS.md` §"V3.1+ Legacy C++ → Rust rewrite scope". See `docs/audit_trail/2026-05-ffi-constant-drift-audit.md` Bug 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. - **`AllKeysBlob` and `KeyImage` typed-wrapper sweep (between Stage 1 PR 3 M3a and M3b; short-lived sweep branch off the M3a PR head per [`docs/completed/STAGE_1_PR_3_MIGRATION_PLAN.md`](./completed/STAGE_1_PR_3_MIGRATION_PLAN.md) §3 "Landing notes (M3a closed)").** Closes the deferred-from-M3a typed-wrapper migration that the M3a `ViewSecret` work pre-announced in `shekyl-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::keys` newtypes:** - **`SpendSecret`** — secret-bearing scalar mirroring `ViewSecret`'s discipline exactly: `#[repr(transparent)]`, `Clone + Zeroize + ZeroizeOnDrop`, no `Copy`, no `Debug`, `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 + Zeroize` for use as registry keys (`LocalKeys`'s `HashMap` reverse-lookup registry); manual truncated `Debug` matching `KeyImage`'s privacy-correlation discipline (first two bytes only); `pub fn from_canonical_bytes` constructor — engine boundaries outside this crate (`shekyl-engine-core::engine::local_keys::derive_subaddress`) are legitimate construction sites, mirroring `KeyImage`'s pattern. No `ZeroizeOnDrop` because that conflicts with `Copy` (Rust trait coherence rule); the surrounding `AllKeysBlob::drop` clears these public fields explicitly via `.zeroize()` for the same uniform- write-pattern reason raw `[u8; 32]` fields had. **`AllKeysBlob` field migration:** - `spend_pk: [u8; 32]` → `spend_pk: SpendPublicKey` - `view_pk: [u8; 32]` → `view_pk: ViewPublicKey` - `spend_sk: [u8; 32]` → `spend_sk: SpendSecret` - `view_sk: ViewSecret` (already typed in M3a Commit 2; unchanged) The `Drop` implementation simplifies: `spend_sk` and `view_sk` now 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 by `shekyl-ffi`'s `size_of::<...>()` test against `ShekylAllKeysBlob`. **`shekyl-crypto-pq::key_image::KeyImage` API extensions:** - Now derives `Zeroize`, `Serialize`, `Deserialize`, with `#[serde(transparent)]`. Wire format remains byte-identical to `[u8; 32]`. - `Zeroize` (without `ZeroizeOnDrop`, which would conflict with `Copy`) lets containers that hold a `KeyImage` alongside genuinely- secret material (`shekyl_engine_state::TransferDetails`, `shekyl_scanner::RecoveredWalletOutput`) wipe every field on `Drop` for uniform-write-pattern hygiene — the same `Copy + Zeroize` pairing the new public-key newtypes use. The manual `Debug` and absence-of-`Display` privacy discipline is unchanged. **`KeyImage` call-site sweep across the workspace:** - **`shekyl-engine-state::TransferDetails.key_image`:** `Option<[u8; 32]>` → `Option`. The on-disk and postcard-schema layouts are preserved by `KeyImage`'s `#[serde(transparent)]`. `TransferDetails::zeroize` continues to wipe the field; `Zeroize` on `KeyImage` removes the special-case `Option`-then-bytes accessor previously needed at the wipe site. - **`shekyl-engine-state::LedgerIndexes.key_images`:** `HashMap<[u8; 32], usize>` → `HashMap`. Method signatures on `mark_spent`, `unmark_spent`, `detect_spends`, `set_key_image`, `freeze_by_key_image`, `thaw_by_key_image` updated to take `&KeyImage` / `KeyImage` / `&[KeyImage]`. The `[0u8; 32]` filter on rebuild/ingest is removed: the runtime-scanner path always produces a real key image, and `Option` already encodes "not yet computed" — sentinel-byte gating was redundant in the on-disk path. (See FOLLOWUPS for the matching deferred promotion of `RecoveredWalletOutput.key_image` to `Option` in V3.1.) - **`shekyl-scanner::RecoveredWalletOutput.key_image`:** `[u8; 32]` → `KeyImage` with `#[zeroize(skip)]`. The boundary in `ledger_ext.rs` retains a `[0u8; 32]` test-fixture filter so `RecoveredWalletOutput::new_for_test`'s zero placeholder maps to `td.key_image = None` (preserving the offline-derivation / `set_key_image` fill-in semantics view-only wallets rely on); a FOLLOWUPS V3.1 entry tracks promoting the field itself to `Option` and deleting the boundary filter. - **`shekyl-engine-core::scan::KeyImageObserved.key_image`:** `[u8; 32]` → `KeyImage`. Constructor sites in `refresh.rs`'s per-block input-walk wrap raw bytes via `KeyImage::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's `write_per_output` consumes via `key_image.as_bytes()` and the verifier wraps the on-wire bytes back into `KeyImage` at the return boundary. - **`shekyl-engine-core::multisig::v31::prover::ProverInputProof.key_image`:** `[u8; 32]` → `KeyImage`. `signable_bytes()` consumes via `key_image.as_bytes()`; serde wire format unchanged. - **`shekyl-engine-core::multisig::v31::counter_proof::CounterProof.consumed_inputs`:** `Vec<[u8; 32]>` → `Vec`; `CounterProofChainView::is_tracked_unspent` signature updated to take `&KeyImage`. - **`shekyl-engine-core::engine::traits::key::SubaddressKeyPair`:** `spend_pk` / `view_pk` typed as `SpendPublicKey` / `ViewPublicKey`. - **`shekyl-engine-rpc::handlers::parse_key_image`:** now returns `KeyImage` (constructor site at the wallet-RPC boundary). - All `[u8; 32]` test fixtures across `shekyl-engine-state`, `shekyl-engine-core` (including bench fixtures and adversarial multisig tests), and `shekyl-scanner` updated to construct `KeyImage::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. New `shekyl-fcmp` regular dependency on `shekyl-crypto-pq` (cycle-free: `shekyl-crypto-pq` references `shekyl-fcmp` only as a `[dev-dependencies]` entry). The type is re-exported as `pub use shekyl_crypto_pq::key_image::KeyImage` from `shekyl-fcmp::proof` so callers (fuzz harnesses) can name it without taking a direct dep. - **`shekyl-ffi::lib`'s `shekyl_fcmp_verify` marshaling:** rebuilds `Vec` via `KeyImage::from_canonical_bytes` from the C-supplied `*const u8` buffer; `pseudo_outs` marshaling is unchanged. - **`shekyl-fcmp` fuzz targets** (`fuzz_tx_deserialize_fcmp_type7`, `fuzz_fcmp_proof_deserialize`) updated their key-image generators to `Vec` since they call `verify` directly. - **`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 the `Input::ToKey { key_image: CompressedPoint(...) }` construction site (the on-wire `CompressedPoint` is 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_recover` writes via `key_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 in `KeyImage` here would inject `from_canonical_bytes` / `as_bytes` shuffles 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 `KeyImage` field 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 landed `ViewSecret` and the `KeyEngine` trait; 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-curve` rename per `60-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.toml` description: `"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.rs` test-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.rs` module 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`'s `hash_to_point`, fork-attribution license headers in `shekyl-scanner`, "Monero mainnet" empirical comparisons, `60-no-monero-legacy.mdc` exclusion 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. - **`KeyEngine` trait surface and `LocalKeys` in-process implementor introduced (Stage 1 PR 3 — M3a; the third trait-boundaries PR per [`docs/V3_ENGINE_TRAIT_BOUNDARIES.md`](./V3_ENGINE_TRAIT_BOUNDARIES.md) §2.3).** The M3a slice of the five-PR Stage-1 PR 3 migration (M3a–M3e per [`docs/completed/STAGE_1_PR_3_MIGRATION_PLAN.md`](./completed/STAGE_1_PR_3_MIGRATION_PLAN.md) §3) lands as `pub(crate)` on `shekyl-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 owns `AllKeysBlob` privately and exposes a workflow-shape surface (no per-output secret material crosses the trait boundary). - **`pub(crate) trait KeyEngine`** in [`engine::traits::key`](../rust/shekyl-engine-core/src/engine/traits/key.rs). Four workflow-shaped methods per [`docs/completed/STAGE_1_PR_3_KEY_ENGINE.md`](./completed/STAGE_1_PR_3_KEY_ENGINE.md) §4: `account_public_address(&self) -> &AccountPublicAddress` (sync borrowed read); `derive_subaddress(&self, idx, purpose) -> Result` (sync, two purposes — `Audit` returns the classical Edwards-curve `(spend_pk, view_pk)` pair, `Recipient` returns the encoded address + hybrid KEM PK pair); `try_claim_output(&self, input) -> impl Future> + Send` (async; bundles X25519 view-tag pre-filter, hybrid decap, HKDF chain, key-image computation, deterministic `OutputHandle` derivation behind a single trait boundary); `sign_transaction(&self, tx) -> impl Future> + Send` (async; resolves per-input handles to per-output spending material and produces hybrid signatures + FCMP++ witnesses). The associated `type Error: Into` lets orchestration code propagate uniform errors regardless of implementor. - **`pub(crate) struct LocalKeys`** in [`engine::local_keys`](../rust/shekyl-engine-core/src/engine/local_keys.rs). Owns `AllKeysBlob` privately; caches `AccountPublicAddress` and pre-computes `(view_scalar, spend_public)` cryptographic forms at construction; guards a reverse-lookup subaddress registry under `RwLock` (the `LocalLedger` precedent for `&self` async with synchronous interior mutation). Real implementations of `account_public_address`, `derive_subaddress(_, Audit)`, and `try_claim_output`; named-infrastructure-gap stubs for `derive_subaddress(_, Recipient)` and `sign_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, varying `tx_hash`, other-wallet rejection, unregistered-subaddress rejection, register-then-claim sequence, and `sign_transaction` stub validation. - **Two named-infrastructure-gap `KeyEngineError` variants:** `RecipientSubaddressKemKeygenNotImplemented` (per-subaddress hybrid X25519+ML-KEM-768 keygen, `shekyl_crypto_pq::subaddress::derive_subaddress_kem_keypair`, is unbuilt; lands per [`docs/completed/STAGE_1_PR_3_KEY_ENGINE.md`](./completed/STAGE_1_PR_3_KEY_ENGINE.md) §6.4 / §3.1.3) and `SignTransactionTraitSurfaceIncomplete` (`TxToSign`'s public-on-chain per-input data and FCMP++ tree-branch context are PR-5-pinned forward-declared; the bridge to `shekyl_tx_builder::sign_transaction` lands when the `PendingTxEngine` PR finalizes the shape). Both variants are `#[non_exhaustive]`-shaped accretions; existing call sites stay source-compatible as the surface evolves. - **`OutputHandle` newtype + `derive_output_handle`** in [`shekyl_crypto_pq::handle`](../rust/shekyl-crypto-pq/src/handle.rs). 16-byte opaque reference deterministically derived via cSHAKE256 over `view_secret || tx_hash || output_index_le8` with customization `"shekyl/output-handle-v1"` per [`docs/completed/STAGE_1_PR_3_KEY_ENGINE.md`](./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 cached `HandleTable` data 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. - **`KeyImage` newtype** in [`shekyl_crypto_pq::key_image`](../rust/shekyl-crypto-pq/src/key_image.rs). 32-byte canonical compressed Ed25519 encoding of `I = x · H_p(O)`; the per-output public on-chain double-spend identifier. Carries the same privacy-correlation discipline as `OutputHandle` (truncated `Debug` exposing the first two bytes only; no `Display`; no `Zeroize` because key images are publicly derivable from on-chain data). Per [`.cursor/rules/18-type-placement.mdc`](../.cursor/rules/18-type-placement.mdc), `KeyImage` is **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. - **`ViewSecret` newtype** in [`shekyl_crypto_pq::keys`](../rust/shekyl-crypto-pq/src/keys.rs). `#[repr(transparent)]` 32-byte wrapper preserving the bit-for-bit FFI layout invariant with `shekyl_ffi::ShekylAllKeysBlob.view_sk: [u8; 32]`. Manual truncated `Debug`; structural `ZeroizeOnDrop`. Wraps `AllKeysBlob::view_sk`; downstream call sites consume the canonical bytes via `.as_canonical_bytes()`. The remaining `AllKeysBlob` typed-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-curve `subaddress_derivation_scalar` and `subaddress_keys` (formerly methods on `shekyl_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 future `derive_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_keys` is preserved as a thin call- through; `ViewPair::subaddress_derivation` was deleted (no live caller after the relocation, per [`.cursor/rules/15-deletion-and-debt.mdc`](../.cursor/rules/15-deletion-and-debt.mdc)). `SubaddressIndex::to_canonical_bytes` accessor and the `PRIMARY` constant added to `shekyl_engine_state::SubaddressIndex` per [`.cursor/rules/18-type-placement.mdc`](../.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. - **`SourceSecretsBundle` transitional contract type** in [`engine::traits::key`](../rust/shekyl-engine-core/src/engine/traits/key.rs). Documents the per-input secret material `KeyEngine::sign_transaction` needs — `(spend_key_x, spend_key_y, commitment_mask, combined_ss, output_index)`, each `Zeroizing`-wrapped — independent of where the secrets originate. The bundle's *shape* is stable across the migration (M3a populates from `TransferDetails`'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 — `TransferDetails` still carries its 5 secret-bearing fields, and the bridge reads from them transitionally. The property activates at M3d's merge per [`docs/completed/STAGE_1_PR_3_MIGRATION_PLAN.md`](./completed/STAGE_1_PR_3_MIGRATION_PLAN.md) §4.1, when those fields are deleted. M3a is what makes the activation possible: the `KeyEngine` trait is the boundary the property eventually attaches to, and the deterministic `OutputHandle` is 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 `Debug` on secret-bearing message shapes.** `SourceSecretsBundle`, `TxInputSigningContext`, and `TxToSign` each now carry a manual `Debug` impl (no `derive(Debug)`) redacting the four `Zeroizing<…>` secret fields under `[REDACTED]`. Per `35-secure-memory.mdc`, `Zeroizing: Debug` delegates to `T: Debug`, so deriving `Debug` on a secret-bearing struct prints raw secret bytes through `tracing` fields, panic backtraces, or `dbg!()` calls. Three new sentinel-byte tests in `engine::traits::key::tests` pin 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`) into `classical_address_bytes` directly, and the reverse-lookup registry pre-registers `keys.spend_pk` against `SubaddressIndex::PRIMARY`. The trait method previously routed `PRIMARY` through `subaddress_keys`, returning `(D + m_0*G, a*(D + m_0*G))` — a different point that matched neither the encoded address nor the registry. Special- casing `idx.is_primary()` to return the base account keys aligns the trait with the encoded address; for `idx >= 1`, the per-index derivation is unchanged. New `derive_subaddress_primary_audit_returns_base_account_keys` test pins the contract; docstrings on `shekyl_engine_state::SubaddressIndex`, `shekyl_crypto_pq::subaddress`, and the `subaddress_keys` primitive itself updated to spell out the special-case truth. - **Hard-coded pinned vector for `subaddress_derivation_scalar`.** The prior `derivation_scalar_pinned_vector` test re-ran the same `keccak256_to_scalar` primitive 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.mdc` named `SubaddressIndex`'s home as `shekyl-engine-core` (twice); the type actually lives in `shekyl-engine-state`. Updated. - **`LedgerEngine` trait extracted; `Engine` parameterized over `L: LedgerEngine` with default `LocalLedger` (Stage 1 PR 2, the second trait-boundaries PR per [`docs/V3_ENGINE_TRAIT_BOUNDARIES.md`](./V3_ENGINE_TRAIT_BOUNDARIES.md) §2.2).** The Phase 2a `LedgerEngine` slice of the Stage 1 trait-extraction work lands as `pub(crate)` on `shekyl-engine-core`. The PR's primary surface — the `LedgerEngine` trait, the `LocalLedger` aggregate, and the `Engine` / `OpenedEngine` parameterization. The new type parameters carry default arguments (`D = DaemonClient, L = LocalLedger`) so non-test consumers continue to name `Engine` / `OpenedEngine` 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 is `Engine::ledger()`, which now returns `LedgerReadGuard<'_>` (a wrapper around `RwLockReadGuard<'_, LedgerState>`) instead of `&WalletLedger`; `LedgerReadGuard` derefs to `WalletLedger`, 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 the `Engine::ledger()` doc-comment in [`rust/shekyl-engine-core/src/engine/mod.rs`](../rust/shekyl-engine-core/src/engine/mod.rs) for 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 — see [`docs/completed/STAGE_1_PR_2_LEDGER_ENGINE.md`](./completed/STAGE_1_PR_2_LEDGER_ENGINE.md) §1.1 / §2.2 for the discipline pattern. - **`pub(crate) trait LedgerEngine`** in [`engine::traits::ledger`](../rust/shekyl-engine-core/src/engine/traits/ledger.rs). Post-Phase-0c four-method surface: `synced_height(&self) -> u64`, `snapshot(&self) -> LedgerSnapshot`, `balance(&self) -> BalanceSummary` (sync, infallible reads), and `apply_scan_result(&self, ScanResult) -> Result<(), RefreshError>` (async, mutating; signals `RefreshError::ConcurrentMutation` for the §5.2 retry contract). The async `&self` mutation is enabled by interior `RwLock` 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. `LedgerError` is reserved as an empty starter type for Phase-2a-specific error variants the trait does not currently emit. - **`pub struct LocalLedger { state: RwLock }`** in [`engine::local_ledger`](../rust/shekyl-engine-core/src/engine/local_ledger.rs). `LedgerState` bundles `WalletLedger` + `LedgerIndexes` (the two fields previously held flat on `Engine`); reservations stay on `Engine` for now and migrate to `LocalPendingTx` when the `PendingTxEngine` PR ships. The aggregate is `pub` (not the originally-planned `pub(crate)`) because Rust requires every default type parameter on a `pub` type to be at least as visible as the type itself; the trait `LedgerEngine` itself stays `pub(crate)` per §1.4 of the contract. See [`docs/completed/STAGE_1_PR_2_LEDGER_ENGINE.md`](./completed/STAGE_1_PR_2_LEDGER_ENGINE.md) §3.4 for the visibility-lift rationale. - **`Engine`** and **`OpenedEngine`**. 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 the `LocalLedger` interior-mutability refactor below; the measured iai-callgrind cost of the combined change on `engine_trait_bench_ledger_synced_height` is `+390%` (10 → 49 instructions, sourced entirely from the `RwLock::read()` acquisition in `LocalLedger::read()`, not from trait dispatch). Per the [`docs/V3_ENGINE_TRAIT_BOUNDARIES.md`](./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 replaces `RwLock` with `Arc`-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 in [`docs/PERFORMANCE_BASELINE.md`](./PERFORMANCE_BASELINE.md)'s `engine_trait_bench_ledger_synced_height` cumulative-delta footnote. Each `pub` item bounded by the `pub(crate)` `LedgerEngine` trait carries an `#[allow(private_bounds)]` annotation paralleling the `DaemonEngine` annotations from PR 1; both clear at Stage 4 when both traits promote to `pub` per §1.4. - **Refresh path migrated to `&self` interior mutation.** `Engine::synced_height` now dispatches through `LedgerEngine::synced_height`; `Engine::apply_scan_result`, `Engine::refresh`, and `Engine::refresh_with` flip from `&mut self` to `&self`; the producer task `run_refresh_task`'s outer `Arc>` write-lock guard becomes a read-lock per the §3.3 over-serialization framing. The synchronous wrappers `refresh` / `refresh_with` retain their `LocalLedger`-specialized impl block because the trait method `apply_scan_result` is `async fn` and the sync entry points use `LocalLedger::write()` directly without a Tokio runtime in scope (queued at V3.x in [`docs/FOLLOWUPS.md`](./FOLLOWUPS.md) for full sync-wrapper generalization). `Engine::start_refresh` and `run_refresh_task` *are* generalized over `L: LedgerEngine`, sufficient for the hybrid retry test to dispatch through the trait against `MockLedger`. - **`MockLedger` deterministic in-memory `LedgerEngine` implementor** in [`engine::test_support`](../rust/shekyl-engine-core/src/engine/test_support.rs). Holds `WalletLedger` + `LedgerIndexes` + a queued-failure pump (`ConcurrentMutation`) + a `ChaCha20Rng` reserved for future RNG-driven fixtures. Constructors mirror PR 1's `MockDaemon`: `with_seed(master, ROLE_LEDGER)`, `with_seed_and_state`, plus a `queue_concurrent_mutation` helper for failure injection. `ROLE_LEDGER` was reserved in PR 1's `test_support.rs` and is now consumed. - **`Engine::replace_ledger(self, ledger: L2) -> Engine`** mirrors `Engine::replace_daemon` from PR 1. `#[cfg(test)] pub(crate)` for now; retires alongside the Stage 4 trait-promotion / production-constructor generalization at V3.2 per the [`docs/V3_ENGINE_TRAIT_BOUNDARIES.md`](./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 via `MockLedger.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 in [`docs/completed/STAGE_1_PR_2_LEDGER_ENGINE.md`](./completed/STAGE_1_PR_2_LEDGER_ENGINE.md) §2.3. - **`engine_trait_bench_ledger_balance` criterion + iai-callgrind bench pair** under `rust/shekyl-engine-core/benches/`, gated on the existing `bench-internals` Cargo feature. Measures the `LedgerEngine::balance` trait method against a 1024- `TransferDetails` state-populated fixture (`LocalLedger::populate_for_bench` injects state through a `bench-internals`-only escape hatch; production state remains behind the trait-dispatched mutating path). The `engine_trait_bench_ledger_synced_height` pair from Stage 0 PR-2 carries forward and gains a cumulative-delta row at the PR-tip SHA `8efae3a40` per §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 for `engine_trait_bench_ledger_balance` (instructions=20580 on a 1024-`TransferDetails` fixture) are now transcribed into [`docs/PERFORMANCE_BASELINE.md`](./PERFORMANCE_BASELINE.md) from N=3-invariant CI `workflow_dispatch` runs `25307774464`, `25307777614`, `25307781436` against PR-tip `8efae3a40`, following the "do-not-transcribe-laptop-captures" discipline established during Stage 0 PR-2. The PR-tip SHA `8efae3a40` includes two preparatory script commits (`80d913ea2`: extend `BENCHES` row 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. - **`DaemonEngine` trait extracted; `Engine` parameterized over the daemon implementor (Stage 1 PR 1, the first trait-boundaries PR per [`docs/V3_ENGINE_TRAIT_BOUNDARIES.md`](./V3_ENGINE_TRAIT_BOUNDARIES.md) §2.5).** The Phase 2a `DaemonEngine` slice of the Stage 1 trait-extraction work lands as `pub(crate)` on `shekyl-engine-core`. The PR's primary surface — the `DaemonEngine` trait and the `Engine` / `OpenedEngine` parameterization — is `pub(crate)` and only visible to crate-internal callers; existing public types (`Engine`, `OpenedEngine`, `DaemonClient`, the lifecycle / refresh / pending re-exports in `lib.rs`) keep their existing shapes for non-test consumers via the `D = DaemonClient` default. The one externally-visible surface change is the removal of the previously-public `DaemonClient::inner()` accessor (called out under "Removed" below); cross-workspace audit found zero remaining callers, and the functionality is preserved via `DaemonClient`'s direct `Rpc` impl. - **`pub(crate) trait DaemonEngine: Rpc + Clone + Send + Sync + 'static`** in [`engine::traits::daemon`](../rust/shekyl-engine-core/src/engine/traits/daemon.rs). `type Error: Into`. Stage 1 surface per §2.5: two method signatures (`get_fee_estimates`, `submit_transaction`) defined as `impl Future` (the in-trait-async stable form) so the trait is dyn-incompatible by design and every consumer monomorphizes against a concrete `D`. Method bodies on `DaemonClient` are `todo!()` 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 are `pub(crate)` and grow additively; Phase 2a may extend `FeeEstimates` with `estimated_block_height` / `estimation_timestamp` etc. and `TxSubmitOutcome` with richer dedup context without breaking callers. - **`Engine` and `OpenedEngine`.** 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 on `engine_trait_bench_ledger_synced_height` is 0% (10 → 10 instructions) since the bench's call path doesn't observe the daemon parameter. Each `pub` item bounded by the `pub(crate)` `DaemonEngine` trait carries an `#[allow(private_bounds)]` annotation with a centralized rationale on the `Engine` struct definition; the annotations clear at Stage 4 when the trait promotes to `pub` per `V3_ENGINE_TRAIT_BOUNDARIES.md` §1.4. - **`DaemonClient` now implements `Rpc` directly** by delegating each method to its inner `SimpleRequestRpc`. The previous `DaemonClient::inner()` accessor is removed; in-tree callers (`engine::refresh::*`) bind against `DaemonEngine` or `Rpc` instead of reaching through to the wrapped transport. `From for IoError` lands in [`engine::error`](../rust/shekyl-engine-core/src/engine/error/io.rs) to satisfy `DaemonEngine::Error: Into` for the `DaemonClient` impl. - **`MockDaemon` (renamed from `MockRpc`) extends to a full `DaemonEngine` implementor** in [`engine::test_support`](../rust/shekyl-engine-core/src/engine/test_support.rs). Adds `submit_transaction` deduplication by deterministic tx hash, `get_fee_estimates` returning a fixed snapshot (configurable via `set_fee_estimates`), fee-error queueing, submit-error queueing, and the `with_seed` / `with_seed_and_chain` constructors that carry a `ChaCha20Rng` reserved 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). - **`MockDaemon` chain-indexing convention now matches the real-daemon protocol** (`chain[0]` is genesis at height 0; `chain[h]` is the block at height `h`; `get_height` returns `chain.len()`). The previous off-by-one convention (`chain[i]` was the block at height `i + 1`) was a latent contradiction that surfaced as soon as a hybrid test composed `MockDaemon` with the production producer's range derivation. Aligning the conventions removes the bug-attractor; the existing `refresh_driver_tests` were 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]`** in `engine::test_support` (HKDF-SHA256 per `V3_ENGINE_TRAIT_BOUNDARIES.md` §6.2). The first role tag `ROLE_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(self, daemon: D2) -> Engine`** in [`engine::lifecycle`](../rust/shekyl-engine-core/src/engine/lifecycle/support.rs). Move-rebuild helper for the §6.3 hybrid-construction discipline: real `Engine::create` with a dummy `DaemonClient` pays the lifecycle cost once (file lock, KDF, ledger init, refresh slot), then `replace_daemon(mock)` swaps in the `MockDaemon` for the measured region. Test-only visibility; cleanup target is V3.2 alongside the production-constructor generalization over `D: 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`.** Wires `MockDaemon` as the engine's daemon component for a real `start_refresh` invocation (fresh wallet at `synced_height = 0`, six-block chain at heights 0..=5), asserting (a) the producer derives `processed_height_range == 1..6`, (b) `blocks_processed == 5` (post-genesis only), (c) post-refresh `synced_height() == 5`, (d) the refresh slot releases within 5s of `join().await` returning. 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 existing `start_refresh_integration_tests` module exercises only the unreachable-daemon error path). - **Closes the FOLLOWUPS.md V3.1 row "Generic `DaemonClient` so `MockRpc` can drive `start_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: the `engine_trait_bench_ledger_synced_height` cumulative-delta row for this PR's tip is captured via GHA `workflow_dispatch` (N=3 invariance) and appended to [`docs/PERFORMANCE_BASELINE.md`](./PERFORMANCE_BASELINE.md) in 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) deletes `tests/core_tests/{tx_validation,fcmp_tests,staking}.{cpp,h}` (~2200 lines, 32 registered tests + 7 already-disabled struct decls), the `chaingen_main.cpp` registrations, and the dead helpers `apply_fcmp_pipeline` / `construct_fcmp_tx` / `construct_fcmp_staked_tx` in `chaingen.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 that `cryptonote_format_utils.cpp:295` rejects under v3-from-genesis ("Shekyl requires tx version >= 3"); no chain ever materializes on the synthetic side, so `fill_tx_sources_and_destinations` returns 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 including `gen_fcmp_tx_valid`. The invariants those tests covered migrate to Rust per [`docs/FOLLOWUPS.md`](./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. Closes [`docs/CI_BASELINE.md`](./CI_BASELINE.md) cluster C. - **`DaemonClient::inner()` accessor** in [`engine::daemon`](../rust/shekyl-engine-core/src/engine/daemon.rs). The method exposed the wrapped `SimpleRequestRpc` so callers could invoke `Rpc` methods through it; with the Stage 1 PR 1 parameterization, `DaemonClient` implements `Rpc` directly 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; per `15-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 replace `client.inner().get_height()` with `client.get_height()` (the `Rpc` supertrait is in scope wherever `DaemonClient` is) with no functional difference. ### Changed - **Rust workspace clippy and rustfmt CI gates** in [`.github/workflows/build.yml`](../.github/workflows/build.yml) (`Rust: audit, test, determinism` job, immediately after `cargo 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 via [`rust/Cargo.toml`](../rust/Cargo.toml) `[workspace.lints.clippy]` (`let_underscore_must_use`, `cast_possible_truncation`, `uninlined_format_args`, et al.); `-D warnings` extends 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 --all` over 15 Rust files (mechanical import-sort and module-declaration reordering, zero behavior change). - 12 machine-applicable clippy auto-fixes (9 `clone_on_copy` deref + 3 `uninlined_format_args` inlines). - 19 `let_underscore_must_use` cures 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_from` at the test-loop cast site, `RefreshHandleFixture` typedef, and per-item `#[allow(dead_code)]` on the Phase 2a-stub `DaemonEngine` trait 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.md` Rust style and lints section.** New section between "CI baseline" and "Branch protection on `dev`" documenting the two new gates, the workspace-vs-per-item-vs- module suppression hierarchy (`[workspace.lints.clippy] allow` in `rust/Cargo.toml` for 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 on `dev`" 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 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@v2` replaces `actions/cache@v5` in the `rust-audit-and-test` CI job** in [`.github/workflows/build.yml`](../.github/workflows/build.yml). The prior cache strategy had three documented waste modes measured against dev tip `1155c1abe`: - ~8m44s post-job cache UPLOAD on every run, regardless of whether the cache key changed (see [`docs/CI_TIMING_BASELINE.md`](./CI_TIMING_BASELINE.md) "Per-step breakdown"). `actions/cache@v5` re-uploads the full path set when the cache key differs from what was restored; `Swatinem/rust-cache@v2` writes deltas only. - No `rustc` version component in the cache key, so a toolchain bump (e.g. 1.94.0 → 1.95.0 as occurred mid-cycle on the `ubuntu-latest` runner) would have silently restored a 1.94-built `target/`. Swatinem's default key includes `rustc --version`. - No `~/.cargo/bin` caching, so `cargo install cargo-audit --locked` recompiled from source every run (~2m34s). Swatinem caches `~/.cargo/bin` by default; combined with `--locked` idempotency, the install becomes a few-second metadata check on cache hits. The `install cargo-audit` step 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-tightening` branch tip `911989b24`, toolchain 1.95.0; full breakdown in `docs/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 tip `1155c1abe`) → 37m 24s cold, 35m 57s hot. Headline numbers are noisy because `cargo test` swings ±~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_iterate` disposition (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, and `cargo-binstall` migration are enumerated as deferred follow-ups in `docs/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. - **`docs/CI_TIMING_BASELINE.md` introduced** 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 the `chore/ci-cache-tightening` baseline 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.). Per `91-documentation-after-plans.mdc`, this file lives under `docs/` rather than scratch so future readers don't have to re-derive baselines from `gh run` logs. ### Fixed - **CI Post Run cleanup no longer surfaces `##[error]ENOENT` on `rust/target/tests/target` for the `Rust: audit, test, determinism` job.** The `Swatinem/rust-cache@v2` post-run cleanup walker ([`src/cleanup.ts` `cleanProfileTarget`](https://github.com/Swatinem/rust-cache/blob/v2.7.5/src/cleanup.ts#L41-L51)) treats any `target/` subdirectory named `tests` as a [`kaos`](https://github.com/vertexclique/kaos) / [`macrotest`](https://github.com/eupn/macrotest) / [`trybuild`](https://github.com/dtolnay/trybuild) nested-workspace layout and recursively cleans both `tests/target/` and `tests/trybuild/`. The recursive `cleanTargetDir` calls are not awaited, so async ENOENT rejections on missing paths escape the synchronous `try`/`catch` and surface as `##[error]ENOENT: opendir rust/target/tests/target` annotations 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.rs`](../rust/shekyl-logging/tests/trybuild.rs) uses `dtolnay/trybuild`, which creates `rust/target/tests/trybuild/`. We do not use `kaos`/`macrotest`, so `rust/target/tests/target/` never gets created — the walker tries it anyway. Confirmed against [Swatinem/rust-cache#144](https://github.com/Swatinem/rust-cache/issues/144) (open since 2023; the user-proposed `if (e.code === "ENOENT") continue;` patch never landed). Workaround: a defensive `mkdir -p rust/target/tests/target` step runs as the last pre-cleanup step in the job, ensuring the walker's `opendir` call 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 adds `await` to the recursive `cleanTargetDir` calls), and the dependency chain (`shekyl-logging` `trybuild` test → `target/tests/trybuild/` → walker → `target/tests/target/` ENOENT). Files touched: [`.github/workflows/build.yml`](../.github/workflows/build.yml) in the `Rust: audit, test, determinism` job (one new step after `determinism 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-latest` GitHub Actions runner advanced past 1.94.0 (which is what the preceding `chore/workspace-fmt-clippy-baseline` PR was triaged against). Without this fix the `cargo clippy --workspace --all-targets --keep-going -- -D warnings` gate added in that PR rejects every push to `dev`. - `clippy::useless_conversion` (×3 in vendored `rust/shekyl-oxide/`): `for (a, b) in xs.into_iter().zip(ys.into_iter())` → `for (a, b) in xs.into_iter().zip(ys)`. `Iterator::zip` accepts any `IntoIterator`, so the inner `.into_iter()` was redundant. Sites: - [`rust/shekyl-oxide/crypto/generalized-bulletproofs/src/inner_product.rs`](../rust/shekyl-oxide/crypto/generalized-bulletproofs/src/inner_product.rs) lines 216 and 220 (BP++ inner-product reduction `g_bold` / `h_bold` recursion). - [`rust/shekyl-oxide/shekyl-oxide/fcmp/bulletproofs/src/plus/weighted_inner_product.rs`](../rust/shekyl-bulletproofs/src/plus/weighted_inner_product.rs) line 380 (verifier folding loop over commitment pairs `(L_i, R_i)`). - `clippy::unnecessary_sort_by` (×2 in Shekyl-native `shekyl-scanner`): [`rust/shekyl-scanner/src/coin_select.rs`](../rust/shekyl-scanner/src/coin_select.rs) lines 114–115. Both calls sort descending by the second tuple element; rewrote to `sort_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 under `rust/shekyl-oxide/` are already Shekyl-modified relative to the `monero-oxide` fork pin (`UPSTREAM_MONERO_OXIDE_COMMIT=3933664`, sync 2026-04-25); a prior commit (`44fe03453 chore: resolve all clippy warnings across the Rust workspace`) rewrote `inner_product.rs` with +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-oxide` `crypto/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 treating `rust/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-bulletproofs` | 5 / 5 | | `shekyl-scanner` | 47 / 47 (1 ignored, pre-existing) | Local `cargo clippy --workspace --all-targets --keep-going -- -D warnings` on 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`. The `shekyl-cli`, `shekyl-ffi`, `shekyl-scanner`, `shekyl-tx-builder`, `shekyl-daemon-rpc`, `shekyl-fcmp`, `shekyl-crypto-pq`, `shekyl-proofs`, `shekyl-address`, `shekyl-shard-visual`, and the `monero-oxide` family (`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` → `Engine`, `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-cli` user-facing strings, help text, REPL prompts (`shekyl-cli [engine]>`), and command names (`engine_info` replaces `wallet_info`) now use "engine" terminology throughout per Option α. The `--wallet-dir` / `--wallet-file` flags are renamed to `--engine-dir` / `--engine-file`. - **Filesystem layout.** Default home directory subtree `~/.shekyl/wallets/` is renamed to `~/.shekyl/engines/`. The `.wallet` and `.wallet.keys` file extensions are retained so that existing tooling and the file format documentation in [`docs/WALLET_FILE_FORMAT_V1.md`](WALLET_FILE_FORMAT_V1.md) stay valid. - **What is *not* renamed in this release.** 1. **FFI C ABI symbols.** `shekyl_wallet_*` `#[no_mangle]` exports and the `ShekylWallet` opaque-handle struct retain their names. The internal Rust types backing those handles are renamed; the C ABI is held stable until the C++ `wallet2.cpp` retirement work in V3.2 lets us cut both at once. See FOLLOWUPS V3.2. 2. **C++ JSON-RPC method names.** `wallet_*` JSON-RPC method strings exposed by the C++ `shekyl-wallet-rpc.exe` binary 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. 3. **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. - **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. ### Added - **`Engine::refresh` driver and `produce_scan_result` producer (Phase 2a `refresh_scan_loop` bundle, Branch 1).** The [`shekyl_engine_core::engine::refresh`](../rust/shekyl-engine-core/src/engine/refresh.rs) module ships the snapshot-merge-with-retry sync driver that replaces the standalone `shekyl-scanner::sync::run_sync_loop`. Public surface: - `Engine::refresh(&mut self, opts: &RefreshOptions, runtime: &tokio::runtime::Handle) -> Result` — synchronous entry point on `Engine`. Captures a `LedgerSnapshot` of the wallet's current `(synced_height, reorg_blocks)` under a brief read borrow, drops the borrow, drives the async producer on `runtime`, and merges the result back via `apply_scan_result_to_state` under `&mut self`. On `RefreshError::ConcurrentMutation` the snapshot is re-taken and the call retries up to `RefreshOptions::max_retries`. - `produce_scan_result(rpc, scanner, &LedgerSnapshot, height_range, cancel) -> Result` — `pub(crate)` async producer that fetches blocks via the `Rpc` trait, scans them with `shekyl_scanner::Scanner`, detects reorgs by comparing `header.previous` against the snapshot's `reorg_blocks` (with a `find_fork_point` walk on mismatch), and returns a typed `ScanResult` envelope rather than mutating wallet state in place. Reorgs surface as `ScanResult::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 (not `Arc`-wrapped) per the snapshot benchmark in `rust/shekyl-engine-core/benches/refresh_snapshot.rs`, which measures clone cost across realistic reorg-window sizes so any future `Arc` switch has an empirical baseline. - `RefreshOptions { max_retries: u32 }` — caller-supplied knobs for the snapshot-merge retry loop. Default `8`; rationale on the bound is in the decision-log entry *"Snapshot-merge-with-retry semantics for `Wallet::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, merge_attempts }` — caller-visible result of a successful refresh. `#[non_exhaustive]`; `stake_events` is reserved for Phase 2b's richer event vocabulary and is always `0` today. - `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 from `ProduceError::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 an `await`. The contract is locked in `docs/V3_WALLET_DECISION_LOG.md` *"`Wallet::refresh` snapshot-merge-with-retry"* (2026-04-26), *"`MalformedScanResult`: producer-bug signal vs. `ConcurrentMutation`"* (2026-04-26), and *"Retire `shekyl-scanner::sync::run_sync_loop` (Phase 2a/4b boundary)"* (2026-04-27). The `RefreshHandle` async surface (cancel-on-drop, watch-based `RefreshProgress`, `AlreadyRunning` enforcement, `start_refresh` spawning) 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`'s `mod 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). The `MockRpc` test scaffold and `make_synthetic_block` helper live in `rust/shekyl-engine-core/src/engine/test_support.rs` for deterministic fault injection across producer and driver suites. - **`Engine::start_refresh` async refresh handle (Phase 2a `refresh_scan_loop` bundle, Branch 2).** The [`shekyl_engine_core::engine::refresh`](../rust/shekyl-engine-core/src/engine/refresh.rs) module ships the cancel-on-drop / one-at-a-time / progress- channel handle that wraps the snapshot-merge driver from Branch 1. 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>, opts: RefreshOptions) -> Result` — async constructor on `Engine`. Claims a `RefreshSlot` under a brief read borrow, spawns a producer task, and returns a handle observing the running task. A second call while a handle is alive returns `RefreshError::AlreadyRunning`. The `Arc>>` 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` (clonable observer of phase / height / blocks-processed / blocks-total updates), `cancel()` (idempotent; fires the shared `CancellationToken`), `is_running() -> bool` (non- blocking poll of the producer's `JoinHandle::is_finished`), `async fn join(self) -> Result` (push-completion via internal `oneshot`; consumes the handle). `Drop for RefreshHandle` is cancel-only — slot release lives on producer task exit, not on handle drop, so the cancel contract is `Drop`-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 a `tokio::sync::watch` channel. Per-attempt semantics: `blocks_total` is the per-retry total, not a cumulative running count. The watch channel is seeded by `Engine::start_refresh` with the wallet's current `synced_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. `Scanning` covers fetch + scan of a per-block batch; `Merging` covers the brief write-locked `apply_scan_result` call; `Retrying` is published when the merge returned `ConcurrentMutation` and the loop is about to retake the snapshot; `Cancelled` is published before the handle's completion `oneshot` fires `Err(Cancelled)`. - `RefreshOptions` extended 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::AlreadyRunning` becomes 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), and `mod start_refresh_integration_tests` (three integration tests against the real engine + unreachable- daemon: `start_refresh` propagates `IoError::Daemon` via `join`, concurrent `start_refresh` returns `AlreadyRunning`, drop releases the slot for a subsequent `start_refresh`). A `pub(crate) fn for_test(...)` constructor on `RefreshHandle` is the testability seam that lets the surface tests run without spinning up an `Engine`. The decision-log scope-closing entry is *2026-04-27 — `RefreshHandle` (Phase 2a Branch 2) ships transitional `Arc>` 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 of `start_refresh` against synthetic block batches lands when `DaemonClient` is generic (deferred outside Branch 2; tracked under V3.1 in `docs/FOLLOWUPS.md`). - **`Engine::create` / `Engine::open_full` / `Engine::change_password` / `Engine::close` lifecycle methods on `shekyl-engine-core` (Phase 1 `lifecycle` task).** The new [`shekyl_engine_core::engine::lifecycle`](../rust/shekyl-engine-core/src/engine/lifecycle/mod.rs) module composes [`shekyl-engine-file`](../rust/shekyl-engine-file/src/), [`shekyl-crypto-pq::account::rederive_account`](../rust/shekyl-crypto-pq/src/account.rs), [`shekyl-engine-prefs`](../rust/shekyl-engine-prefs/src/), [`shekyl-engine-state::WalletLedger`](../rust/shekyl-engine-state/src/wallet_ledger.rs), and [`shekyl-engine-state::LedgerIndexes`](../rust/shekyl-engine-state/src/ledger_indexes.rs) into the `Engine` orchestrator's open / create / rotate / close surface. Public API: - `Credentials<'a>` — forward-compatible authentication parameter. V3.0 has a private `password: &'a [u8]` field reachable through `Credentials::password_only(&[u8])` and `Credentials::password()`; V3.1 adds `authenticator: Option>` and `Credentials::password_with_authenticator(pwd, auth)` without breaking existing call sites. See `docs/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` typed-sum return for `open_full`. `Loaded(Engine)` 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 at `from_height = restore_height_hint` and the caller must drive a refresh to rebuild state. See `docs/V3_WALLET_DECISION_LOG.md` *"`Wallet::open_full`: lost-state surfacing via typed `OpenedWallet` sum"* (2026-04-26). - `EngineCreateParams<'a>` (9 public fields) and `CapabilityInput<'a>::Full { master_seed_64, seed_format }` for `Engine::create`. ViewOnly / HardwareOffload `CapabilityInput` variants are deferred alongside the matching `open_*` 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, OpenError>` — delegates to `WalletFile::create` with derived `DerivationNetwork` / `SeedFormat`, runs `rederive_account` to populate `AllKeysBlob`, cross-checks `blob.classical_address_bytes` against the envelope's `expected_classical_address` (failure → `KeyError::PublicBytesMismatch`), initializes `WalletLedger::empty()` and `LedgerIndexes::empty()`, persists initial prefs via `WalletFile::save_prefs`, and assembles the `Engine` instance. - `Engine::open_full(base_path, &credentials, network, daemon, overrides) -> Result, OpenError>` — opens the envelope (mapping `WalletEnvelopeError::InvalidPasswordOrCorrupt` to `OpenError::IncorrectPassword`, `RequiresMultisigSupport` to `OpenError::RequiresMultisig`, and capability / network mismatches to the corresponding typed variants), enforces FULL-only on this entry point (`OpenError::CapabilityMismatch` if the disk envelope is ViewOnly or HardwareOffload), runs the same rederive + public-bytes-cross-check sequence as `create`, surfaces tampered prefs as a structured `tracing::warn!` and falls back to defaults per `docs/WALLET_PREFS.md §5`'s advisory failure policy, rebuilds `LedgerIndexes` from the persisted `LedgerBlock`, and returns `Loaded` or `Restored { from_height }` based on the `WalletFile::open` outcome. - `Engine::open_view_only(...)` / `Engine::open_hardware_offload(...)` — signature-only stubs that return `OpenError::CapabilityNotYetImplemented { capability }` pending the matching `shekyl-crypto-pq` `AllKeysBlob` constructors. The error variant is deletion-tracked at the code site and in `docs/FOLLOWUPS.md` *V3.0 → "View/HW lifecycle bodies in `shekyl-engine-core`"*. See `docs/V3_WALLET_DECISION_LOG.md` *"`Wallet` lifecycle: capability scoping for V3.0"* (2026-04-26) for the stub-shape rationale. - `Engine::change_password(&old, &new, new_kdf) -> Result<(), OpenError>` — delegates to `WalletFile::rotate_password`, mapping `WalletEnvelopeError::InvalidPasswordOrCorrupt` to `OpenError::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 with `OpenError::OutstandingPendingTx { count }` when `outstanding_pending_txs() > 0` (drives cross-cutting lock 4's "no clean close while reservations are live" invariant). Otherwise saves state via `WalletFile::save_state`, saves prefs via `WalletFile::save_prefs`, and consumes `self`. The method's doc comment names the zeroization chain explicitly: `WalletFile::Drop` releases the advisory lock on `.keys`; `AllKeysBlob::Drop` zeroizes `spend_sk` / `view_sk` / `ml_kem_dk` and the public-key fields. The chain is single-level (`Engine.keys: AllKeysBlob` directly, no wrapper), and the underlying `Drop` semantics are tested in `shekyl-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`, the `Restored { from_height }` lost-state path (state file deleted between create and open), `OpenError::OutstandingPendingTx` (close refused while a synthetic reservation is in `Engine::reservations`), the structured `tracing::warn!` on prefs HMAC tamper events, and the typed `OpenError::CapabilityNotYetImplemented` returns from the view-only and hardware-offload stubs. The `apply_scan_result_post_open_works` lifecycle ↔ scan-result composition test is deferred to the Phase 2a `refresh` commit where it can exercise a real `ScanResult` against the lifecycle's `LedgerIndexes::rebuild_from_ledger` output. The lifecycle commit ships `tracing = "0.1"` as a runtime dependency on `shekyl-engine-core` (used for the prefs-tamper warn log only) and `tempfile = "3"` plus `tokio = { version = "1", features = ["macros", "rt"] }` as dev-dependencies (lifecycle tests construct on-disk fixtures and instantiate a `SimpleRequestRpc` against an unreachable URL for the dummy `DaemonClient`). - **`Engine::build_pending_tx` / `submit_pending_tx` / `discard_pending_tx` three-method `PendingTx` lifecycle (Phase 1 `pending_tx` task).** The new [`shekyl_engine_core::engine::pending`](../rust/shekyl-engine-core/src/engine/pending.rs) module 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 by `build_pending_tx`. `tx_bytes` is `Vec::new()` in Phase 1 and is explicitly documented as Phase-2a's integration point for `shekyl-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` — selects largest-amount-first spendable outputs from `LedgerIndexes`/`LedgerBlock` (excluding outputs already reserved by another in-flight `PendingTx`), captures real chain state (`synced_height` + `block_hash_at(synced_height)`), bumps a monotonic `next_reservation_id`, and inserts a `Reservation` into `Engine::reservations`. Phase 1 uses a fixed `STUB_FEE_ATOMIC_UNITS = 1_000` stub fee; Phase 2a will replace it with a `daemon.get_fee_estimates()` call. - `Engine::submit_pending_tx(id) -> Result` — runs the cross-cutting-lock-4 invariants (`PendingTxError::TooOld { built, current, max_reorg }` against `NetworkSafetyConstants::for_network(network).max_reorg_depth`, `PendingTxError::ChainStateChanged { height }` against the stored `built_at_tip_hash`, `PendingTxError::UnknownHandle` for unknown `id`s), and on success removes the reservation, marks each selected `TransferDetails` as `spent = true` with `spent_height = None` (the "unconfirmed-spent" Phase-1 state, made proper in Phase 2a once daemon broadcast confirmation arrives), and returns a stub `TxHash` whose first 8 bytes encode the `ReservationId`. - `Engine::discard_pending_tx(id) -> Result<(), PendingTxError>` — idempotent: returns `Ok(())` regardless of whether `id` is currently recognized, releases the reservation entry so the referenced outputs become selectable by a subsequent build. - `Engine::outstanding_pending_txs() -> usize` — count accessor used by `Engine::close` (lifecycle commit) to refuse closing while any reservation is active. Reservations live exclusively on `Engine` as a runtime-only `BTreeMap` field alongside the existing runtime-only `indexes: LedgerIndexes`. They are not persisted in `WalletLedger.bookkeeping`; `BOOKKEEPING_BLOCK_VERSION` does not change. Process crash between build and submit/discard drops reservations along with the in-memory `PendingTx` handle — 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, ...)` so unit tests can drive the full lifecycle without standing up an `Engine` (whose constructors land in the lifecycle commit). Twelve unit tests cover output reservation, the reserved-output filter, insufficient-funds, the no-block-yet `SendError::CannotSign`, all three `PendingTxError` paths, the spent-state mutation on submit, the rebuild-after-discard path, discard idempotency on unknown handles, and `FeePriority::Custom` preservation. See `docs/V3_WALLET_DECISION_LOG.md` *"Reservation tracker: runtime-only on `Wallet`, never persisted"* (2026-04-26 sub-section of the `Wallet` struct entry) for the runtime-vs-persisted decision and the supersession of the original cross-cutting-lock-4 draft phrasing. - **`shekyl_engine_core::scan::ScanResult` typed scanner-output value and `Engine::apply_scan_result` merge surface (Phase 1 `scan_result` task).** A new [`shekyl_engine_core::scan`](../rust/shekyl-engine-core/src/scan.rs) module defines the additive event vocabulary the Phase 2a `Engine::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; `RecoveredWalletOutput` already `ZeroizeOnDrop`, so dropping the enclosing `ScanResult` wipes PQC re-derivation material in place. - `KeyImageObserved { block_height, key_image }` — drives `LedgerIndexes::detect_spends` per height. - `StakeEvent::Accrual { height, record }`, `#[non_exhaustive]` so Phase 2b `StakeInstance` variants can land additively. - `ReorgRewind { fork_height }` — drives `LedgerIndexes::handle_reorg` before 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 in [`engine::merge`](../rust/shekyl-engine-core/src/engine/merge.rs) and is the only audited code path that mutates the scanner-derived slice of `WalletLedger` plus `LedgerIndexes` during refresh. It enforces two snapshot-consistency invariants before applying any events, rejecting with `RefreshError::ConcurrentMutation` on either failure: 1. **Start-height equality.** `processed_height_range.start` must equal `synced_height + 1` (or `fork_height` when `reorg_rewind` is present, since the rewind sets `synced_height` to `fork_height - 1` first). 2. **Parent-hash chain.** `parent_hash` must match `LedgerBlock::block_hash_at(start - 1)`, with `None` matching `None` at genesis (`start == 1`). The merge runs in a fixed order: optional reorg rewind first, then per-height ingest (`process_scanned_outputs` + `detect_spends`) driven by `block_hashes` so `synced_height` advances exactly once per scanned block — even when the block had no events — then staker-pool aggregate events. `Engine` now carries `indexes: LedgerIndexes` as a direct field so the merge can mutate both the persisted `LedgerBlock` (via `WalletLedger.ledger`) and the runtime indexes under a single `&mut self` borrow without needing an inner lock. The full merge body is exposed `pub(crate)` as `apply_scan_result_to_state(&mut LedgerBlock, &mut LedgerIndexes, ScanResult)` so tests can drive it without standing up a full `Engine` (whose lifecycle methods land in a follow-up commit). See `docs/V3_WALLET_DECISION_LOG.md` *"`ScanResult` type"* (2026-04-25, **crate location: `shekyl-engine-core::scan`**) and *"`Wallet::apply_scan_result` invariants and Wallet-side `LedgerIndexes`"* (2026-04-26). ### Changed - **`RuntimeWalletState` folded into `LedgerBlock` + `LedgerIndexes` (Phase 1 `runtime_state_audit` task).** The `RuntimeWalletState` type and the transitional `pub use ... as WalletState` re-export are deleted. Its responsibilities split along the persistence boundary: - **Persisted, on-disk state** — `transfers`, `synced_height`, `reorg_blocks`, claim watermarks — was already covered by `WalletLedger.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 on `LedgerBlock`. - **Runtime-only derived state** — the `key_images` and `pub_keys` lookup maps plus the `staker_pool` accrual aggregate — moves to a new `pub struct LedgerIndexes` in `rust/shekyl-engine-state/src/ledger_indexes.rs`. `LedgerIndexes` is **never serialized**, has no `Serialize` / `Deserialize` derives, and is rebuilt by scanner replay at every wallet open via `LedgerIndexes::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: `LedgerIndexes` is reconstructible from `LedgerBlock` plus 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 both `shekyl-engine-rpc::scanner_state` and the (cfg `rust-scanner`) `shekyl-scanner::sync` background loop. Scanner-specific behavior that needs `Timelocked` / `RecoveredWalletOutput` / `BalanceSummary` / `ClaimableInfo` lives in extension traits in `shekyl-scanner::ledger_ext` (`TransferDetailsExt`, `LedgerIndexesExt`, `LedgerBlockExt`); the canonical `shekyl-engine-state` crate stays scanner-free. The old `shekyl-scanner::runtime_ext` and `shekyl-scanner::wallet_state` modules are deleted. See `docs/V3_WALLET_DECISION_LOG.md` *"`RuntimeWalletState` audit: full fold, derived indexes rebuilt at open"* (2026-04-25); the same commit also corrects two errata in that entry: the persisted transfer path is `WalletLedger.ledger.transfers` (not `bookkeeping.transfers`), and `staker_pool`'s home on `LedgerIndexes` is now pinned explicitly. ### 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.md`](./PERFORMANCE_BASELINE.md) is rewritten from the Round 4b template stub into the per-bench frozen-baseline shape that [`docs/design/STAGE_0_HARNESS.md`](./completed/STAGE_0_HARNESS.md) §4.5 operationalizes (one populated section for `engine_trait_bench_ledger_synced_height` frozen at Stage 0 PR-2's merge SHA; four deferred-bench placeholder sections for `engine_trait_bench_ledger_balance`, `engine_trait_bench_economics_current_emission`, `engine_trait_bench_economics_parameters_snapshot`, and `engine_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-`), 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 (criterion `median_ns` is 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`](./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`](./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 the `PERFORMANCE_BASELINE.md` rewrite 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`](./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-A `3d313256c`, PR-A-extension `2e5309ad3`, and PR-C `93d515123` merged without `## [Unreleased] / ### Documentation` entries). The CHANGELOG-backfill row is targeted at V3.0 and can land any time before V3.0 cut. - **`engine_trait_bench_ledger_synced_height` frozen baseline transcribed (Stage 0 PR-2 commit 5).** The validated CI capture values (iai `instructions=10`, hardware-dependent informational rows `l1_hits=16` / `ll_hits=0` / `ram_hits=2` / `total_read_write=18` / `estimated_cycles=86`, criterion `median_ns=0.6221` / `std_dev_ns=0.005864`) are recorded in [`docs/PERFORMANCE_BASELINE.md`](./PERFORMANCE_BASELINE.md) under the bench's frozen-baseline source, gate metric, informational metric, and cumulative-delta tables. The `env-0276d210` capture 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 GHA `workflow_dispatch` run `25239954863`, one of the three N=3 invariance-verification captures (runs `25239954863`, `25239956447`, `25239958016`) that produced byte-identical iai-callgrind output (±0% variance on the gate metric per [`STAGE_0_HARNESS.md`](./completed/STAGE_0_HARNESS.md) §4.4 dynamic check). The bench's "frozen at" SHA is the capture SHA `0276d210e` (PR-2 commit 4c, post-Q `Box>` fixture); the in-tree [`reference-captures/stage-0-pr-2-c4c-shekyl_rust_v0.json`](./benchmarks/reference-captures/stage-0-pr-2-c4c-shekyl_rust_v0.json) remains 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`](./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 the `phase_2b_prep_stage_1_trait_boundaries` plan. Pins six trait surfaces (`KeyEngine`, `LedgerEngine`, `RefreshEngine`, `PendingTxEngine`, `DaemonEngine`, `PersistenceEngine`), the composition shape (`Engine` with default type parameters; concrete fields, generic-bounded methods, no `Box`), the per-trait async story, the per-trait error model (per-trait families with a single shared `EngineError` aggregate), the test boundary unlocked by `MockKeyEngine` / `MockDaemonEngine` / etc. (closes today's gap that there is no way to plug `MockRpc` into `start_refresh` end-to-end), the Stage 4 transition guarantee (the trait surface in §2 does not change at Stage 4; `kameo` actors implement the same traits with the same signatures), the Stage 1 migration order (`DaemonEngine` first to unlock integration tests; `LedgerEngine` second; 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`](../.cursor/rules/20-rust-vs-cpp-policy.mdc), the document runs through 4–6 review rounds against `dev` before 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-rpc` is settled as `HashMap>`, not `Arc>`. Per-engine concurrency control is the `kameo` mailbox; 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 an `Update (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 on `kameo >= 0.20.0` version pin, MSRV `>= 1.88` verification, and a workspace-wide bounded-mailbox default. - **Phase 1 sub-decision log entries appended (Phase 1 `decision_log_entries` task).** Three new dated entries land in `docs/V3_WALLET_DECISION_LOG.md` to lock the Phase 1 surface decisions whose defaults were taken from the Phase 0 `surface_decisions` review: - *"`RuntimeWalletState` audit: full fold, derived indexes rebuilt at open"* — `RuntimeWalletState` ceases to exist; `key_images` / `pub_keys` indexes promote into a `pub(crate) LedgerIndexes` owned by `Wallet`, rebuilt from the authoritative ledger at open time, never persisted. Schema unchanged. Closes the `runtime_state_audit` Phase 1 task and the `pub use ... as WalletState` transitional alias deletion. - *"`tx_keys` storage: persist in `TxMetaBlock`, never re-derived"* — pins the rule that per-tx randomness lives in `TxMetaBlock::tx_keys: BTreeMap` (already shipped in schema), is never reconstructed from any other state, and that `Engine::tx_proof` / `Engine::reserve_proof` (Phase 2) read it by `txid` lookup with a typed `ProofError::TxKeyNotPersisted` on miss. - *"Daemon-side `tracing` install: `shekyl_log_install_tracing_forwarder` under `shekyl-logging::ffi`"* — locks the FFI export name, signature (`pub unsafe extern "C" fn() -> i32`, idempotent, returns typed `ALREADY_INSTALLED` / `NOT_INITIALIZED`), home (`shekyl-logging::ffi`, **not** `shekyl-daemon-rpc::ffi`), and the rule that `shekyl-daemon-rpc`'s `tracing::*` call sites are kept verbatim — the forwarder routes them through `shekyl-logging` automatically. Closes the `docs/FOLLOWUPS.md` V3.2 entry *"`shekyl-daemon-rpc` staticlib: `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 `RuntimeWalletState` fold 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.md` to pin major Phase-2-and-beyond architectural commitments whose rationale must be in tree before the supporting code commits land: - *"`Wallet` renamed to `Engine`: 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 on `shekyl-core` `dev`. - *"Engine architecture: actor model with staged migration from composition"* — pins the migration of `Engine` from composition to an actor model with `kameo` as the framework, over five staged actor builds plus a Stage 1 framework- agnostic preparation pass. Stage 2 introduces `kameo` and builds `KeyEngine` first (smallest internal state, cleanest privacy boundary, framework-friction surfaces with bounded blast radius). Stage 3 builds `StakeEngine` native-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) builds `ArchivalEngine` as a sibling to `StakeEngine` (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`, with `inspect`, `adjust_fee`, `sign_partial`, `aggregate_signatures`, `export` as additional pending-tx operations). The single- phase `send(request, confirm_fn) -> Result` 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.md` updates land in the same commit: - V3.0 — Stage 2 `KeyEngine` migration; Stage 3 `StakeEngine` native build; Stage 4 remaining-subsystem migrations (`DaemonEngine`, `PersistenceEngine`, `PendingTxEngine`, `RefreshEngine`, `LedgerEngine` in suggested order); RPC boundary refinements (idle eviction with TBD-at-implementation rationale, `engine_lock` JSON-RPC method, multi-engine registry, snapshot reads from `LedgerEngine`, multi-peer archival routing client surface). - V3.1 — sibling resolution entry for the `assemble_tree_path_for_output` bug, locking the resolution architecture (foundation `--no-prune` archival as floor; staker-distributed archival via `ArchivalEngine` as primary path; multi-peer routing against per-block root snapshots). The original bug entry is preserved untouched as historical record. - V3.x — Stage 5 `ArchivalEngine` native build (simulation- gated); no-tradeability invariant codification placeholder cross-referencing `docs/V3_SHARD_VISUALIZATION.md` and `docs/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` over `RefCell` / 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-core` `dev`; Stage 1 and beyond ship over subsequent PRs per the locked stage sequence in the actor-architecture decision-log entry. - **`docs/V3_STAKER_ARCHIVAL.md` and `docs/V3_SHARD_VISUALIZATION.md` added under `shekyl-core/docs/` (relocated and rescoped from `shekyl-dev/docs/V4_*`).** Two design documents covering the staker-distributed chain-history archival mechanism and the deterministic shard visualization surface relocate from the `shekyl-dev` planning workspace to the `shekyl-core` canonical 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 established `ArchivalEngine` as a sibling to `StakeEngine` and `shekyl-shard-visual` as a domain-primitive library crate. The earlier `docs/V4_STAKER_ARCHIVAL.md` and `docs/V4_SHARD_VISUALIZATION.md` copies in `shekyl-core/docs/` (added in commit 9dc44687d) are removed in this commit; the V3-named documents are the canonical homes going forward. The companion `git rm` of the V4-named drafts from `shekyl-dev/docs/` ships as a separate commit on `shekyl-dev` `dev` that 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. 1. **`shekyl-engine-core` crate-root re-exports for `Refresh*` types.** [`rust/shekyl-engine-core/src/lib.rs`](../rust/shekyl-engine-core/src/lib.rs) now re-exports `RefreshHandle`, `RefreshOptions`, `RefreshPhase`, `RefreshProgress`, `RefreshReorgEvent`, and `RefreshSummary` alongside the `RefreshError` it already re-exported. Downstream callers (CLI, JSON-RPC server, benches, FFI) no longer have to reach through `engine::refresh::*`. The `engine` module itself already re-exported the full set ([`engine/mod.rs:168–170`](../rust/shekyl-engine-core/src/engine/mod.rs)). 2. **CHANGELOG `[Unreleased]` editorial sweep — `Wallet` → `Engine` running prose.** Phase 1 and Phase 2a Branch 1 bullets (lifecycle, pending-tx, scan-result, refresh-driver, struct, module-skeleton) carried `Wallet` / `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 cited `docs/V3_WALLET_DECISION_LOG.md` entries still carry their historical titles. 3. **`docs/FOLLOWUPS.md` V3.1 row added — `transfer_details` Rust migration.** [`.cursor/rules/15-deletion-and-debt.mdc`](../.cursor/rules/15-deletion-and-debt.mdc) cites `transfer_details` Rust migration as V3.1 scope; the row now exists. Rewrites each C++ consumer of `struct transfer_details` (balance, output selection, key-image / spend tracking, payment-id surface, password rotation, persistent wallet-cache I/O) to drive `shekyl-engine-state::TransferDetails` through FFI, then deletes the C++ struct from `src/wallet/wallet2.h` and `src/wallet/wallet_rpc_server_commands_defs.h`. Closes either at V3.1 or by superseding deletion in the V3.2 `wallet2.cpp` retirement. 4. **`docs/FOLLOWUPS.md` V3.2 row added — *"Re-examine `/FIiso646.h` and `rct::` → `ct::` deferrals."*** Reconciles a dead citation in [`docs/STRUCTURAL_TODO.md`](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). 5. **`Engine::refresh` cancellation contract pinned in the docstring at [`rust/shekyl-engine-core/src/engine/refresh.rs`](../rust/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_refresh` returning `RefreshHandle`) 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.md`]( ../.cursor/plans/phase_2b_prep_stage_1_trait_boundaries_0d37a30e.plan.md) Track B items 1–5. Track 2 (Stage 1 trait-boundaries spec, V3.2) begins after this hygiene pass lands. ### Removed - **`shekyl-scanner::sync` module and `shekyl-scanner::rust-scanner` Cargo feature retired (Phase 2a `refresh_scan_loop` bundle, Branch 1).** The standalone background-sync surface (`run_sync_loop`, `LiveLedger`, `SyncProgress`, `SyncError`) and its feature flag are deleted in favor of the `shekyl-engine-core::Engine::refresh` driver. `shekyl-scanner` becomes a pure scanning library — `Scanner`, extra-field parsing, KEM rederivation, the `LedgerBlock` / `LedgerIndexes` extension traits, balance, and coin selection — and drops its `tokio` / `tokio-util` optional dependencies along with the feature. `shekyl-engine-rpc::rust-scanner` is **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 alongside `shekyl-engine-rpc`'s Rust cutover. See `docs/V3_WALLET_DECISION_LOG.md` *"Retire `shekyl-scanner::sync::run_sync_loop` (Phase 2a/4b boundary)"* (2026-04-27) for the rationale and Phase boundary. The `sync_bookkeeping` test module in `shekyl-scanner` is retained: it exercises the `(LedgerBlock, LedgerIndexes)` state-management primitives (progress monotonicity, reorg handling, spend-detection tracking) that the producer side of `Engine::refresh` now drives, and remains load-bearing regardless of who owns the outer loop. - **`rust/shekyl-ffi/src/wallet_ledger_ffi.rs` deleted 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` / `ShekylSyncStateScalarsC` and their `shekyl_wallet_{get,set,free}_*` trios plus `shekyl_wallet_ledger_preflight` — is gone. The corresponding declarations in `src/shekyl/shekyl_ffi.h` are stripped; the reserved `SHEKYL_WALLET_ERR_BLOCK_NOT_HYDRATED` (codepoint 29) retires alongside the surface that produced it. `save_as` (the in-scope C-ABI export from `wallet_file_ffi.rs`) and its refusal codes (`SAVE_AS_CROSS_FILESYSTEM` / `SAVE_AS_TARGET_EXISTS`) remain unchanged. The `shekyl-primitives` main- and dev-dep are also removed from `rust/shekyl-ffi/Cargo.toml`; the only consumer was the deleted file's `Commitment` reconstruction path. **Caller evidence (commit message body).** Pre-flight `git grep` against `*.cpp` / `*.cc` / `*.h` / `*.hpp` for every export of the deleted surface returned only `src/shekyl/shekyl_ffi.h` itself (the prototypes that this commit removes). Zero `.cpp` consumers ever materialized — the original consumer (`wallet2_handle_views.h/.cpp`) was scheduled but never written, and Phase 5 will delete the enclosing `wallet2.cpp` shim wholesale. Full `git grep` transcript 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 `.cpp` callers, (2) grep evidence in the deletion commit's message body, and (3) atomic update of `docs/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 u8` casts, explicit `iter()` loop, `_keep_imports` arg 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](../.cursor/plans/shekyl_v3_wallet_rust_rewrite_3ecef1fb.plan.md), `primitives` task).** `SubaddressIndex` is now a `u32` newtype with `index == 0` reserved 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 in `rust/shekyl-ffi/src/wallet_ledger_ffi.rs`. Account-level concepts inherited from wallet2 (`AccountTags`, the `tag_descriptions` / `account_tags` FFI 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 separate `SubaddressLabels::primary` slot is gone too — the primary label is the `index == 0` entry of `per_index` like every other label. **FFI surface delta (this commit).** `shekyl_ffi.h` mirrors the Rust: `ShekylSubaddressRegistryEntryC` and `ShekylSubaddressLabelEntryC` carry a single `index: u32` field (sizes 36 and 24 respectively, no trailing pad — there are zero `.cpp` callers in tree, so preserving the legacy stride for hypothetical future callers would be a defensive measure for nobody); the `ShekylTagDescriptionEntryC` / `ShekylAccountTagAssignmentEntryC` typedefs and their `static_assert`s, plus the `shekyl_wallet_{get,set,free}_{tag_descriptions,account_tags,primary_label}` prototypes, are removed. The FFI file `wallet_ledger_ffi.rs` itself 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_registry` now rejects an entry with `index == 0` by returning `SHEKYL_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 test `wallet_ledger_ffi::tests::registry_set_rejects_index_zero` pins the contract. **On-disk schema.** All three persisted-block version constants are bumped from `1` to `2`: `BOOKKEEPING_BLOCK_VERSION` (the direct field-shape changes — `subaddress_registry` / `subaddress_labels.per_index` flatten and `account_tags` removal), `LEDGER_BLOCK_VERSION` (transitive — every `TransferDetails` in `LedgerBlock::transfers` now carries the flattened newtype), and `WALLET_LEDGER_FORMAT_VERSION` (transitive — the bundle's serialized bytes shift wherever any nested `SubaddressIndex` or `SubaddressLabels` appears). The strict pairing of "snap drift ↔ paired version-constant bump" is enforced by the `ci/schema-snapshot` workflow per [`docs/MID_REWIRE_HARDENING.md`](MID_REWIRE_HARDENING.md) §3.4 and [`.cursor/rules/42-serialization-policy.mdc`](../.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 ~/.shekyl` is the migration path per [`.cursor/rules/15-deletion-and-debt.mdc`](../.cursor/rules/15-deletion-and-debt.mdc). The `bookkeeping_block.snap` / `ledger_block.snap` / `wallet_ledger.snap` schema fixtures are regenerated; the `SubaddressIndex` shape went from a two-field struct to a `NewtypeStruct(u32)`, and `BookkeepingBlock::account_tags` is 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}` (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` struct + `DaemonClient` thin wrapper (Phase 1 of the [shekyl-v3-wallet-rust-rewrite plan](../.cursor/plans/shekyl_v3_wallet_rust_rewrite_3ecef1fb.plan.md), cross-cutting locks 1, 3, 4 type-layer realization).** Lands the `Engine` 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` for compile-time signer-kind dispatch. `network` and `capability` are cached from `WalletFile`'s region 1 (which is write-once after `create`) so the hot accessors are infallible and O(1). Read-only accessors (`network()`, `capability()`, `file()`, `ledger()`, `prefs()`, `daemon()`) plus a `pub(crate) keys()` for in-crate sign / proof code paths. Redacted `Debug` impl: `keys` prints as ``, `ledger` / `prefs` print as `<…>`, `file` and `daemon` delegate to their own already-redacting impls. No `Drop` impl on `Engine` itself: `AllKeysBlob` and `WalletFile` each ship their own `Drop` for the secret bytes / KEK / advisory lock; composing types that already wipe correctly is sound, and a wrapper `Drop` would risk shadowing the inner ones. New `DaemonClient` thin wrapper around `shekyl_simple_request_rpc::SimpleRequestRpc` insulates `Engine`'s public API from the transport choice and gives Phase 2a a single audited site for `get_info` network verification, `get_fee_estimates` fee-priority resolution, and tx submission. The six lifecycle methods (`create`, `open_full`, `open_view_only`, `open_hardware_offload`, `change_password`, `close`), `RefreshHandle`, `PendingTx`, and `ScanResult` each land in their own follow-up commits on this same Phase 1 branch. Cargo dependency graph: `shekyl-crypto-pq` is now a non-optional dependency of `shekyl-engine-core` (the `multisig` feature flag previously gated it; with `keys: AllKeysBlob` on the struct it is mandatory regardless of feature). Full rationale and field-by-field justification recorded in [`docs/V3_WALLET_DECISION_LOG.md`](V3_WALLET_DECISION_LOG.md) §"`Wallet` struct shape and accessor surface". - **`shekyl-engine-core::engine` module skeleton (Phase 1 of the [shekyl-v3-wallet-rust-rewrite plan](../.cursor/plans/shekyl_v3_wallet_rust_rewrite_3ecef1fb.plan.md), cross-cutting locks 2, 4, 5, 6, 7, 8 type-layer realization).** New module `rust/shekyl-engine-core/src/engine/` ships the type-layer foundations of the V3 wallet orchestrator without yet introducing the `Engine` struct 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 of `shekyl_address::Network` (the fourth `Fakechain` variant lands in a separate scoped commit on the same branch); a re-export of `shekyl_engine_file::Capability` (canonical spelling — the plan's "`CapabilityMode`" reference is satisfied); and a sealed `EngineSignerKind` trait with `SoloSigner` ZST as the V3.0 default. V3.1's `MultisigSigner` will join behind the existing `multisig` Cargo 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 in [`docs/V3_WALLET_DECISION_LOG.md`](V3_WALLET_DECISION_LOG.md) §"Per-domain `Wallet` error enums + sealed `WalletSignerKind`". - **`shekyl-engine-state::LocalLabel` and `SecretStr<'a>` (Phase 1 of the [shekyl-v3-wallet-rust-rewrite plan](../.cursor/plans/shekyl_v3_wallet_rust_rewrite_3ecef1fb.plan.md), 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. `LocalLabel` is `Zeroizing` with redacting `Debug` / `Display` (`""`); no derived `Serialize` / `Deserialize`. Persistence routes through the explicit `serde_helpers::local_label` adapter, which is wire-byte-identical to a plain `String` (test `serde_helpers::tests::local_label_postcard_wire_matches_plain_string` pins this), so the upcoming bookkeeping_block / tx_meta_block retypes will not bump `BOOKKEEPING_BLOCK_VERSION` or `TX_META_BLOCK_VERSION`. Borrowed in-process inspection goes through `LocalLabel::expose() -> SecretStr<'_>`, whose only `Display` / `Debug` output is the redaction marker; callers that need raw bytes call `SecretStr::as_str()` explicitly so the call site is the audit point. Full rationale (including why the value-typed `SecretStr<'a>` shape rather than the literal `&SecretStr` shorthand from the decision log — `unsafe_code` is forbidden workspace-wide) recorded in [`docs/V3_WALLET_DECISION_LOG.md`](V3_WALLET_DECISION_LOG.md) §"`LocalLabel` / `SecretStr` typing for locally-sensitive UTF-8". ### Changed - **`monero-oxide` vendor-bump `87acb57` → `3933664` (PR 0.6 of the [shekyl-v3-wallet-rust-rewrite plan](../.cursor/plans/shekyl_v3_wallet_rust_rewrite_3ecef1fb.plan.md), closing Operation A of the `monero-oxide` un-pin question).** Updated [`rust/shekyl-oxide/UPSTREAM_MONERO_OXIDE_COMMIT`](../rust/shekyl-oxide/UPSTREAM_MONERO_OXIDE_COMMIT) from `87acb57e0c3935c8834c8a270bd3bdcbbe36bcde` (sync_date 2026-04-06) to `3933664d0851871c976f07298b862373d1c6fec0` (sync_date 2026-04-25), the current Shekyl fork tip on `Shekyl-Foundation/monero-oxide` `fcmp++`. **No vendored source files changed.** Of the five fork commits between the two pins, the only ones with code-content deltas (`182b648` Cargo profiles + base58 decoder hardening) touched `shekyl-oxide/wallet/base58/`, a Monero-shaped wallet path that is not vendored in shekyl-core per `60-no-monero-legacy.mdc` — Shekyl uses native Bech32m via `shekyl-address` instead. The umbrella `shekyl-oxide/Cargo.toml` is byte-identical between the vendored copy and fork tip; `182b648`'s Cargo profile changes live in the fork's workspace-root `Cargo.toml`, which we do not vendor either. Workspace grep for `monero_base58 | shekyl-oxide.*base58 | ::base58::` returns zero matches across `rust/`, confirming that `shekyl-address` (Bech32m via the `bech32` crate) and no other Shekyl crate imports the fork's base58 module. The hardening itself is strictly more restrictive — `checked_add` overflow detection plus non-canonical-encoding rejection — so even a hypothetical downstream consumer would only see additional `None` returns, never different `Some(_)` payloads. Verification per [`docs/SHEKYL_OXIDE_VENDORING.md`](SHEKYL_OXIDE_VENDORING.md): `cd rust && cargo build --locked -p shekyl-fcmp` clean, `cd rust && cargo test --locked --workspace` **900 passed, 0 failed, 6 ignored** (exit 0). `ninja shekyld` skipped because PR 0.6 does not touch the C++ side and `docs/SHEKYLD_PREREQUISITES.md` already certifies the C++ daemon as ready. The `.github/workflows/shekyl-oxide-divergence.yml` CI 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 cypherstack `generalized-bulletproofs-fix` audit response and the Veridise `HelioseleneField::invert` correctness cluster) remains a separate V3.1.x follow-up per [`docs/FOLLOWUPS.md`](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 in [`docs/V3_WALLET_DECISION_LOG.md`](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 doc [`docs/MONERO_OXIDE_VENDOR_STATUS.md`](MONERO_OXIDE_VENDOR_STATUS.md) amended 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](../.cursor/plans/shekyl_v3_wallet_rust_rewrite_3ecef1fb.plan.md)). Mechanical rename across all call sites in `shekyl-engine-file`, `shekyl-engine-prefs`, `shekyl-ffi`, and the C FFI doc-comment in `src/shekyl/shekyl_ffi.h`. No ABI change (the C-ABI symbols use the `shekyl_wallet_*` prefix, not the Rust type name). Frees the `Engine` identifier for the Phase 1 `shekyl-engine-core::Engine` orchestrator and aligns the file-orchestrator type name with what it actually is — envelope, atomic IO, advisory locking, payload framing. Rationale and decision archive in [`docs/V3_WALLET_DECISION_LOG.md`](V3_WALLET_DECISION_LOG.md) ("Wallet stack greenfield Rust rewrite", 2026-04-25). ### Documentation - **`shekyld` Phase 0 prerequisites audit (PR 0.3 of the [shekyl-v3-wallet-rust-rewrite plan](../.cursor/plans/shekyl_v3_wallet_rust_rewrite_3ecef1fb.plan.md)).** New file [`docs/SHEKYLD_PREREQUISITES.md`](SHEKYLD_PREREQUISITES.md) consolidating the audit of three daemon-side prerequisites against the rewrite plan's later phases: 1. **Instant-mining regtest mode** (Phase 6 prereq): PRESENT — `--regtest --offline --fixed-difficulty 1` + `generateblocks` JSON-RPC works as inherited from Monero; V3-specific caveats documented (FCMP++ tx-type and `curve_tree_root` header checks bypassed on `FAKECHAIN`, reference-block age rules still enforced). No daemon change required. 2. **`get_fee_estimate(s)` RPC** (Phase 2a prereq): PRESENT as singular `get_fee_estimate` returning a positional 4-element `fees` vector 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. 3. **Fee policy / rules version exposure**: ABSENT entirely — no `fee_version` / `fee_policy_id` on `get_fee_estimate`, on `get_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. - **`monero-oxide` vendor freshness audit (PR 0.4 of the V3 wallet rewrite plan, [`docs/MONERO_OXIDE_VENDOR_STATUS.md`](MONERO_OXIDE_VENDOR_STATUS.md)).** Point-in-time (2026-04-25) record of where the vendored `shekyl-oxide` snapshot (`87acb57e`) sits relative to the Shekyl fork tip (`Shekyl-Foundation/monero-oxide` `fcmp++` `3933664d`, +5 commits, all non-crypto) and the original upstream (`monero-oxide/monero-oxide` `fcmp++` `0e438ae`, +40 commits since the 2025-11-22 merge base, including the cypherstack `generalized-bulletproofs-fix` audit response, the Veridise `HelioseleneField::invert` correctness 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 only `416d8d1` rename and `87acb57` extra 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 to `tests/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 (commits `2a`…`2k.4`, merged) and the C++ consumer rewire (commits `2k.5a` onward, deferred). Covers: Google Benchmark C++ baseline capture against the existing `wallet2.cpp` hot paths; criterion + iai-callgrind Rust benchmark harness mirroring the same five paths; GitHub Actions CI integration with bidirectional thresholds for `crypto_bench_*` (any drift is suspicious — constant-time property defense) and slowdown-only thresholds for `hot_path_bench_*`; rolling baseline on a dedicated `bench-baseline` branch; `postcard-schema` snapshot files with CI-enforced `block_version` bump on every drift; ripgrep + allowlist secret-wipe discipline for `shekyl-engine-state` blocks; `WalletLedger::check_invariants()` with five cross-block tripwires and a new `WalletFileError::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-fuzz` targets. Also captures the dual-path output-equivalence requirement for `2k.5b`…`2l` as 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`](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 the `ci/benchmarks` gate. New sentinel file [`docs/benchmarks/MID_REWIRE_WARNING_WINDOW.active`](benchmarks/MID_REWIRE_WARNING_WINDOW.active) toggles warning-only mode — when present, the `fail job on threshold trip` step in [`.github/workflows/benchmarks.yml`](../.github/workflows/benchmarks.yml) downgrades the would-be `::error::` annotation to a `::warning::` and exits 0, preserving the upstream `compare` / PR comment / `profile-on-fail` observability chain without blocking merges. Policy paragraph in `MID_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 of `bench-baseline`). The sentinel path is included in the workflow's `paths:` filters for both `pull_request` and `push` triggers, 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_keys` writes on SHKW1 wallets (commit 2k.b of the wallet-state-promotion plan, [`.cursor/plans/wallet-state-promotion_ab273bfe.plan.md`](../.cursor/plans/wallet-state-promotion_ab273bfe.plan.md) §2k.b).** Installs the keys-layer fault line in `wallet2::store_to` so SHKW1-backed wallets cannot silently corrupt their on-disk file by falling back to the legacy `store_keys` JSON path. The two triggers that would otherwise reach the legacy save branch — save-as (`path` differs from the current `m_wallet_file`) and password change (`force_rewrite_keys=true`, as routed from `wallet2::change_password`) — now throw a typed `tools::error::wallet_shkw1_operation_unsupported` before any wallet-state mutation (no `trim_hashchain` cache touch, no `prepare_file_names` path 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 common `store()` → `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 legacy `shekyl_encrypt_wallet_cache` path until 2l. Callers audited: `wallet2::change_password` (exposed via `wallet2_ffi.cpp` and `wallet_rpc_server.cpp`) and direct `store_to(path, pw)` invocations in `tests/wallet_bench/` and `tests/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.h` hierarchy extended with the new `wallet_logic_error` subclass carrying both the operation name and the keys file path for UX rendering. Verified locally: full shekyl-core C++ rebuild clean across `wallet`, `daemon`, `shekyl-engine-rpc`, `unit_tests`, `core_tests`, `functional_tests`; no new lints introduced. - **2k.a — rewire `wallet2` load/verify/rewrite onto the SHKW1 handle (commit 2k.a of the wallet-state-promotion plan, [`.cursor/plans/wallet-state-promotion_ab273bfe.plan.md`](../.cursor/plans/wallet-state-promotion_ab273bfe.plan.md) §2k.a).** The keys-side half of the wallet2 → Rust rewire. `wallet2::load_keys` now magic-sniffs via `shekyl_wallet_keys_inspect`; on an SHKW1 match it routes through `shekyl_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-local `TransitionalRederivationInputs` RAII wrapper (`epee::mlocked>`). `m_account.load_from_shkw1` rebuilds every derived field (classical SK/PK, view SK/PK, ML-KEM decap key, account address) from the seed; `m_account.forget_master_seed` immediately scrubs the C++ copy (Option β — the `ShekylWallet` handle is the single in-memory source of truth for the master seed post-load). An AAD-bound address-match sanity check against `ShekylWalletMetadata::expected_classical_address` catches corruption, HKDF policy drift, and handle-repoint bugs via a distinct `tools::error::wallet_keys_aad_address_mismatch`; `init_type` and `set_createtime` land atomically with the handle-stash on `m_shekyl_wallet`. `wallet2::load_keys_buf` refuses SHKW1 inputs with `error::wallet_internal_error` — the envelope requires the file-lock path and cannot be driven through a raw buffer. Both `verify_password` overloads route SHKW1 verification through `shekyl_wallet_keys_open` with 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 repoints `m_keys_file` without 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 passes `no_spend_key=false` (no in-tree caller does today; the log guarantees any future regression trips test output). `wallet2::rewrite` becomes a logged L1 no-op for SHKW1 wallets — settings writes land in 2k.b's `store_to` rewire. `wallet2::deinit` resets `m_shekyl_wallet` *before* `m_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 in `src/wallet/wallet_errors.h` discriminate 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 of `load_from_shkw1`, under `mlock`; the address-match check fires before any scalar is materialized in C++; `xor_with_key_stream` / `rederive_from_master_seed` / `decrypt` are 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 across `wallet`, `daemon`, `shekyl-engine-rpc`, `unit_tests`, `core_tests`, `functional_tests`; `cargo check -p shekyl-engine-file -p shekyl-ffi` clean. 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`](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. New [`rust/shekyl-engine-state/tests/fuzz_region2.rs`](../rust/shekyl-engine-state/tests/fuzz_region2.rs) is a stable-Rust proptest harness that drives randomized input into `WalletLedger::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** (either `Ok`, or one of the four enumerated `WalletLedgerError` variants). 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 in `assert_typed_or_ok` is deliberately exhaustive with distinct classification tags per arm, so adding a new `WalletLedgerError` variant 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 at [`rust/shekyl-engine-state/fuzz/`](../rust/shekyl-engine-state/fuzz/): a minimal `fuzz_target!` wrapping `let _ = WalletLedger::from_postcard_bytes(data)`, excluded from the workspace via new `exclude = ["shekyl-engine-state/fuzz"]` in [`rust/Cargo.toml`](../rust/Cargo.toml) so stable CI never tries to resolve `libfuzzer-sys`. Runnable locally with `cargo +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 existing `shekyl-engine-state` unit tests remain green; 5-test proptest harness passes in 0.06 s; `cargo check --workspace --tests` on 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`](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. New [`rust/shekyl-engine-file/tests/adversarial_corpus.rs`](../rust/shekyl-engine-file/tests/adversarial_corpus.rs) drives 16 programmatic attack shapes through `WalletFile::open` and asserts the exact `WalletFileError` variant 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, future `state_version`, region-2 ciphertext bit flip → `StateSeedBlockMismatch` as currently mapped, cross-wallet companion swap → `StateSeedBlockMismatch`); SWSP frame attacks (`BadMagic`, `UnsupportedPayloadVersion`, `BodyLenMismatch`); `WalletLedger` body attacks (bundle `format_version` bump → `UnsupportedFormatVersion`, per-block `block_version` bump → `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-level `CapContentLenMismatch { mode, len }` variant unchanged — the plan's proposed new `CapabilityPayloadMismatch` was dropped on review because `validate_cap_content` in `shekyl-crypto-pq::wallet_envelope` already 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 via `WalletFile::create(...)`, then performs narrow byte surgery (on ciphertext-protected regions via the public `shekyl_crypto_pq::wallet_envelope::seal_state_file` helper) so it stays green across future format-field renames and AEAD parameter changes. New [`docs/WALLET_FILE_FORMAT_V1.md`](WALLET_FILE_FORMAT_V1.md) §2.5 writes up the capability decode posture the corpus enforces — mode first, then `cap_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. New [`rust/shekyl-engine-file/tests/fixtures/adversarial/`](../rust/shekyl-engine-file/tests/fixtures/adversarial/) holds a README + one `.md` per attack row documenting the construction and the rationale behind each typed refusal (including the deliberate `region-2-bit-flip → StateSeedBlockMismatch` collapse rather than `InvalidPasswordOrCorrupt`, 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 the `shekyl-engine-file` suite 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`](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 `.wallet` bundle 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). New [`rust/shekyl-engine-state/src/invariants.rs`](../rust/shekyl-engine-state/src/invariants.rs) owns 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 single `HashSet<[u8; 32]>` allocation, well under 100 µs for a 10 k-transfer bundle. New [`WalletLedgerError::InvariantFailed { invariant, detail }`](../rust/shekyl-engine-state/src/error.rs) variant carries the stable name plus a pointed diagnostic ("missing minor index 3 in [1, 4]" rather than "file is corrupt"), which flows through `shekyl-engine-file`'s `WalletFileError::Ledger` by existing `#[from]`. Two call sites wire the checks in: `WalletLedger::from_postcard_bytes` runs them after the per-block version gates pass (typed refusal on load), and `WalletLedger::preflight_save` runs them ahead of every `save_state` in `shekyl-engine-file/src/handle.rs` — `debug_assert!` in debug so a runtime-induced invariant break aborts tests loudly, typed `Err` in release so a user save never panics mid-write. Two invariants (subaddress density, key-image uniqueness) replace the plan's §3.6 `spent_images` and `transfer_index` proposals with shapes that match the actual blocks (`BookkeepingBlock::subaddress_registry` and `TransferDetails::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-test `shekyl-engine-state` suite and 51-test `shekyl-engine-file` suite 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`](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. New [`scripts/ci/check_zeroize.sh`](../scripts/ci/check_zeroize.sh) walks `rust/shekyl-engine-state/src/**/*.rs` and emits every `[u8; N]` or `Vec` 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-line `fn` signatures so `pub fn new(x: [u8; 32], …)` parameters are not mistaken for struct fields, and with standard filters on `//`, `///`, `use`, `type`, `impl`, `let`, `for`, `match`, `->` , and `assert` lines. Every hit must either carry a `Zeroizing<...>` / `SecretKey<...>` wrapper on the same line (auto-pass, no allowlist entry needed) or be enumerated verbatim — `|` — in [`rust/shekyl-engine-state/.zeroize-allowlist`](../rust/shekyl-engine-state/.zeroize-allowlist). The allowlist is bi-directional: a new unwrapped field with no entry fails with `FATAL: unwrapped byte-shaped field(s) without allowlist entry`, and an allowlist line whose field no longer exists fails with `FATAL: 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 on `TransferDetails`, (c) 32-byte map keys keying per-tx metadata (tx hashes are public lookup handles; values that carry secrets, like `TxSecretKey`, are wrapped on their own line), (d) the clear `PaymentId([u8; 8])` handle (obfuscation is applied by the tx-builder, not the storage type), (e) FCMP++ `path_blob: Vec` (public-input proof bytes; leaks anonymity-set choice but not spender secrets), (f) mirror-struct schema fields on `TransferDetailsSchema` / `TxSecretKeySchema` that exist only to drive the `postcard_schema::Schema` derive and never allocate at runtime, (g) `runtime_state.rs` in-memory indexes that are rebuilt from `LedgerBlock` on every load and never persisted. New [`.github/workflows/zeroize-check.yml`](../.github/workflows/zeroize-check.yml) runs the script on PRs into `dev` that touch the wallet-state source tree, the allowlist, the script itself, or this workflow. Policy captured in [`.cursor/rules/42-serialization-policy.mdc`](../.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 unwrapped `scratch_field: [u8; 32]`, adding a stale allowlist entry, unwrapping an `Option>` to `Option<[u8; 32]>` — each produce the expected pinpoint error. - **Wire-schema snapshot + paired `block_version` CI guard (commit 4 of the mid-rewire hardening pass, [`docs/MID_REWIRE_HARDENING.md`](MID_REWIRE_HARDENING.md) §3.4).** Converts the `block_version` discipline 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 a `postcard-schema = "0.2"` dependency to `shekyl-engine-state` (pinned at the same major as the on-disk `postcard = "1"` wire-format crate, stable schema representation), derives `postcard_schema::Schema` on every persisted block (`WalletLedger`, `LedgerBlock`, `BookkeepingBlock`, `TxMetaBlock`, `SyncStateBlock`, plus the nested `BlockchainTip`, `ReorgBlocks`, `FcmpPrecomputedPath`, `SubaddressLabels`, `AddressBookEntry`, `AccountTags`, `TxSecretKeys`, `ScannedPoolTx`, `SubaddressIndex`, `PaymentId` types), and hand-rolls `Schema` for 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-only `TransferDetailsSchema` / `TxSecretKeySchema` that mirrors the wire layout with `Vec` for byte sequences, then lifts `NamedType.ty` out of its derived `Schema` impl under the domain-facing type name. This is wire-identical to the original types (both produce length-prefixed byte sequences under postcard) but participates in `postcard-schema`'s `NamedType` tree, which is the load-bearing part of the check. [`rust/shekyl-engine-state/src/schema_snapshot.rs`](../rust/shekyl-engine-state/src/schema_snapshot.rs) is a new test module that renders each block's `NamedType` tree as pretty JSON (via `OwnedNamedType` — `NamedType` holds `&'static` references that `serde_json` cannot roundtrip through) and diff-compares against a committed `.snap` file under [`rust/shekyl-engine-state/schemas/`](../rust/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. Running `UPDATE_SNAPSHOTS=1 cargo test -p shekyl-engine-state schema_snapshot` regenerates; 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.yml`](../.github/workflows/schema-snapshot.yml) wires two jobs. The first runs `cargo test -p shekyl-engine-state schema_snapshot --no-fail-fast` against the PR head. The second diffs the PR against the `dev` merge-base and, for every `.snap` that changed, insists that both (a) the paired source file was touched, and (b) the `pub const` line that declares the matching version constant appears on either side of the file's unified diff. Pairing is canonical in both the workflow (`PAIRS` array) and the `schema_snapshot.rs` module 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 a `NamedType` diff names every field and spells out its `DataModelType`; (b) the schema-stability contract leans on `postcard-schema`'s SemVer (pinned `0.2`), because the `NamedType` representation is part of the crate's public API; (c) the mirror-struct pattern is preferred over upstream-patching `postcard_schema` to 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")]` on `SyncStateBlock::restore_from_height`), and the workflow's grep-logic dry-run correctly accepts a `pub const … = N → N+1` diff 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`](MID_REWIRE_HARDENING.md) §3.3).** New `ci/benchmarks` workflow ([`.github/workflows/benchmarks.yml`](../.github/workflows/benchmarks.yml)) running on PRs into `dev` (the gate) and pushes to `dev` (the rolling-baseline updater). On a PR: `ubuntu-latest` runs the full five-bench iai-callgrind harness via `scripts/bench/capture_rust_baseline.sh` (~8-10 min, cached cargo registry + target dir), diffs the resulting `shekyl_rust_v0.json` against the tip of the orphan `bench-baseline` branch's `baseline.json` via [`scripts/bench/compare.py`](../scripts/bench/compare.py), and upserts a Markdown PR comment via [`scripts/bench/post_comment.py`](../scripts/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 under `samply record` and uploads a `profile.json` artifact for flamegraph review. Bootstrap: the first PR before the `bench-baseline` branch exists gets a `bootstrap-pending` comment and the gate passes; the first subsequent push to `dev` creates 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 only `BM_balance_compute` ships live on the C++ side and it is wall-clock (same Tier-2 bucket as criterion); (c) the gate diffs against `bench-baseline/baseline.json` directly 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.md`](benchmarks/README.md) gains 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: write` only on the comment-posting job; `contents: write` only on the baseline-updater job), using the default `GITHUB_TOKEN` — no PAT, no self-hosted runner, no secret provisioning required. - **Provisional laptop-captured `shekyl_rust_v0` baseline (follow-up to hardening-pass commit 2).** The harness commit's CHANGELOG entry deferred the frozen `shekyl_rust_v0.json` + `shekyl_rust_v0.iai.snapshot` to 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 for `iai-callgrind-runner` is also fixed in the same landing: the tool's `--version` flag exits 1 outside the cargo-bench handshake protocol, so the envelope's `iai_callgrind_runner_version` field was previously `"unknown"`; it now resolves via `cargo install --list` with a fallback through the runner's own error banner. [`docs/benchmarks/README.md`](benchmarks/README.md) gains 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`](MID_REWIRE_HARDENING.md) §3.2).** Five hot paths from the §3.1 list, each shipped with a `criterion` binary (wall-clock, Tier-2 metric) and an `iai-callgrind` sibling (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 under `cargo check --benches`, run locally under `cargo bench -p --bench `, and — on a host with `valgrind` + `iai-callgrind-runner` on `PATH` — produce byte-identical instruction counts across back-to-back runs (§3.2 exit criterion). One deliberate deviation from production code is documented: the `transfer_e2e_iai` bench bypasses `HybridEd25519MlDsa::sign` and inlines the two sign steps with `fips204::ml_dsa_65::try_sign_with_seed` + `try_keygen_with_rng(seeded)` because the production wrapper's `OsRng` draws 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 full `sign_transaction` call 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 future `shekyl_rust_v1` schema bump. Companion artifacts: [`docs/benchmarks/shekyl_rust_v0.manifest.md`](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`](../scripts/bench/capture_rust_baseline.sh) (reference-machine capture wrapper — sibling of `capture_cpp_baseline.sh` from commit 1 — emits a schema-versioned `shekyl_rust_v0.json` envelope with toolchain + host CPU + git-rev metadata alongside a raw `shekyl_rust_v0.iai.snapshot` text artifact), [`docs/benchmarks/README.md`](benchmarks/README.md) updated with a "Capturing the Rust baseline" section and the shipped file-layout listing. Workspace impact is dev-dep-only: `criterion` + `iai-callgrind` land as `[dev-dependencies]` on the four crates that own a bench (`shekyl-engine-state`, `shekyl-engine-file`, `shekyl-scanner`, `shekyl-tx-builder`); the `shekyl-scanner` bench gains a self-referential `shekyl-scanner = { path = ".", features = ["test-utils"] }` dev-dep so `WalletOutput::new_for_test` + `RecoveredWalletOutput::new_for_test` are available in the bench without exposing them to downstream consumers. The frozen `shekyl_rust_v0.json` is 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`](MID_REWIRE_HARDENING.md) §3.1).** Google Benchmark v1.9.1 harness fetched via `FetchContent`, 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** with `state.SkipWithError(...)` (`BM_open_cold`, `BM_cache_roundtrip`): those two depend on `wallet2::generate` → `store_to` → `load` round-tripping, which is broken on this tree and reproduced by the already-failing unit test `wallet_storage.store_to_mem2file`. Root-causing the wallet2 regression is the work scope of hardening-pass commits `2l` / `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 own `wallet_accessor_test` in `tests/wallet_bench/bench_fixtures.h` (matching the existing friend declaration in `src/wallet/wallet2.h`, disjoint from the same-named class in `tests/core_tests/wallet_tools.h` — the two headers are never included in the same TU) with a minimal surface: `m_transfers` get, `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 in `docs/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`](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`](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). *(Both deleted in the Phase-5 wallet2 cutover; the paths above are historical.)* The frozen `wallet2_baseline_v0.json` is 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_options` link-time dep on `libcommon` (`src/common/CMakeLists.txt`).** `removed_flags.cpp` calls `boost::program_options::error_with_option_name::get_option_name()`, which inlines `get_canonical_option_name` and therefore requires the `libboost_program_options` symbol to resolve at link time (`libcommon.so` is linked with `-Wl,--no-undefined`). The dep was missing since `removed_flags` landed and only surfaced during a clean rebuild triggered by the benchmark harness above. Fix is a one-line `PRIVATE ${Boost_PROGRAM_OPTIONS_LIBRARY}` in `src/common/CMakeLists.txt`. No behavior change outside CMake. ### Chore - **Workspace `cargo fmt --all` baseline (PR 0.5 of the V3 wallet rewrite plan, [`.cursor/plans/shekyl_v3_wallet_rust_rewrite_3ecef1fb.plan.md`](../.cursor/plans/shekyl_v3_wallet_rust_rewrite_3ecef1fb.plan.md) Phase 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 --check` flagged them on `dev`. Mechanical, fmt-only run; no logic, behaviour, or API change. Lands before Phase 1 begins so subsequent rewrite PRs can use `cargo fmt --all --check` as a cheap branch-hygiene signal without wading through pre-existing drift. Drift cause was hand-edits bypassing fmt (verified: `git log --follow` on each file shows the drifting hunks were introduced under the same `rustfmt` toolchain in use today), so unconditional `cargo fmt --all` is the correct fix — no `#[rustfmt::skip]` warranted. - **Phase 0 PR 0.6 planning + FOLLOWUPS scope adjustments (`chore/phase0-pr06-vendor-bump-planning`).** Split the `monero-oxide` re-pin question into two distinct operations and scoped them differently: - **Operation A — vendor-bump `87acb57` → `3933664` (fork tip).** Mechanical, cheap, none crypto-substantive except `182b648`'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::invert` Veridise edge case) does not change this assessment: the bug exists today on `dev`, 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`](../.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 staticlib `tracing` silently-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-cli` key image binary format — no Monero binary-format port; `wallet_tools.cpp` mixin/decoy — swept with `tests/unit_tests/wallet*.cpp`); (5) Phase 3b deliverables flag an optional `--format=qr-chunks` on 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`](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; the `monero-oxide` un-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.cpp` mixin, daemon staticlib `tracing`) 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`](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. - **Phase 0 audit cleanup (`chore/phase0-audit-cleanup`).** Three small follow-ups surfaced by the post-merge comprehensive audit of `dev` against the V3 wallet rewrite plan's Phase 0 expectations: (1) consolidated the duplicate `### Documentation` heading 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 in [`docs/SHEKYLD_PREREQUISITES.md`](SHEKYLD_PREREQUISITES.md) pointing forward to the two consuming [`docs/V3_WALLET_DECISION_LOG.md`](V3_WALLET_DECISION_LOG.md) entries (positional fee mapping, `fee_policy_version` absence) and the daemon-side V3.1 follow-up in [`docs/FOLLOWUPS.md`](FOLLOWUPS.md), so the audit's downstream consumers are reachable from the audit doc itself; (3) fixed a pre-existing `clippy::needless_return` lint in `rust/shekyl-engine-file/src/handle.rs::is_cross_device_error` (introduced under commit `2l.a`, not by Phase 0) for readability. Recorded a follow-up in [`docs/FOLLOWUPS.md`](FOLLOWUPS.md) noting that the workspace as a whole is **not** `clippy --workspace -- -D warnings` clean (`shekyl-ffi` carries ~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_check` argument-order mismatch between Rust definition and C-side declaration** (Track 0a CI triage, 2026-04-28). The Rust definition in [`rust/shekyl-ffi/src/account_ffi.rs`](../rust/shekyl-ffi/src/account_ffi.rs) takes `(pqc_pk_ptr, view_pk_ptr)`; the C header in [`src/shekyl/shekyl_ffi.h`](../src/shekyl/shekyl_ffi.h) declared `(view_pub_ptr, pqc_public_key_ptr)`, and the one C++ caller in [`src/cryptonote_basic/cryptonote_basic_impl.cpp`](../src/cryptonote_basic/cryptonote_basic_impl.cpp) followed the wrong order. Every decode therefore ran the FIPS-203 well-formedness check on garbage bytes, surfacing in CI as 14 `uri.*` unit_tests failures with the log line `cn: Address failed v1 canonical invariant check (view_pub <-> X25519 prefix or malformed ML-KEM-768 encapsulation key)`. Introduced in commit `0092a8da1` ("ffi,cryptonote_basic: pin m_pqc_public_key format and publish v1 account FFI"); reached `dev` only at the `feat/wallet-account-rewire` merge `30db140fe` (2026-04-22). The Rust unit tests at `rust/shekyl-ffi/src/account_ffi.rs:954,975` use 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 14 `uri.*` 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 are `wallet_storage.{store_to_mem2file, change_password_mem2file}` tracked in `docs/CI_BASELINE.md` Cluster B and `docs/FOLLOWUPS.md` (V3.1, wallet2 hardening-pass close). - **CI baseline established as [`docs/CI_BASELINE.md`](./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.2 `wallet2.cpp` removal; Cluster D — `shekyl-oxide divergence` canary, currently green). The document also pins the interim `shekyl-oxide` divergence-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 to `dev` until investigated, with mechanical enforcement (a required-status-check on the failing-test set) tracked separately as a follow-up. Linked from [`docs/CONTRIBUTING.md`](./CONTRIBUTING.md) under "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_state` strict-contract enforcement (Phase 2a `refresh_scan_loop` bundle, Branch 1).** Closes the PR #16 Copilot-review finding tracked in `docs/FOLLOWUPS.md` *V3.0 → "`apply_scan_result` strict-contract enforcement (refresh commit)"* (now retired to *Recently resolved*). The merge in [`rust/shekyl-engine-core/src/engine/merge.rs`](../rust/shekyl-engine-core/src/engine/merge.rs) previously had two defensive-coding gaps: 1. `block_hashes` was collected via `BTreeMap::insert`, silently overwriting duplicate height entries instead of rejecting them. 2. `new_transfers` / `spent_key_images` / `block_hashes` entries with heights outside `processed_height_range` were silently dropped at scope end (the per-height `BTreeMap::remove` loop 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_state` now pre-validates `block_hashes` for 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 new `RefreshError::MalformedScanResult { reason: &'static str }` variant; this is distinct from `RefreshError::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-state` `ledger` / `ledger_iai` benches: pin `BlockchainTip.synced_height` to the synthetic transfers' max `block_height`.** The benches under [`rust/shekyl-engine-state/benches/ledger.rs`](../rust/shekyl-engine-state/benches/ledger.rs) and [`rust/shekyl-engine-state/benches/ledger_iai.rs`](../rust/shekyl-engine-state/benches/ledger_iai.rs) were authored against `WalletLedger::empty()` (commit `a9a81a17e`) before invariant I-1 (`tip-height-not-below-transfer`) was wired into `WalletLedger::from_postcard_bytes` by hardening-pass commit 6 (`def7d3379`, "feat(wallet-state): WalletLedger::check_invariants"). `build_ledger` was inheriting `tip.synced_height = 0` from the empty constructor while the synthetic transfers carried `block_height ∈ [1_000, 1_000 + N)`, so the deserialize half of the round-trip panicked with `WalletLedgerError::InvariantFailed { invariant: "tip-height-not-below-transfer", … }` on every iteration. The fix reconstructs the `LedgerBlock` with `tip.synced_height = max(transfers[*].block_height)` (and a non-`None` `tip_hash`) so the fixture is invariant-coherent before postcard sees it. The outdated `docs/FOLLOWUPS.md` entry that claimed four iai-callgrind targets failed to *compile* against the post-`RuntimeWalletState` fold has been replaced with a re-review entry capturing the actual finding (see *"Phase 1 bench harness re-review post-`RuntimeWalletState` fold (April 26, 2026)"*). All ten core benches under `capture_rust_baseline.sh` now build and smoke-run cleanly. - **`source archive` CI job: pin `git describe` to release tags (`v*`).** The branch-archival policy in [`.cursor/rules/06-branching.mdc`](../.cursor/rules/06-branching.mdc) rule 5 has accumulated seven `archive/-` annotated tags since 2026-04-13 (four of them on 2026-04-25, on commits that are merge-ancestors of `dev`). The `source-archive` job in [`.github/workflows/build.yml`](../.github/workflows/build.yml) was calling plain `git describe`, which returns the *closest reachable tag*. Once an `archive/*` tag became the closest tag to `dev`, `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 by `git-archive-all`, failing with `[Errno 2] No such file or directory: '…/shekyl-archive/-.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 keeps `VERSION` shaped like `shekyl-vX.Y.Z-N-gSHA`. Verified locally: `git describe origin/dev` returns `archive/phase0-pr06-oxide-vendor-bump-2026-04-25` (broken), `git describe --match 'v*' origin/dev` returns `v3.1.0-alpha.3-135-g39981643f` (correct). No behavior change for branches with a `v*` tag in their ancestry, which is every branch off `dev` since the first release tag. ### Security - **`rand` 0.8.5 → 0.8.6** in [`rust/Cargo.lock`](../rust/Cargo.lock) (RUSTSEC-2026-0097 / GHSA-cq8v-f236-94qc, severity Low). The advisory describes an unsoundness in `ThreadRng::TryRng` that can produce aliased mutable references — Undefined Behaviour — when all of the following hold simultaneously: (a) the `log` and `thread_rng` features are enabled, (b) a custom `log::Logger` is installed, (c) the custom logger calls `rand::rng()` / `rand::thread_rng()` and any `TryRng` (formerly `RngCore`) method on it, and (d) `ThreadRng` reseeds while called from inside the logger. This bump is **defense-in-depth, not active-vulnerability fix**: the project's custom logger lives in [`shekyl-logging`](../rust/shekyl-logging/) and a workspace-wide audit confirmed it does not call `rand::rng()` or `rand::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.lock` change (`cargo update --precise 0.8.6 -p rand@0.8.5`) 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-0097` against the fuzz-only lockfile ([`rust/shekyl-crypto-pq/fuzz/Cargo.lock`](../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 at `v2.0.0` pre-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 audit` exits 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`, and `cargo clippy --workspace --all-targets --keep-going -- -D warnings` all exit 0. Two further open dependabot alerts on `Shekyl-Foundation/shekyl-core` are stale and self-clear on the next `main` rescan: `rustls-webpki` GHSA-82j2-j2ch-gfr8 (already at the patched `0.103.13` in the workspace lockfile) and `cryptography` (pip) CVE-2026-39892 (the alert points at `tools/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 to [`docs/design/STAGE_1_PR_4_REFRESH_ENGINE.md`](completed/STAGE_1_PR_4_REFRESH_ENGINE.md) landed mid-Phase-1 between C5β (legacy producer scaffolding deletion) and C6 (test substrate). The Round 4 §7.X C6 plan ("`MockRefresh` test substrate; mirrors `MockDaemon` / `MockLedger` from PR 1 / PR 2") is **stale prose** from before PR 3 §2.1.2's Mock-X rejection landed; building `MockRefresh` would re-instantiate the parallel-implementation anti-pattern PR 3 rejected as a category and compound the Mock-X debt that [`docs/FOLLOWUPS.md`](FOLLOWUPS.md) already scheduled to be paid down. The amendment dispositions: 1. **C6 replaces `MockRefresh` with `FaultInjecting`.** Composable wrapper around the production `LocalRefresh` (landed at C4); queues `RefreshError::Cancelled` / `Io` / `InternalInvariantViolation` for failure injection at the trait boundary; composes against any current or future `R` implementor without per-impl parallel-Mock proliferation. 2. **Retroactive Mock-X cleanup of `MockLedger` lands in PR 4 C6β** (not deferred to PR 5). Extracts the existing `MockLedger` body into `FaultInjecting`; adds `LocalLedger::from_test_blocks(...)` constructor replacing the parallel-implementation `MockLedger::new(...)` surface. Current `MockLedger` is structurally already a `FaultInjecting`-shaped wrapper (delegating to the canonical `apply_scan_result_to_state`); the cleanup is mostly extraction-and-rename, not a re-implementation. Closes [`docs/FOLLOWUPS.md`](FOLLOWUPS.md) lines 578–604. 3. **`MockDaemon` → `TestDaemon` rename 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 the `Mock` naming was the bug. Closes [`docs/FOLLOWUPS.md`](FOLLOWUPS.md) lines 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`](../.cursor/rules/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.mdc`](../.cursor/rules/15-deletion-and-debt.mdc) pre-genesis discount applies. Cross-references: [`docs/completed/STAGE_1_PR_3_KEY_ENGINE.md`](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 4 `PendingTxEngine` — PR 4 is `RefreshEngine`; PR 5 is `PendingTxEngine`), 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).** 1. **F-Mock-1 — `cfg`-gating symmetry (Option (a)).** All four C6 surfaces (`FaultInjecting`, `Engine::replace_refresh`, `FaultInjecting`, `LocalLedger::from_test_blocks`) are gated uniformly `#[cfg(any(test, feature = "test-helpers"))]`. The `test-helpers` feature is introduced as part of C6α's scope per the F-Mock-7 disposition, with a rationale comment matching the existing `bench-internals` precedent at [`rust/shekyl-engine-core/Cargo.toml`](../rust/shekyl-engine-core/Cargo.toml) lines 223–227. 2. **F-Mock-2 — `FaultInjecting` queue contract.** Wrapper- internal queue (not actor mailbox) holding `RefreshError` values directly per Option (i) below. Contract: FIFO ordering; `queued_failures(&self) -> usize` drain inspector per the existing [`MockLedger::queued_failures`](../rust/shekyl-engine-core/src/engine/test_support.rs) precedent; `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. 3. **F-Mock-3 + F-Mock-3-sharpening + Option (i) wrapper API.** The wrapper carries `type Error = RefreshError` (not `R::Error`) and queues `RefreshError` values directly, uniform across all `R`. Cross-wrapper symmetry justifies the choice: `FaultInjecting` must queue `RefreshError` by trait necessity (per [`engine/traits/ledger.rs:270–273`](../rust/shekyl-engine-core/src/engine/traits/ledger.rs) — `apply_scan_result` returns `Result<(), RefreshError>` with no `Self::Error` indirection), so `FaultInjecting` queuing `RefreshError` matches. **Empirical variant enumeration (per source).** Of the six `RefreshError` variants at [`engine/error/refresh.rs`](../rust/shekyl-engine-core/src/engine/error/refresh.rs), three are reachable from a `RefreshEngine` impl's `Self::Error` via the `From` conversion: `Cancelled` (unit), `Io(IoError)` (payload), and `InternalInvariantViolation { context: &'static str }` (payload constructed at the `From` impl site per [`engine/local_refresh.rs:368–384`](../rust/shekyl-engine-core/src/engine/local_refresh.rs)). Three are orchestrator-constructed only: `MalformedScanResult { reason }` (constructed **exclusively** by the merge layer at [`engine/merge.rs:315–451`](../rust/shekyl-engine-core/src/engine/merge.rs) when scan-result internal-shape invariants fail — superseding the doc's prior framing that grouped it with `Cancelled` / `Io` as trait-reachable), `ConcurrentMutation { wallet, result }` (constructed at the merge gate), and `AlreadyRunning` (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; for `InternalInvariantViolation` both direct injection (testing producer-returned-then-orchestrator-propagated path) and **cause injection** (driving causes through `FaultInjecting::queue_concurrent_mutation` per F-Mock-2 to exhaust the retry budget at orchestrator- side construction sites in `engine/refresh.rs`) are legitimate test classes. 4. **F-Mock-4 — `MockLedger`-structurally-already-`FaultInjecting` verification gate anchored.** The Round 5 amendment's load-bearing claim ("current `MockLedger` is structurally already a `FaultInjecting`-shaped wrapper") is anchored to source at [`engine/test_support.rs:773–812`](../rust/shekyl-engine-core/src/engine/test_support.rs): `MockLedger::apply_scan_result` (line 792) pops from `concurrent_mutation_queue` (line 794); on empty-queue, delegates to the canonical `apply_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 `RefreshEngine` trait carries a deliberate two-enum architecture worth pinning as a positive architectural reference and forward- template for future per-trait PRs. Producer-internal [`LocalRefreshError`](../rust/shekyl-engine-core/src/engine/local_refresh.rs) is `pub(crate)`, unit-variant-only by convention, four variants (`Cancelled`, `Io`, `Malformed`, `Internal`). Orchestrator-facing [`RefreshError`](../rust/shekyl-engine-core/src/engine/error/refresh.rs) is `pub`, payload-bearing throughout. The `From` impl boundary at [`engine/local_refresh.rs:368–384`](../rust/shekyl-engine-core/src/engine/local_refresh.rs) is 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 return `Result<_, Self::Error>` with `Self::Error: Into`; it is **not** load-bearing for traits whose canonical method signatures return `Result<_, OrchestratorError>` directly (per the `LedgerEngine` precedent). Per-trait PR pre-flight checks include "does this trait have an impl-side `Self::Error` indirection, 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` to inject `RefreshError` values directly (per Option (i) wrapper API); verify the orchestrator handles each variant correctly. Lives in C6α's new `fault_injecting_refresh.rs` test module plus the trait-dispatched `Engine` integration tests. Sub-properties: empty-queue passthrough; single-injection- then-delegation; multi-injection FIFO ordering; queue-drain-on-teardown (with Drop-time `debug_assert!` `#[should_panic]` separately verified). - **Class 2 — From-conversion tests against `LocalRefresh`.** Tests drive `LocalRefresh` directly via the `pub(crate)` producer-internal surface to produce each `LocalRefreshError` variant; verify the `From` impl produces the correct `RefreshError` variant. Lives in [`local_refresh.rs`](../rust/shekyl-engine-core/src/engine/local_refresh.rs)'s existing tests module per the [`local_refresh_error_maps_to_refresh_error`](../rust/shekyl-engine-core/src/engine/local_refresh.rs) test precedent; **sibling to Class 1, not a replacement** because the wrapper bypasses the `From` conversion by injecting `RefreshError` directly 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`](../.cursor/rules/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 "replaces `MockLedger::new(...)`" prose error (the constructor is `with_seed` / `with_seed_and_state`, not `new`). F-Mock-6 adds a Phase 1 author commit-message-template note to C6γ enumerating the `MockDaemon` test affordances surviving the rename unchanged. F-Mock-7 confirms the `test-helpers` feature does not currently exist in [`Cargo.toml`](../rust/shekyl-engine-core/Cargo.toml) and 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 `TestLedgerBuilder` substrate design).** The three V3.x invariant-test FOLLOWUPS entries (tx-validation, FCMP++ tx-pool, staking lifecycle at [`docs/FOLLOWUPS.md`](FOLLOWUPS.md) lines 2411–2438) share a common test-infrastructure need beyond what PR 4 C6β's `LocalLedger::from_test_blocks` covers. The sub-pin lands a new V3.1 substrate-design FOLLOWUPS entry pinning the coordinated-design disposition: build one `TestLedgerBuilder` / `TestBlockBuilder` / `TestTransactionBuilder` substrate designed **before** the first daemon Rust port lands (cost asymmetry from [`16-architectural-inheritance.mdc`](../.cursor/rules/16-architectural-inheritance.mdc) "cost-benefit-defer-to-later anti-pattern"); design to be forward-composable with PR 4 C6β's `LocalLedger::from_test_blocks` signature; 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 list `FaultInjecting` / `FaultInjecting` / `TestDaemon` entries updated; §7.X C6α wrapper-definition / F-Mock-3-sharpening / F-Mock-2 queue contract / F-Mock-7 `test-helpers` feature / 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 coordinated `TestLedgerBuilder` substrate-design entry), and this CHANGELOG entry. - **Stage 1 PR 3 (`KeyEngine`) M3a pre-flight closures landed.** The four open `STAGE_1_PR_3_KEY_ENGINE.md` dispositions 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_secret` in the input phase is a PRF in `view_secret` under standard assumptions). Implementation crate: `sha3 = "0.10"` (already a workspace dep) with the `zeroize` feature flag enabled, giving `Sha3State` wipe-on-drop discipline structurally per `35-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 (no `HandleTable` data structure; `derive_output_handle` pure function instead; `source_ciphertext` + `output_handle` added to `TransferDetails` at M3b alongside the legacy fields, with legacy fields removed at M3d). M3a feat branch cleared to cut. Documentation-only change; no code shipped. ## [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) and `fips204` (ML-DSA-65), consumed on the hot path by `shekyl-crypto-pq` and `shekyl-tx-builder` — state their constant-time guarantees against native 64-bit arithmetic. On 32-bit targets the compiler lowers `u64` operations through compiler-emitted libgcc helpers (`__muldi3`, `__udivdi3`, `__ashldi3`) with no constant-time guarantee, plus variable-latency `u64` multiply 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 the `src/crypto/slow-hash.c` 32-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_VL32` paging 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):** 1. **Tripwire D — `CMakeLists.txt`.** C++-side configure gate: `message(FATAL_ERROR …)` on `NOT CMAKE_SIZEOF_VOID_P EQUAL 8`, placed before any `find_package` / `include` / `add_subdirectory` so configure fails early with the CT argument in the message. Exercised on every PR to `dev` by `.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 no `find_package` chatter (so a PR that moves the gate below a probe also fails the test). 2. **Tripwire A — `rust/shekyl-crypto-pq/src/lib.rs`.** Primary `compile_error!` on `not(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. 3. **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 from `shekyl-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 and `docs/audit_trail/RESOLVED_260419.md` §"Chore #3". 4. **Tripwire C — `rust/shekyl-tx-builder/src/lib.rs`.** Direct `fips204` (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-bit `Makefile` targets that actually existed on `dev` (`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` / `ARM7` machinery and the Clang+32 `libatomic` workaround in the root `CMakeLists.txt`; the `-D BUILD_64=ON` argument on all remaining 64-bit `Makefile` targets; `ARCH_WIDTH != 32` conditional in `src/blockchain_utilities/blockchain_import.cpp` (body retained, guard deleted); `-D MDB_VL32` in `external/db_drivers/liblmdb/CMakeLists.txt` (vendored `mdb.c` `MDB_VL32` code paths are now unreachable in Shekyl builds and deliberately left unpatched in-tree — see `docs/VENDORED_DEPENDENCIES.md` §"`MDB_VL32` — 32-bit retirement note" for the future-update drill); `contrib/depends/` toolchain template `i686` / `armv7` / `BUILD_64` / `LINUX_32` branches, package recipes for `boost` / `openssl` / `android_ndk` / the arch-asymmetric `_cflags_mingw32+="-D_WIN32_WINNT=0x600"` line in `unbound.mk`, `README.md` host list, `.gitignore` `i686*` / `arm*` entries, `packages.md` example; `cmake/BuildRust.cmake` all non-64-bit `CMAKE_SYSTEM_PROCESSOR` branches; 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___divmoddi4` block and `__i386__`/`__arm__` glob symver arms (plus the corresponding `-Wl,--wrap=__divmoddi4` linker flag in the root `CMakeLists.txt`); `src/crypto/slow-hash.c` outer guard narrowed from `__arm__ || __aarch64__` to `__aarch64__` and the 32-bit fallback `cn_slow_hash_{allocate,free}_state` stubs removed; `src/crypto/CryptonightR_JIT.{c,h}`, `src/crypto/CryptonightR_template.h` x86 gates narrowed from `__i386 || __x86_64__` to `__x86_64__`; `src/cryptonote_basic/miner.cpp` FreeBSD APM gates narrowed from `__amd64__ || __i386__ || __x86_64__` to `__amd64__ || __x86_64__`; `src/blockchain_db/lmdb/db_lmdb.h` `__arm__` `DEFAULT_MAPSIZE` branch removed; `src/blockchain_db/lmdb/db_lmdb.cpp` `MISALIGNED_OK` gate narrowed to `__x86_64` only. **Disambiguation:** `tests/hash/main.cpp:192,206` `` SSE-intrinsic gates are x86_64 arch gates, not 32-bit gates, and are **not** deleted — an earlier draft of `STRUCTURAL_TODO.md` lumped them with the 32-bit retirement imprecisely. Rust: three `compile_error!` tripwires (A/B/C, above); `rust/shekyl-oxide/crypto/helioselene/benches/helioselene.rs` `target_arch = "x86"` branches collapsed to `x86_64` only. CI: `.github/workflows/depends.yml` ARM v7 stub replaced with a pointer to this chore; new `.github/workflows/cmake-gate-test.yml` + `tests/cmake-gate-test/` enforcing Tripwire D placement. Docs: `README.md`, `docs/INSTALLATION_GUIDE.md`, `docs/RELEASING.md`, and `docs/COMPILING_DEBUGGING_TESTING.md` are now 64-bit-only; `docs/VENDORED_DEPENDENCIES.md` carries the `MDB_VL32` future-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`, `i386` are 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 past `v3.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 §. ### Changed - **Shekyl Foundation institutional release-signing key adopted.** `v3.1.0-alpha.5` is the first release signed by the Shekyl Foundation institutional signing key (subkey fingerprint `3778 B4C8 63C6 1512 B5FC 2203 6914 D748 23DD A8DC`, long ID `6914D74823DDA8DC`; primary fingerprint `F5F7 5A47 70C9 4FE1 D5A5 AE59 844E 424F 9866 4F44`, long ID `844E424F98664F44`). 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.md` is rewritten as the canonical, self-contained reference: both key blocks inline (no loose `.asc` files), 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-tag` before push) as a summary, not a replacement. Resolves the `docs/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 Rust `tracing-subscriber` stack. The default formatter is `tracing_subscriber::fmt::layer`, and its line shape is *not* byte-compatible with the vendored `easylogging++` 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 the `E` / `W` / `I` / `D` / `V` single letters), the target appears as a structured `target:` 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 of `MONERO_LOGS` now reads `SHEKYL_LOG` instead. This closes the C++-side half of the per-`.cursor/rules/93-legacy- symbol-migration.mdc` rename — Chore #1 (V3.1 alpha.4) already migrated the Rust binaries. `SHEKYL_LOG` accepts the same `tracing-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`, numeric `0..=4` presets, `+`/`-` modifiers) routed through the Rust-side translator. The legacy grammar is preserved on purpose: the ~1,345 `MINFO` / `MDEBUG` / etc. call sites in `src/` and `contrib/` ship category strings in that grammar, and operator runbooks doing `SHEKYL_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 a `SHEKYL_LOG=...` line alongside each `MONERO_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 as `net.p2p` / `daemon.rpc` now appear as `net::p2p` / `daemon::rpc` in every `tracing-subscriber`-rendered line. The FFI boundary (`shekyl_log_emit` / `shekyl_log_level_enabled` in `rust/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-supplied `SHEKYL_LOG` directives continue to accept both spellings — the legacy-grammar translator rewrites `.` to `::` on the way in, so `SHEKYL_LOG='*:WARNING,net.p2p:TRACE'` and `SHEKYL_LOG='warn,net::p2p=trace'` behave identically. Only the rendered output changes. Log-scraping pipelines that grep for `target=net\.p2p` need to grep for `target=net::p2p` (or, per the format-break entry above, `net::p2p:` at the front of the fields block) instead. - **`shekyld` default log sink moved to `~/.shekyl/logs/`.** Under `chore/cxx-logging-consolidation`, the daemon's default `--log-file` path changed from `/shekyld.log` (next to the blockchain database) to `~/.shekyl/logs/shekyld.log`, resolved through the Rust FFI's `shekyl_log_default_path`. Testnet/stagenet/regtest runs use the suffixed base names `shekyld-testnet.log` / `shekyld-stagenet.log` / `shekyld-regtest.log` so 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 mode `0600` on Unix — operator-tunable permissions are not a supported knob. Operators who want to keep the legacy next-to-data-dir layout can pass `--log-file` explicitly; the override path is unchanged. - **CMake Python discovery modernized (Chore #3 follow-up).** `include(FindPythonInterp)` at the top of `CMakeLists.txt` is replaced with `find_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 and `find_package(PythonInterp)` before the tests subdir) are deleted. The legacy `PYTHON_EXECUTABLE` and `PYTHONINTERP_FOUND` variables are aliased post-discovery so consumers under `tests/difficulty/CMakeLists.txt`, `tests/block_weight/CMakeLists.txt`, and the `cmake/CheckTrezor.cmake` fallback arm continue to work without a cascading migration. The `cmake_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; addresses `docs/CHANGELOG.md` V3.1.0-alpha.3 entry's own callout of the same migration debt. ### Removed - **`MONERO_LOG_FORMAT` env var (no replacement).** The custom format string that `MONERO_LOG_FORMAT` used 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 with `tracing-subscriber` feature 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 in `ded9875b6`. All call sites that reached `el::Logger` / `el::Configurations` / `el::base::Writer` etc. directly have been rewritten to route through the `shekyl_log_emit` / `shekyl_log_level_enabled` FFI in `src/shekyl/shekyl_log.h`. The `el::` namespace survives only as a thin typedef-only compatibility shim in `contrib/epee/include/misc_log_ex.h` (`el::Level`, `el::Color`, `el::base::DispatchAction`) so the existing `MINFO` / `MDEBUG` / `MWARNING` / `MCINFO` macros expand without touching the ~1,345 call sites. Closes the `STRUCTURAL_TODO.md` §"Replace easylogging++ with a maintained logger" item (both chores); swept narrative in `docs/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 in `cmake/GitVersion.cmake` from the annotated tag on HEAD, then emits `SHEKYL_VERSION_FULL` over 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.3` tag-push CI run ([#394](https://github.com/Shekyl-Foundation/shekyl-core/actions/runs/24637252528), `test-ubuntu` matrix): on a tagged build, `SHEKYL_VERSION_FULL` resolves to `3.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.N` numbering 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 by `cmake/GitVersion.cmake`; the daemon should not re-parse its own output to re-check it. `rpc_command_executor.cpp` keeps 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/cli` jobs on `test-ubuntu` matrix.** Follows from the `rpc_version_str` removal above. `v3.1.0-alpha.3` shipped 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.4` will 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_PROCESSOR` arm of the 64-bit-only gate in `CMakeLists.txt` previously used `armv[67]l?`, which only matches `armv[67]` and `armv[67]l` exactly — real toolchains also emit `armv7-a`, `armv7a`, `armv7ve`, `armv7hf`, `armv6kz`, `armv5te`, etc., which are all 32-bit ARM profiles. Broadened to `armv[567].*` so the "defense-in-depth" half of the predicate (which fires when `CMAKE_SIZEOF_VOID_P` is 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 in `tests/cmake-gate-test/run.sh`: the probe-chatter assertion now also catches `-- Performing Test ...` (from `CheckCCompilerFlag` / `CheckCXXCompilerFlag` / `CheckLinkerFlag`), matching the set of modules actually relocated below the gate; `-- Detecting C/CXX compiler ABI info` is deliberately NOT caught because those lines come from `project()` itself, which runs before the gate by construction (the gate's `CMAKE_SIZEOF_VOID_P` predicate is populated by `project()`'s own compiler probe). Resolves the second Copilot review on PR #15. - **`contrib/depends` Win64 unbound build restored (Chore #3 fixup).** The `$(package)_cflags_mingw32+=-D_WIN32_WINNT=0x600` line in `contrib/depends/packages/unbound.mk` was deleted in the Chore #3 build-system commit under the mistaken framing of "arch-asymmetric 32-bit MinGW carve-out." The `_mingw32` suffix in `contrib/depends` is the OS segment of the host triple, not an architecture gate: it matches every `*-w64-mingw32` host including `x86_64-w64-mingw32`. Unbound 1.19.1's `util/netevent.c` uses `WSAPoll` / `POLLOUT` / `POLLERR` / `POLLHUP` unconditionally and requires `_WIN32_WINNT >= 0x0600` to be defined before `` is included; the vendored `x86_64-w64-mingw32` toolchain does not default this the way MSYS2 pacman toolchains do, so the deletion broke the `depends.yml` Win64 lane (the `build.yml` MSYS2 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-value` stays quiet at the call sites), but the label (`[SRV_MAIN]` from `abstract_tcp_server2.inl`, `[miner N]` from `miner.cpp`, `DLN` from `download.cpp`) does not appear in emitted events. easylogging++ used this hook to stamp a semantic label into every subsequent log line; the Rust `tracing-subscriber` formatter reads the OS-level thread name instead (via the platform `pthread_getname_np` / `GetThreadDescription` path), and those names are not being populated in Chore #2. Restoring semantic thread labels — either by teaching the C++ shim to call `pthread_setname_np` + Windows equivalents, or by routing the label through the Rust subscriber as a `span` field — is tracked as a V3.2 follow-up in `docs/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 from `v3.1.0-alpha.3` onward is a signed annotated tag created with `git tag -a -s`. It records the initial maintainer signing key (Rick Dawson, ed25519 `FEFEC7EF9952D40C`, 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 with `git 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 `dev` to `main`.** `.cursor/rules/06-branching.mdc` was updated to require that `main` advance 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 to `main` are now explicitly forbidden. The rule cross-links to `docs/SIGNING.md` at 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.md` tracks 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 from `docs/SIGNING.md` §"Future: Foundation institutional signing key". ### Security - **Bump `cryptography` from `44.0.2` to `46.0.6`** in `tools/reference/requirements.txt` to clear two Dependabot advisories indexed 2026-04-13: - [GHSA-r6ph-v2qm-q3c2](https://github.com/advisories/GHSA-r6ph-v2qm-q3c2) (high): missing subgroup validation for SECT curves could allow a small-subgroup attack during ECDH. - [GHSA-m959-cc7f-wv43](https://github.com/advisories/GHSA-m959-cc7f-wv43) (low): incomplete DNS name constraint enforcement on peer names. **Not exploitable against Shekyl users.** `cryptography` is pulled in only by `tools/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 the `cryptography.hazmat.primitives.{hashes,kdf.hkdf}` imports in that script are unused — all HKDF logic is hand-rolled with stdlib `hmac`/`hashlib` — so the bump cannot change its output. Verified by regenerating `docs/test_vectors/PQC_OUTPUT_SECRETS.json` under the new version in a clean venv; SHA-256 matches byte-for-byte (`1159cb6de2ce3fa4af5d7a8f88eac71ed35c8f00ebf297a4d9259439b6477163`). - **Accept seven `rand 0.8.5` Dependabot alerts as risk-tolerated.** [GHSA-cq8v-f236-94qc](https://github.com/advisories/GHSA-cq8v-f236-94qc) ("Rand is unsound with a custom logger using rand::rng()") indexes against the five workspace crates that pin `rand = "0.8"` plus two `Cargo.lock` files. CVSS is 0 on all seven; the actual exploit requires calling `rand::rng()` (a 0.9+ thread-local RNG API that does not exist in 0.8) while a custom `log::Log` implementation is installed. Shekyl uses `rand::rngs::OsRng` directly and `rand_chacha::ChaCha20Rng::from_seed` for deterministic derivation, and the daemon installs no custom `log::Log`, so no Shekyl code path reaches the vulnerable code. Migrating to `rand = "0.9"` cascades into bumping `curve25519-dalek` 4 → 5 plus several other crypto crates; per `.cursor/rules/20-rust-vs-cpp-policy.mdc` that is a planning activity with its own design doc and review cycle, tracked in `docs/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_ffi` no longer carries wallet-directory state.** Removed `wallet2_ffi_set_wallet_dir` and the `wallet_dir` field on `wallet2_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 full `wallet_path` parameter in place of the bare `filename` that was joined with `wallet_dir` using a hardcoded `"/"` separator. Path construction was inherited Monero `wallet_rpc_server` scaffolding and produced mixed-separator paths on Windows (`C:\Users\x\...\...//My Wallet.keys`). Callers now join paths in Rust via `PathBuf::join`, which is platform-correct on every target. The legacy C++ `wallet_rpc_server.cpp` keeps its own `wallet_dir` state and is unaffected — it does not go through the FFI. The `shekyl-cli` `WalletContext` now holds the directory and joins filenames before each call; the `shekyl-wallet-rpc` Rust shim keeps `ServerConfig.wallet_dir` for the V3.2 cutover when its handlers will own wallet-file creation. `validate_filename` was narrowed and renamed to `validate_wallet_path` (empty-path check only) — path-component validation is the caller's responsibility now that the caller also owns the directory. - **Nightly `proptest-exhaustive` job tuned and extended to `dev`.** Dropped `PROPTEST_CASES` from `1_000_000` to `200_000` — the old value could not finish inside the 30-minute runner cap on `ubuntu-latest` (ML-KEM-768 keygen per case dominates wall time, the run was being cancelled not failed). Raised `timeout-minutes` to `180` so 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 range `v3.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/` (POSIX `fork()` detach, Windows Service Control Manager registration, console-control glue) and the four thin wrapper classes in `src/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 where `daemon/command_line_args.h` transitively pulled `windows.h` into 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`. Both `shekyld` and `shekyl-wallet-rpc` accept these only long enough to print a migration message pointing at platform service managers (see `src/common/removed_flags.{h,cpp}`, marked `TODO(v3.2)` for deletion alongside the `shekyl-wallet-rpc` Rust cutover). `--non-interactive` is preserved in both binaries. ### Changed - **Daemon orchestration class renamed.** `daemonize::t_daemon` is now `daemonize::Daemon` in `shekyld`, and `shekyl-wallet-rpc`'s unrelated inline class is now `WalletRpcDaemon`. 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-user `CSIDL_*` branching formerly in `daemonizer` now lives in `common/daemon_default_data_dir.{h,cpp}`, preserving the exact path `shekyld` resolved before V3.1. Pinned by a new `daemon_default_data_dir` unit test so a future refactor cannot silently point operators at an empty data directory. - MSVC CI job now builds `--target daemon wallet` instead 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 `#ifdef` inside `MERROR()` macro argument in `core_rpc_server.cpp` (undefined behavior, C2059 on MSVC). Replaced with literal function name. - Explicitly captured `handshake` in lambda in `abstract_tcp_server2.inl` (C3493 on MSVC). - Explicitly captured `credits_per_hash_threshold` in lambda in `core_rpc_server.cpp` (C3493 on MSVC). - SFINAE-constrained `network_address` template constructor in `net_utils_base.h` to 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.md` audit 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.md` and wire format in `docs/SHEKYL_MULTISIG_WIRE_FORMAT.md`. 93 unit tests, 19 integration tests, 11 fuzz harnesses. - **Versioning scheme established.** `docs/VERSIONING.md` defines SemVer for software versions and a separate integer protocol version. `SHEKYL_PROTOCOL_VERSION` constant added to `cryptonote_config.h`, exposed via `--version` output and `/get_info` RPC. ## 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: - `MultisigKeyContainer` v1.1 with `spend_auth_version` field and `multisig_group_id` v1.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_i7` for output lifecycle - `GriefingTracker`: per-output cost bounding for invalid outputs - `shekyl1m` Bech32m address format with file-based handling and 3-representation fingerprint - `SpendIntent`: 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 - `MultisigEnvelope` with 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 detection - `CounterProof`: 8-rule chain evidence verification for counter recovery - C++ `tx_extra` tags 0x08, 0x09, 0x0A for multisig metadata - FFI: `shekyl_pqc_verify_with_group_id` for 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 metadata - `ProverView`: per-participant prover assignment breakdown - `LossAcknowledgment`: mandatory 1/N loss checkbox - `AddressProvenance`: fingerprint history with change detection - `RelayConfig`: multi-relay management with operator diversity - `ViolationAlert`: I1–I7 violation display with auto-abort - `SigningDashboard`: 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 in `shekyl-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.mdc` codifying that `shekyl1m` is 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) and `82-failure-mode-ux.mdc` (every feature must enumerate failure modes before implementation, failure states get dedicated UI). ### 🔒 Security - **Zeroize ephemeral multisig signing seeds.** `ed_seed` and `ml_seed` stack copies in `construct_multisig_output_for_sender` are now wrapped in `Zeroizing<[u8; 32]>`, ensuring automatic zeroing on drop. Closes a theoretical side-channel surface from FOLLOWUPS.md V3.1 audit response. - **`PersistedMultisigOutput` Debug redaction.** The `Debug` derive on `PersistedMultisigOutput` was replaced with a manual implementation that redacts `my_shared_secret` (64-byte KEM-derived material). Prevents accidental secret exposure through `dbg!` or structured logging. - **`validate_balance` checked arithmetic.** `SpendIntent::validate_balance` now uses `checked_add` for input sums, output sums, and fee addition. Previously used wrapping `sum()` — crafted u64 values could wrap both sides to the same value and pass the equality check. - **HKDF derivations return `Result`.** `derive_multisig_kem_seed` and `derive_participant_kem_randomness` now return `Result<..., CryptoError>` instead of panicking via `.expect()` on the transaction construction path. - **`eprintln!` removed from `shekyl_fcmp_verify` FFI.** Two diagnostic `eprintln!` 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_prove` and `shekyl_fcmp_verify` previously converted LMDB depth to upstream `layers` internally (`layers = depth + 1`). This created an ambiguous contract where the same `tree_depth` parameter meant different things in different FFI functions. Now both functions accept the upstream `layers` count directly; C++ callers (`blockchain.cpp`, `rctSigs.cpp`) perform `depth + 1` before calling. `shekyl_sign_fcmp_transaction` still accepts LMDB depth and converts internally (wallet callers pass LMDB depth). Added diagnostic tracing to `proof::verify` for `FcmpPlusPlus::read` and key image decompression failures. Fixed `validate.rs` c1/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 audit` failure from RUSTSEC-2026-0098/0099.** Bumped `rustls-webpki` 0.103.10 -> 0.103.12 and `rand` 0.9.2 -> 0.9.4 in `Cargo.lock`. Added `rust/audit.toml` to acknowledge `rand` 0.8.5 (RUSTSEC-2026-0097, not applicable: Shekyl uses `OsRng`, not `rand::rng()` with a custom logger). - **Remove dead `verify_transaction_pqc_auth` one-arg overload.** The no-argument overload in `tx_pqc_verify.cpp` had zero callers — the sole production caller (`blockchain.cpp`) uses the two-arg form with `expected_scheme_id`. Replaced with a default parameter. Per `15-deletion-and-debt.mdc`: dead code goes. - **Fix stale `shekyl_ffi.h` `shekyl_pqc_verify_debug` comment.** The error code documentation (0-4) did not match the Rust `PqcVerifyError` enum (0-11). Updated to reflect the actual `repr(u8)` discriminants. - **Reconcile FOLLOWUPS.md and STRUCTURAL_TODO.md.** Marked 5 items in STRUCTURAL_TODO as resolved (code already fixed). Corrected the `expected_scheme_id` FOLLOWUPS entry (parameter is actively used by `blockchain.cpp`, contrary to the prior note). Marked `rpassword` audit as covered by CI. ### 🔄 Changed - **FFI: verification functions return typed `u8` error codes instead of `bool`.** `shekyl_pqc_verify`, `shekyl_pqc_verify_with_group_id`, and `shekyl_fcmp_verify` now return 0 on success and a nonzero error discriminant on failure. PQC verify uses `PqcVerifyError` codes 1-11; FCMP verify uses `VerifyError` codes 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. Per `30-ffi-discipline.mdc`. - **Clippy lint rename: `unchecked_duration_subtraction` → `unchecked_time_subtraction`.** Updated in workspace `Cargo.toml` to track the upstream rename. ### 🗑️ Removed - **`shekyl_pqc_verify_debug` deleted.** Now that production `shekyl_pqc_verify` returns typed error codes, the debug-only variant is redundant. All call sites and the `#ifndef NDEBUG` C header guard removed. ### 🐛 Fixed (continued) - **All Rust clippy warnings resolved in `shekyl-crypto-pq`.** Fixed 1 error (`missing_fields_in_debug` in `PersistedMultisigOutput`) and 13 warnings: `op_ref` (11 sites in `kem.rs`, `montgomery.rs`, `output.rs`), `needless_range_loop` and `unnecessary_map_or` (in `multisig_receiving.rs`), `uninlined_format_args` (in `output.rs` tests). Also ran `cargo fmt` across 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: 1. **FFI depth/layers off-by-one.** LMDB stores 0-indexed `tree_depth`; the upstream library expects 1-indexed `layers` count. Fix: `layers = tree_depth + 1` at the FFI boundary. 2. **C++ branch extraction loop was `< depth` instead of `<= depth`.** Both `genRctFcmpPlusPlus` and `assemble_tree_path_for_output` skipped the root layer's branch data. Fix: `layer <= tree_depth` in both. 3. **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_scalar` applied during `genRctFcmpPlusPlus` branch assembly. 4. **`compute_leaf_count_at_height` off-by-one.** Maturity comparison used `<= target_height + 1` while LMDB's `drain_pending_tree_leaves` uses `<= current_height`. Fix: removed the `+ 1` to match LMDB semantics. 5. **`key_image_y_normalize` broke Ed25519 batch verification.** The normalization (clearing byte 31 sign bit) modified the key image away from the true `x * Hp(O)` used by the Rust prover. Fix: deleted `key_image_y_normalize` entirely — FCMP++ key images are not y-normalized. 6. **PQC signing payload computed before all public keys were derived.** `get_transaction_signed_payload` hashes all inputs' `hybrid_public_key` values, 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 Rust `shekyl-fcmp` tests now pass. This is the first green CI in the repository's history. - **Consensus-critical: curve tree leaf ordering bug (DB v6 → v7).** `pending_tree_leaves` used `MDB_DUPSORT` on 128-byte leaf data, causing outputs with the same maturity height to drain into the curve tree in byte-sorted order rather than `global_output_index` order. This broke the implicit `global_output_index == tree_leaf_index` assumption that every caller of `get_curve_tree_leaf()` relied on. Replaced with 16-byte composite keys `BE(maturity) || BE(output_index)` enforcing canonical drain order. Same restructuring applied to `pending_tree_drain`. Added explicit bidirectional mapping tables (`output_to_leaf`, `leaf_to_output`) and a `block_pending_additions` journal for robust `pop_block` reversal. DB schema bumped to v7 (incompatible with v6 — requires resync). - **`get_curve_tree_leaf()` parameter was silently misnamed.** The function accepted `global_output_index` in its signature but actually looked up by tree position. Renamed to `get_curve_tree_leaf_by_tree_position()` and added `get_curve_tree_leaf_by_output_index()` (double lookup via mapping table). All callers updated — compile errors catch any missed sites. - **`check_stake_claim_input` now 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 heed `BytesEncode`/`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. See `POST_QUANTUM_CRYPTOGRAPHY.md` §X25519 Binding to View Key. - **Unclamped Montgomery DH (not RFC 7748 X25519).** The classical KEM component performs `Scalar * MontgomeryPoint` with 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. See `POST_QUANTUM_CRYPTOGRAPHY.md` §DH Semantics. - **Low-order Montgomery point rejection (validation rule).** Recipients MUST reject low-order Montgomery points on `kem_ct_x25519` before 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. See `POST_QUANTUM_CRYPTOGRAPHY.md` §DH Semantics. - **`m_pqc_public_key` layout invariant: 1216 bytes.** `X25519_pub[0..32] || ML-KEM-768_ek[32..1216]` where `X25519_pub` is 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.json` pins 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_pub` FFI export** for C++ callers. (`shekyl-ffi`) - **Genesis reproducibility artifacts**: `verify_genesis.py` script and `GENESIS_BUILD_INFO.txt`. (`shekyl-dev/tools/genesis_builder/`) ### 🔄 Changed - **`genesis_builder` print_usage updated to Bech32m.** Usage example now shows `` addresses instead of ``. ### 🐛 Fixed - **Fixed `core_tests` FCMP++ proof verification failures.** `gen_fcmp_tx_valid`, `gen_fcmp_tx_double_spend`, and `gen_staking_lifecycle` all failed with "FCMP++ proof verification failed" because test-chain block headers carried a placeholder `curve_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_roots` LMDB table) so both the prover and verifier read the correct historical root for any reference block height. Also aligned `compute_leaf_count_at_height` in `chaingen.cpp` with production `collect_outputs` logic (output-type filtering and `outPk` bounds checks). - **Reverted `vcpkg.json` manifest that broke MSVC CI.** Commit `397817b` introduced a `vcpkg.json` with `"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.FcmpPlusPlusTransaction` test.** Replaced ring-style `make_transaction` with `make_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. Deprecated `wallet_tools::gen_tx_src` with migration note pointing to the FCMP++ pipeline in `chaingen.cpp`. - **Fixed `rctSig` JSON serializer missing `message` and `referenceBlock`.** The JSON round-trip for `rct::rctSig` did not serialize the `message` field (tx prefix hash) or the `referenceBlock` field (for `RCTTypeFcmpPlusPlusPqc`). Both are part of the binary wire format in `rctTypes.h` but were silently lost during JSON serialization. Added `message` to all rctSig JSON output and `referenceBlock` for FCMP++ transactions. Discovered by the `FcmpPlusPlusTransaction` JSON round-trip test. - **`on_get_curve_tree_path` RPC consistency fix.** The RPC handler read `leaf_count` from tip state but returned a `reference_block` several blocks behind tip. If the tree grew in between, the returned leaf data and layer hashes did not match the reference block's `curve_tree_root`. Fixed by computing `ref_leaf_count` at `reference_height` via 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 in `chaingen.cpp`. - **MSVC portability batch.** Expanded `src/common/compat.h` with centralized platform-conditional includes for `unistd.h`/`io.h`, `dlfcn.h`, and `sys/mman.h`. Added `AND NOT MSVC` guards to `monero_enable_coverage` (GCC-only `--coverage` flags) and `enable_stack_trace` (GNU `ld` `-Wl,--wrap=__cxa_throw`). Fixed `bootstrap_file.cpp` `long` types to `std::streamoff`/`uint64_t` for LLP64 correctness. Fixed unsigned negation in `wallet2.cpp:772` (`std::advance(left, -N)` where N is `size_t`) with `static_cast`. Created root `vcpkg.json` manifest for deterministic dependency management. - **FCMP++ test harness: tree state mismatch.** `assemble_tree_path_for_output` and `construct_fcmp_tx` in `tests/core_tests/chaingen.cpp` read the current (tip) curve tree state but the verifier checks against the reference block's historical tree root. Fixed by computing `ref_leaf_count` at the reference block height and capping all leaf/layer reads to that count, with boundary chunk hash trimming via `shekyl_curve_tree_hash_trim_selene` for siblings that changed since the reference block. Also fixed a layer offset bug where sibling hashes were read from `layer` instead of `layer - 1`. - **FCMP++ test harness: staking tests missing FCMP++ pipeline.** `gen_staking_lifecycle` and `gen_stake_all_tiers` used `construct_staked_tx` which produced stub RCT signatures without FCMP++ proofs or PQC auth. Rewritten to use callback-based testing (like `gen_fcmp_tx_valid`) with a new `construct_fcmp_staked_tx` that routes through the full FCMP++ proving and PQC signing pipeline via `apply_fcmp_pipeline`. ### 🔄 Changed - **Unified constant-time comparison for all 32-byte crypto types.** `public_key`, `key_image`, and `hash` now use `crypto_verify_32` via `CRYPTO_MAKE_HASHABLE_CONSTANT_TIME` instead of `memcmp`-based `CRYPTO_MAKE_HASHABLE`. Eliminates the footgun of a developer choosing the non-constant-time macro for a new secret-bearing 32-byte type. - **Added `ct_signatures` type alias.** `using ct_signatures = rct::rctSig;` added in `cryptonote_basic.h` as the starting point for migrating away from the Monero-era `rct_signatures` name. Full caller migration and `rct::` namespace rename deferred to V4. - **Documented alternative tokens decision.** Keeping `/FIiso646.h` workaround for MSVC; mechanical replacement of `not`/`and`/`or` is high-effort, low-value. Recorded in STRUCTURAL_TODO.md. - **Workspace-wide clippy cleanup.** Resolved all `cargo clippy --all-targets --no-deps -- -D warnings` errors across the Rust workspace (14 crates, 52 files). Key changes: replaced `as u128` casts with `u128::from()`, added `#[allow]` for intentional truncation in economics/FFI code, marked FFI `extern "C"` functions `unsafe` with `# Safety` docs, replaced redundant closures with method references, used `let...else`, switched `from_slice` to `GenericArray::from()` in chacha20poly1305, changed `&Vec` to `&[T]` in public APIs. No behavioral changes. ### ✨ Added - **Fuzz target for `derive_output_secrets`.** New `fuzz_derive_output_secrets` cargo-fuzz harness in `rust/shekyl-crypto-pq/fuzz/`. Exercises arbitrary `combined_ss` inputs (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_roundtrip` test in `rust/shekyl-ffi/` with locked vectors in `docs/test_vectors/WITNESS_HEADER.json`. Proves `shekyl_fcmp_build_witness_header` (writer) and `parse_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=0` T-component cannot be implemented: the verifier does not know `y` (it is a KEM-derived secret) and testing whether `O` lies in the G-only subgroup requires knowing the DL between G and T. Defense is structural via `derive_output_secrets` hard-assert and fuzz coverage. Closes FOLLOWUPS.md y=0-consensus item. - **scheme_id binding analysis corrected in `PQC_MULTISIG.md`.** The `expected_scheme_id` parameter in `verify_transaction_pqc_auth` is unused because FCMP++ hides which output is being spent. Scheme downgrade protection is provided by the `h_pqc` curve tree leaf commitment — the FCMP++ proof binds `H(hybrid_public_key)` to the leaf, making a downgrade require a Blake2b-512 collision. Updated Attack 1 mitigation description and `POST_QUANTUM_CRYPTOGRAPHY.md` accordingly. - **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.rs` now exercises FFI, `AUDIT_SCOPE.md` exists, C++20-isms audit complete, easylogging++ MSVC fully fixed, `wallet2.h:2324` bool/char pattern removed by wallet refactoring. Updated 2 stale references: `simplewallet.cpp` deleted (removed from `long` type sites and `memcmp` resolution list), `wallet2.cpp:782` shifted to line 772. Updated test `memcmp` count from 84 to ~90. Annotated `expected_scheme_id` removal 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.conf` references. 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.md` reference (covered in `FCMP_PLUS_PLUS.md`), v2.0 tx references (should be v3). - `INSTALLATION_GUIDE.md`: `FCMP_PLUS_PLUS.md` exists, 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 on `derive_output_secrets`, witness header round-trip test, y=0 consensus check, and `AUDIT_SCOPE.md` creation. - KEM plan: Updated 18 todo items from `pending` to `completed` matching actual codebase state. ### 🗑️ Removed - **`tests/unit_tests/address_from_url.cpp` deleted.** The test referenced `MONERO_DONATION_ADDR` (removed constant) and tested Monero OpenAlias DNS resolution against `donate.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. Removed `src/simplewallet/` directory, CMake target, CI artifact references, and Windows installer entries. The `translations/` directory retains simplewallet-era `.ts` strings as dead entries within shared i18n files. - **`wallet/api/` C++ wrapper layer deleted.** The 3,909-line Monero-era C++ wrapper (`wallet2_api.h` and 10 implementation files) had no production consumer -- the GUI uses `wallet2_ffi` via `shekyl-wallet-rpc` (Rust). Removed `src/wallet/api/` directory, `tests/libwallet_api_tests/`, and the `add_subdirectory(api)` entry from `src/wallet/CMakeLists.txt`. Cleaned up stale `#include "wallet/api/*.h"` references in `object_sizes.cpp` and `address_from_url.cpp`. ### 🐛 Fixed - **19 `core_tests` failures and SEGFAULT from v3 transaction incompatibility.** The test framework's `construct_miner_tx_manually` was hardcoded to produce v2 transactions without PQC output construction, causing 16 block validation tests to fail during generation and a SEGFAULT in `tx_validation` tests. Rewrote the function to perform genuine v3 output construction via `shekyl_construct_output` FFI. Added `append_v3_output_to_miner_tx` helper for tests that add outputs to coinbase. Fixed `fill_tx_sources` to populate `ho`/`v3_ho_valid` on source entries via `try_v3_scan_output`. Removed stale classical key derivation from view tag tests. Fixed serialization consistency in tests that modify `vout`/`vin` without updating `rct_signatures` fields. - **Non-exhaustive `TxBuilderError` match in FFI error-code mapping.** Commit `aff9f777` added `TreeDepthTooLarge(u8)` to `TxBuilderError` but did not add the corresponding arm to `tx_builder_error_code()` in `shekyl-ffi`, breaking CI compilation on all platforms. Added `TreeDepthTooLarge(_) => -27`. ## [core-v3.1.0] - 2026-04-13 ### 🔄 Changed - **Dev merged into main.** 128 commits from `dev` promoted to `main` including: 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 as `core-v3.1.0` for GUI wallet CI pinning. ### ✨ Added - **`shekyl-cli` full parity with simplewallet (40 of 81 commands).** The `rust/shekyl-cli/` crate now covers all actively-used simplewallet functionality. Key additions since the initial scaffold: - **Security-hardened UX**: `display.rs` for secret display with TTY checks, multiplexer warnings, best-effort scrollback clear, and honest residual-scrollback warning. `errors.rs` for JSON-RPC error sanitization (strips paths/hex; `--debug` routes raw errors to stderr or 0600 log file, never stdout). Context-specific `confirm_dangerous()` tokens for destructive operations (sweep amount, address prefix, acknowledgment phrase). - **Stateless account model**: `ReplSession` holds session-default account on REPL stack; `ResolvedCommand` enum resolves `--account N` at parse time. No wallet-level current-account state. `--subaddr-index`/`--subaddr-indices` for subaddress selection. - **Independent daemon client**: `daemon.rs` using ureq (rustls backend, pinned) for `chain_health`. SOCKS stream isolation via distinct auth username. `--daemon-ca-cert` and `--proxy` CLI flags. Differentiated error reporting (5 failure modes). - **Staking**: `stake`, `unstake`, `claim`, `staking_info`, `chain_health`. - **Keys**: `viewkey`, `spendkey` with terminal safety; `export_key_images` (0600 permissions, `--since-height`, `--all`); `import_key_images` with 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-relay` on `transfer`. - **Signing**: `sign`, `verify` (domain separation documented), `version`, `wallet_info` (no filename). - **Input validation**: `validate.rs` with hex, txid, address, and input-length validators. - **Fuzz tests**: `proptest` dev-dependency with 14 property tests for amount parsing, hex validation, address validation, and argument parsing. - **Parity matrix**: `docs/CLI_PARITY_MATRIX.md` maps 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. - **CI gate: `dalek-ff-group` version isolation.** Added a workflow step that asserts `shekyl-ffi`'s normal dependency tree never pulls in `dalek-ff-group` v0.4. The 0.4 version is allowed transitively inside `ciphersuite` internals 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!`, and `println!` 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_FOREACH` usage is reintroduced via upstream cherry-picks. All 31 prior instances were replaced with range-based for loops. ### 🔄 Changed - **CI lint: exclude `shekyl-cli` from debug-macro ban.** The interactive CLI REPL legitimately uses `println!`/`eprintln!` for terminal output. The lint now skips `rust/shekyl-cli/` to avoid false positives on binary crate I/O. ### 🐛 Fixed - **[CONSENSUS] Genesis TX blobs upgraded to v3 wire format.** The hardcoded `GENESIS_TX` hex in `cryptonote_config.h` (mainnet, testnet, stagenet) was still in the legacy v2 format, missing the `enc_amounts` and `outPk` arrays required by the current `serialize_rctsig_base`. Updated all three blobs to v3 (`tx.version = 3`) with zero-filled `enc_amounts`/`outPk` for `RCTTypeNull` coinbase. This was the root cause of `core_tests` SEGFAULT, `block_weight` failure, and wallet init failures in CI. - **JSON serialization now includes `enc_amounts`/`commitments` for `RCTTypeNull` coinbase.** The `toJsonValue`/`fromJsonValue` for `rct::rctSig` previously skipped these fields for `RCTTypeNull`, 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_auth` test used hardcoded empty cnonce.** The test computed the expected MD5 digest with `cnonce=""` while the production `http_auth.cpp` generates a random cnonce. Fixed to extract the actual cnonce from the parsed auth response. ### 🗑️ Deprecated - **`test::make_transaction` ring-style helper.** The helper constructs Monero-era ring-signature source entries incompatible with v3/FCMP++ transaction construction. `BulletproofPlusTransaction` is `GTEST_SKIP`'d pending FCMP++ test infrastructure. - **[CONSENSUS-ADJACENT] Branch layer depth validation off-by-one in `shekyl-tx-builder`.** The rule `c1 + c2 == depth` was corrected to `c1 + 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 == c2` or `c1 == c2 + 1`), the error.rs doc was corrected (previously stated the relationship backwards), and `MAX_TREE_DEPTH=24` was added as a named constant in `shekyl-fcmp` with 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.rs` now exercises the full C-ABI FFI boundary: KEM keypair generation, output construction, output scanning, curve tree leaf/root computation, JSON serialization of `FcmpSignInput` + `OutputInfo`, signing via `shekyl_sign_fcmp_transaction`, and verification via `shekyl_fcmp_verify`. Runs 10 iterations with different random seeds. Previously called `proof::prove` directly, 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 in `src/shekyl/shekyl_ffi.h` to `///` 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 the `SHEKYL_PROVE_WITNESS_HEADER_BYTES` comment from `DEPRECATED`/`TODO` language to document its role as test infrastructure for `genRctFcmpPlusPlus` in `core_tests`. ### 🔄 Changed - **`simplewallet` marked deprecated.** Added a yellow deprecation banner to `simplewallet.cpp` startup: "shekyl-wallet-cli is deprecated and will be removed. Use shekyl-cli instead." No new features will be added; the binary will be deleted once `shekyl-cli` reaches parity. - **Axum RPC binds to standard port.** When `--no-rust-rpc` is 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 to `epee_port + 10000`. - **Production `eprintln!` removed from Rust FFI.** Replaced 6 `eprintln!` calls in `shekyl-ffi/src/lib.rs` error handlers with silent error suppression (the C++ caller checks the bool return). Converted 1 `eprintln!` in `shekyl-daemon-rpc/src/ffi_exports.rs` to `tracing::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/`, and `tests/benchmark.cpp` to remove references to `derive_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`, and `generate_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 in `chaingen.cpp` and `chain_switch_1.cpp` falls through to the v3 scan path. All `additional_tx_keys` parameters removed from `construct_tx_and_get_tx_key` call sites. Benchmark harnesses for `derive_subaddress_public_key` and 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 the `rpc_pub`, `daemon_rpc_server`, `daemon_messages` CMake targets. Removed `libzmq` build 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 the `zeromq.mk` depends recipe with its patches. Removed `--zmq-rpc-bind-ip`, `--zmq-rpc-bind-port`, `--zmq-pub`, `--no-zmq` CLI arguments. ZMQ was a duplicate, unauthenticated RPC surface inherited from an abandoned Monero "migrate RPC to ZMQ" effort. It had zero first-party consumers, leaked `do_not_relay` transactions, 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`) from `json_serialization.cpp`, and fixed daemon link order (`rpc` after `${SHEKYL_DAEMON_RPC_LINK_LIBS}`) to resolve circular FFI back-references previously satisfied transitively through `daemon_rpc_server`. - **`wallet/api/` C++ wrapper layer deleted (~3,900 lines).** The `src/wallet/api/` directory (22 files) wrapped `wallet2` for GUI consumption. With the Tauri GUI using `wallet2_ffi` via Rust, no production consumer remained. Removed the directory, `add_subdirectory(api)` from wallet CMakeLists, `wallet/api` includes and sizeof reporters from `object_sizes.cpp`, broken includes in `subaddress.cpp` and trezor tests, `wallet_api` link target from trezor CMakeLists, and CI `--target wallet_api` build steps. - **`libwallet_api_tests/` test suite deleted (~1,300 lines).** Removed the `tests/libwallet_api_tests/` directory and its CMake entry. Cleaned up the Makefile's `libwallet_api_tests` ctest exclusions (originally disabled for Issue #895, now fully removed). Also removed the `wallet_api_tests` class and implementation from trezor tests. - **`load_deprecated_formats` / `is_deprecated` dead code excised (Phase 6 completion).** Removed the `is_deprecated()` method, `is_old_file_format` member, `m_load_deprecated_formats` member and its getter/setter from `wallet2.h`. Deleted the `is_deprecated()` definition, JSON save/load of `load_deprecated_formats`, the non-JSON wallet keys file fallback (now a hard error), and the boost `portable_binary_iarchive` version `\003`/`\004` branches in `parse_unsigned_tx_from_str` and `parse_tx_from_str` from `wallet2.cpp`. Removed the `set_load_deprecated_formats` command, its `CHECK_SIMPLE_VARIABLE` entry, settings display line, and the `is_deprecated()` upgrade flow from `simplewallet.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_keys` infrastructure fully removed.** Deleted member variables, struct fields, serialization entries, and function parameters referencing additional transaction keys from `wallet2.h`, `wallet2.cpp`, `cryptonote_tx_utils.h/.cpp`, `cryptonote_format_utils.h`, `device.hpp`, `device_default.hpp/.cpp`, and `device_ledger.hpp/.cpp`. In `wallet2.cpp`, removed all `additional_tx_pub_keys` / `additional_tx_keys` local variables, derivation computation loops, `m_additional_tx_keys` map operations, `etd.m_additional_tx_keys` export/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. The `conceal_derivation` device method implementations were updated to match the simplified signatures (no additional keys/derivations parameters). The `ABPkeys` struct no longer carries `additional_key`. Cleaned up all remaining call sites across `wallet2_ffi.cpp`, `wallet/api/wallet.cpp`, `simplewallet.cpp`, `wallet_rpc_server.cpp`, and `trezor/protocol.cpp` — removing additional-key parsing loops, serialization, and pass-through parameters. `get_additional_tx_pub_keys_from_extra` is 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`, and `derivation_to_scalar` removed from the device interface chain.** Deleted the pure virtual declarations from `device.hpp` and all override implementations from `device_default` and `device_ledger`. Also deleted `derive_public_key` and `derive_secret_key` from `crypto.cpp`/`crypto.h` (kept `derivation_to_scalar` in crypto, still needed by `derive_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 in `cryptonote_tx_utils`. - **`out_can_be_to_acc`, `is_out_to_acc_precomp`, and `derive_view_tag` dead code removed.** Deleted the Keccak-based `out_can_be_to_acc` and `is_out_to_acc_precomp` functions from `cryptonote_format_utils`, the `derive_view_tag` function from `crypto`, and the `derive_view_tag` virtual 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. - **`ecdhHash` and `genCommitmentMask` dead code removed.** Deleted the `ecdhHash` and `genCommitmentMask` function definitions from `rctOps.cpp`, their declarations from `rctOps.h`, the `genCommitmentMask` virtual method from the device interface chain (`device.hpp`, `device_default`, `device_ledger`), and the `ecdhDecode` unit 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_count` parameters from `create_transactions_2`, `create_transactions_all`, `create_transactions_single`, and `create_transactions_from`. Removed `transfer_selected_rct`'s `fake_outputs_count` and `outs` parameters. Deleted `get_output_relatedness`, `outs_unique`, `m_print_ring_members`, and `m_rings` bookkeeping. FCMP++ eliminates ring signatures, making decoy selection and output relatedness scoring dead code. ### 🔒 Security - **`m_combined_shared_secret` changed to `scrubbed_arr` (Phase 6, Gate 3).** Replaced `std::vector` with `tools::scrubbed_arr` in both `transfer_details` and `exported_transfer_details`. This ensures zero-on-drop semantics consistent with `m_y` and `m_mask`. A boolean `m_combined_shared_secret_set` flag 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()` to `WalletState` verifying 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.rs` exercises `construct_output` → `scan_output_recover` → `derive_proof_secrets` → `compute_key_image` with random keys and amounts via `proptest`. Asserts determinism (same inputs → identical outputs) and non-zero secrets (`ho`, `y`, `z`, `k_amount`, `key_image`). Includes boundary cases for `amount=0` and `amount=u64::MAX`. Runs with `--release` in CI. - **Wallet cache AEAD tests (Phase 6, Gate 2).** `cache_crypto.rs` covers 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_100` in `proof_round_trip.rs` runs full outbound prove+verify cycle 100 times with unique randomness per iteration. - **`unmark_spent` unit 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).** `proptest` drives random sequences of `AddOutputs`, `MarkSpent`, `UnmarkSpent`, `Freeze`, `Thaw`, and `Reorg` operations, asserting `check_invariants()` after each step. - **Sync bookkeeping tests (Phase 6, Gate 7).** Mock-block-driven tests for `WalletState` mutations: 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_y` absence, `derivation_to_y_scalar` absence, legacy RCT type absence, v1/v2 tx version branch absence, `HASH_KEY_TXPROOF` absence, `combined_shared_secret` confinement to wallet boundary, `ecdhEncode`/`ecdhDecode` confinement to Ledger gate. All run without `continue-on-error`. - **FFI header documentation (Phase 6).** `shekyl_ffi.h` now has Doxygen-style file-level documentation covering the memory model, secret handling conventions, and error reporting contract. ### 🗑️ Removed - **`derivation_to_y_scalar` deleted (Phase 6).** Removed the function body from `crypto.cpp`, declarations from `crypto.h`, and all call sites in `derive_public_key` and `derive_subaddress_public_key`. The `"shekyl_y"` salt no longer appears in the binary. - **Test stubs 9-10 deleted (Phase 6).** Removed `#[ignore]` placeholder tests `test_09_watch_only_outbound_proof_error` and `test_10_restored_wallet_outbound_proof_error` from `proof_round_trip.rs`. Future implementations tracked in `WALLET_STATE_MIGRATION.md`. - **Dead v1/v2 transaction branches in consensus (Phase 5).** `check_tx_outputs` now rejects `tx.version < 3` instead of `< 2`. Removed redundant `if (tx.version >= 2)` zero-amount guard (now unconditional). Tightened coinbase version check from `>= 2` to `>= 3`. Removed dead `tx.version < 3` early return in `check_commitment_mask_valid`. Commitment mask checks are now unconditional (version is always >= 3). - **Dead legacy code excision (Phase 6 completion).** Deleted `decodeRctSimple` and its overload from `rctSigs.cpp/.h`. Deleted `tools::decodeRct` wrapper and all callers in `wallet2.cpp`. Deleted `generate_output_ephemeral_keys` declaration from `cryptonote_tx_utils.h`. Deleted `tx_proof.cpp` unit test (referenced removed `crypto::generate_tx_proof_v1`). Deleted `is_out_to_acc.h` performance test and its registrations. - **`generate_key_image_helper` / `generate_key_image_helper_precomp` fully removed.** Migrated remaining production callers in `wallet2.cpp` (`export_key_images`, two `import_outputs` overloads) to the v3 HKDF path via `shekyl_derive_proof_secrets` FFI. Replaced dead `else` branch in `cryptonote_tx_utils.cpp::construct_tx_with_tx_key` with a hard error. Replaced `scan_output`'s `generate_key_image_helper_precomp` call with a v3-only assertion (function is dead for v3 scanning). Deleted both function definitions from `cryptonote_format_utils.cpp/.h`, the `compute_key_image` virtual method from `device.hpp` and its Trezor override in `device_trezor.hpp/.cpp`. Updated test callers in `chaingen.cpp` and `tx_validation.cpp` to use v3 `sc_add(ho, b)` derivation. ### 🔒 Security (Phase 5 Audit Notes) - **Consensus hardening: commitment mask validation verified (Phase 5).** Audited `check_commitment_mask_valid` in `blockchain.cpp`: confirms rejection of identity commitment (mask=0, amount=0), generator-point commitment (mask=1, amount=0), and coinbase `zeroCommit(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])` and `assert!(ho != [0u8; 32])` in `derive_output_secrets` (Rust, release-mode assert). Both sender (`construct_output`) and receiver (`scan_output_recover`) hit the same assert. Documented in `POST_QUANTUM_CRYPTOGRAPHY.md` with full defense stack analysis. ### ✨ Added - **GUI wallet native-sign activation (Phase 4a).** Added `native-sign` feature to the GUI wallet's `shekyl-wallet-rpc` dependency. The transfer path is now: C++ prepare → Rust sign → C++ finalize. - **Scanner keys FFI export (Phase 4b).** Added `wallet2_ffi_get_scanner_keys` to the wallet2 FFI layer, returning all keys needed by the Rust scanner (spend/view secrets, X25519 SK, ML-KEM DK) as JSON. Added `get_scanner_keys` wrapper method to `Wallet2`. - **Hybrid PQC KEM scanner (Phase 3a).** `shekyl-scanner` now scans blocks using the V3 two-component key derivation: X25519 + ML-KEM-768 hybrid KEM. The `InternalScanner::scan_transaction` pipeline parses `TX_EXTRA_TAG_PQC_KEM_CIPHERTEXT` (0x06), applies X25519 view-tag pre-filtering (~99.6% rejection), and calls `scan_output_recover` for full KEM decapsulation, HKDF secret derivation, amount decryption, and B' recovery. Key images are computed natively in Rust via `hash_to_point` + `compute_output_key_image`. Legacy ECDH scan path removed. - **`RecoveredWalletOutput` struct.** New scan result type carrying all KEM-derived secrets (`ho`, `y`, `z`, `k_amount`, `combined_shared_secret`), the computed `key_image`, and decrypted `amount` alongside the base `WalletOutput`. Implements `ZeroizeOnDrop` — secrets are wiped when the struct leaves scope. - **`TransferDetails` PQC fields and `eligible_height`.** Extended with `ho`, `y`, `z`, `k_amount`, `combined_shared_secret` (all `Zeroizing`) and `eligible_height: u64` (`block_height + SPENDABLE_AGE`). Outputs below `eligible_height` are immature (no curve-tree path) and cannot be spent. `is_spendable()` enforces this gate. - **`WalletState` KEM-aware processing.** `process_scanned_outputs` now populates all PQC fields from `RecoveredWalletOutput`, sets key images at scan time, and performs duplicate output key detection (burning bug). `spendable_outputs` filters on `eligible_height`. - **`unmark_spent` for rollback.** `WalletState::unmark_spent` reverses 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_loop` polls the daemon RPC for new blocks, feeds them through the hybrid KEM scanner, detects spent outputs via key-image matching against block inputs, and emits `SyncProgress` events after each block. Cancellation-safe via `tokio_util::CancellationToken`. Configurable flush interval: every 100 blocks on desktop, every block on mobile (OS can kill without warning). - **`BalanceSummary` uses `eligible_height`.** Timelock categorization now reads `td.eligible_height` directly instead of recomputing from `block_height + DEFAULT_LOCK_WINDOW`. - **`ViewPair` extended with KEM keys.** Added `x25519_sk` and `ml_kem_dk` fields to `ViewPair` for hybrid KEM decapsulation. The scanner requires both the X25519 secret and ML-KEM decapsulation key. ### 🐛 Fixed - **Stale `fake_outs_count` arguments in wallet transaction creation.** Removed vestigial `0` (decoy count) from 9 call sites across `wallet2_ffi.cpp`, `wallet_rpc_server.cpp`, and `wallet/api/wallet.cpp` that no longer match `create_transactions_2`, `create_transactions_all`, and `create_transactions_single` signatures after ring removal. - **Test compilation: `wallet_tools.cpp` and `transactions_flow_test.cpp`.** Replaced removed `td.is_rct()` calls with `true` (all Shekyl outputs are RCT), changed `tools::wallet2::get_outs_entry` to the local typedef from `chaingen.h`, and removed stale `mix_in_factor` argument in the functional test. - **PQC doc label error.** Fixed incorrect HKDF label reference in `POST_QUANTUM_CRYPTOGRAPHY.md`: the output-key check uses `ho` with label `shekyl-output-x`, not `shekyl-pqc-output` (which is the ML-DSA seed label). - **Test compilation: `json_serialization.cpp` aggregate init.** Replaced brace-enclosed initializer list for `tx_source_entry` with explicit member assignment. The struct is no longer an aggregate (user-declared destructor for `ho` wiping) and the old initializer also referenced a removed `real_out_additional_tx_keys` field. - **Multi-output scan bug.** Removed erroneous `break` in `InternalScanner::scan_transaction` that 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`.** Rewrote `WalletState::handle_reorg` to use `(height, hash)` pairs instead of treating height as a direct vector index. Correctly handles non-genesis-aligned and sparse sync histories. `synced_height` is now derived from the last remaining block entry. - **Reorg detection in sync loop.** `run_sync_loop` now compares each incoming block's `header.previous` hash against the wallet's stored hash for the prior height. On mismatch, walks backwards to find the fork point and calls `handle_reorg` before resuming. - **Block fetch retry with backoff.** Per-block `get_scannable_block_by_number` calls 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.** `TransferDetails` now implements both `Zeroize` (covering all fields including `key`, `commitment`, and `fcmp_precomputed_path`) and `Drop` (calls `zeroize()` on drop). `WalletState` implements `Drop` to wipe all transfers, key images, pub keys, and block hashes. Removed unsafe `#[derive(Clone, Debug)]` from `TransferDetails`; `Debug` is now manual and redacts secret fields. - **Misleading payment ID comment.** Corrected comment in `scan.rs` that 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 - **`EncryptedAmount` wire format fix.** The Rust `EncryptedAmount` struct (in `shekyl-oxide::fcmp`) now correctly includes both `amount: [u8; 8]` and `amount_tag: u8`, matching the C++ 9-byte wire format. Previously only the 8-byte amount was read, causing silent data misalignment. - **`Scanner::new` signature.** Now requires the wallet's `spend_secret` (`Zeroizing<[u8; 32]>`) for native key image computation at scan time. Both `Scanner::new` and `GuaranteedScanner::new` updated. - **Deterministic KEM encapsulation from `tx_key_secret`.** `construct_output` now 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-derive `combined_ss` at proof time from `tx_key_secret` and 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`, and `compute_output_key_image_from_ho`. These support the V3 tx_proof / reserve_proof / key-image protocols. The narrow `ProofSecrets(ho, y, z, k_amount)` projection ensures `combined_ss` never crosses the FFI boundary. - **`ProofSecrets` widened to include `z`.** The Pedersen commitment mask is now part of the proof secrets projection, enabling direct `C = z*G + amount*H` verification in TX proofs. `derive_proof_secrets` passes `z` through instead of discarding it. - **`shekyl-proofs` crate: full Phase 1a implementation.** Three modules: - `dleq.rs`: Two-base Schnorr DLEQ proof with domain separator `shekyl-reserve-proof-dleq-v1` and 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-output `ho`, `y`, `z`, `k_amount` with algebraic output key and commitment checks. - `reserve_proof.rs`: Reserve proof (69+192N bytes) with per-output DLEQ key image binding. `enc_amount` sourced 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 into `transfer_details` fields (no intermediate scratch buffers). `persist_combined_ss` flag controls whether `combined_ss` is 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 key `b` + per-input `{combined_ss, output_index, ...}`. Rust derives `x = ho + b` and `y` internally via HKDF. C++ never touches `x`. - `shekyl_derive_proof_secrets`: Helper writing `ho`, `y`, `z`, `k_amount` directly to caller-provided destination addresses. - `shekyl_encrypt_wallet_cache` / `shekyl_decrypt_wallet_cache`: AEAD encryption with AAD binding on `cache_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 to `shekyl-proofs` internals deferred to Phase 2e. - **`shekyl-chacha` AEAD extension.** Added `chacha20poly1305` (v0.10) support: `encrypt_with_aad` and `decrypt_with_aad` wrapping XChaCha20-Poly1305. No hand-rolled AEAD — nonce handling, constant-time tag comparison, and AD framing delegated to audited crate. 6 new tests. - **`RecoveredOutput` now includes `combined_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 by `ZeroizeOnDrop`. - **ML-KEM shared secret `Zeroizing` wrap (W5 fix).** All 4 production sites where `ml_ss.into_bytes()` produces a bare stack-local now wrap the result in `Zeroizing<[u8; 32]>`, ensuring the ML-KEM shared secret bytes are zeroed on scope exit. Closes the W5 correlation leak. - **Fixed stale `shekyl_construct_output` C header.** Added missing `tx_key_secret` parameter to match the Rust implementation. - **KEM derivation KAT vectors.** `docs/test_vectors/KEM_DERIVE_V1_KAT.json` with 8 pinned vectors for `derive_kem_seed`. Serves as tripwire against silent behavior changes from `fips203` or `curve25519-dalek` upgrades. - **`fips203` exact version pin.** Pinned to `=0.4.3` with audit comment explaining the `DummyRng::fill_bytes = unimplemented!()` risk. - **Fuzz target for `derive_output_key`.** Exercises `derive_output_key` and `recover_recipient_spend_pubkey` round-trip with fuzzer-supplied inputs. - **Ledger V3 hard gate.** `device_ledger.cpp` now has a `#error` that fires when `WITH_DEVICE_LEDGER` is 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_ct` exercises corrupted, truncated, and random ML-KEM ciphertexts through `scan_output_recover` with 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_tag` as a ~99.6% cheap pre-filter (performance optimization), not a security gate. Commitment algebraic check `C == z*G + amount*H` is 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_version` in 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` / `ecdhDecode` removal.** Deleted the Monero-era ECDH amount-masking struct and encode/decode functions from `rctTypes.h`, `rctOps.h/.cpp`, `device.hpp`, `device_default.hpp/.cpp`, `device_ledger.hpp/.cpp`, and the Trezor protocol files. The `enc_amount_to_ecdh_compat` shim is deleted. - **`check_tx_key_helper` / `is_out_to_acc` deletion.** Both overloads of `wallet2::check_tx_key_helper` and `wallet2::is_out_to_acc` removed. These used `derive_public_key` (Keccak Category 1) and the old ecdhDecode path. Replaced by KEM-based proof FFI round-trip in `check_tx_key`. - **`crypto::generate_tx_proof` / `generate_tx_proof_v1` / `check_tx_proof` deletion.** Monero-era DH-based Schnorr proof functions removed from `crypto.cpp`, `crypto.h`, `device_default.cpp`, `device_ledger.cpp`, `device.hpp`, and derived device headers. `HASH_KEY_TXPROOF_V2` removed from `cryptonote_config.h`. - **`ecdh.rs` module stub cleanup.** Removed orphaned `mod ecdh` declaration and associated test functions from `shekyl-tx-builder` (module file was previously deleted, declaration left behind). - **V3-from-genesis Boost serialization purge (`wallet2.h`).** Deleted all `if (ver < N)` migration branches from Boost `serialize` functions for `transfer_details`, `unconfirmed_transfer_details`, `confirmed_transfer_details`, `payment_details`, `address_book_row`, `unsigned_tx_set`, `signed_tx_set`, `tx_construction_data`, and `pending_tx`. Deleted the `initialize_transfer_details` helper (both saving and loading overloads). Reset all `BOOST_CLASS_VERSION` macros to 1 (genesis version). Added `assert(ver == 1)` guards. Epee cache envelope `if (version < N)` branches also removed, replaced with `assert(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 the `transfer_details` Boost serializer. Legacy `m_rct` field 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 the `shekyl-proofs` Rust crate via the FFI bridge. `check_tx_key` also uses the FFI round-trip (generate outbound proof + verify with on-chain data). The intermediate C++ helpers `check_tx_key_helper` (both overloads) and `is_out_to_acc` have been deleted. New `gather_on_chain_proof_data` helper 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. Removed `HASH_KEY_TXPROOF_V2` from `cryptonote_config.h`. Removed orphaned `ecdh.rs` module declaration and tests from `shekyl-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. `ecdhHash` and `genCommitmentMask` have been removed. - **Phase 2d: Collapsed signing via `shekyl_sign_fcmp_transaction` (PR-wallet).** The CLI wallet's `transfer_selected_rct` now calls the Rust collapsed signing FFI instead of C++ `genRctFcmpPlusPlus`. C++ builds JSON arrays of `FcmpSignInput` (per-input `combined_ss`, `output_index`, tree layers) and `OutputInfo` (per-output `commitment_mask`, `enc_amount`), then unpacks the returned `SignedProofs` (BP+ blob, FCMP++ proof, pseudo-outs, commitments, enc_amounts) into `tx.rct_signatures`. Rust owns all witness assembly — C++ never touches the ephemeral spend secret `x`. `genRctFcmpPlusPlus` is deprecated (retained only for `chaingen.cpp` test infrastructure). - **Rust `sign_transaction` updated for v3 HKDF semantics (PR-wallet).** `OutputInfo` now carries `commitment_mask: [u8; 32]` and `enc_amount: [u8; 9]` (pre-derived by `construct_output`), replacing the old `amount_key` field. `SignedProofs.enc_amounts` widened 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.cpp` `enc_amounts` field name fix.** The native-sign finalize path now reads `enc_amounts` from Rust `SignedProofs` JSON (was incorrectly reading `ecdh_amounts`). - **`enc_amounts` field comment updated in `rctTypes.h`.** Clarifies that byte [8] is the HKDF-derived `amount_tag` AAD, documents the Rust scanner validation behavior (reject on mismatch), and removes the stale `RESERVED_AMOUNT_TAG_PLACEHOLDER` reference. - **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 in `staking.cpp` constructs 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. - **`u128` saturation 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 `BlockchainLMDB` write paths (block connect, block pop, txpool, alt blocks, staking, FCMP++ curve tree). Documented in `docs/LMDB_WRITE_ATOMICITY_AUDIT.md`. Found and fixed a missing `lock.commit()` in `get_relayable_transactions` (Dandelion++ timestamp rollback bug) and added a defensive `db_wtxn_guard` around the staker accrual reversal in `pop_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 OpenLDAP `mdb.master` branch), applied upstream patches (ITS#9385, ITS#9496, ITS#9500, etc.), CVE review (CVE-2026-22185 does not affect us), and the `mdb.master` vs `mdb.master3` branch 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-core` crate.** New Rust crate providing transaction builder plans for stake, unstake, and claim operations. Includes `ClaimTxBuilder` for constructing claim transaction plans with automatic MAX_CLAIM_RANGE splitting, and `ClaimAndUnstakePlan` for 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.** `WalletState` now supports freeze/thaw of individual outputs by index or key image, with frozen outputs excluded from spendable candidate lists. New `spendable_outputs()` method with optional account, subaddress, and minimum amount filters. - **Staker pool tracking in Rust (`shekyl-scanner/staker_pool.rs`).** Wallet-side `StakerPoolState` mirrors 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.** `TransferDetails` now carries `last_claimed_height` for monotonic claim watermark management. `WalletState` exposes `update_claim_watermark()`, `claimable_outputs()`, and `claimable_rewards_summary()` methods. New `ClaimableInfo` struct provides per-output claim state including accrual frozen status. - **New RPC methods.** `get_claimable_stakes`, `get_unstakeable_outputs`, `freeze`, and `thaw` added to the Rust scanner-backed RPC handler. All four are routed through the Rust scanner when `rust-scanner` feature is active. - **GUI wallet staking bridge.** `wallet_bridge.rs` extended with `get_scanner_claimable_stakes`, `get_scanner_unstakeable_outputs`, `scanner_freeze`, and `scanner_thaw` for Tauri frontend integration. - **Staking transaction types in `shekyl-oxide`.** `Input::StakeClaim` variant (binary tag 0x03) and `Output::staking: Option` (binary tag 0x04) added with full binary serialization/deserialization. `StakingMeta` carries the `lock_tier` field (`lock_until` is 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-chacha` crate.** New Rust crate providing XChaCha20 (192-bit nonce) stream cipher for wallet and cache file encryption. Wraps the NCC-audited RustCrypto `chacha20` crate. Exported via FFI as `xchacha20()`, replacing the C implementation in `chacha.c`. - **KEM-derived output secrets (`OutputSecrets`).** New Rust infrastructure in `shekyl-crypto-pq/src/derivation.rs` derives 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. Includes `derive_view_tag_x25519` for 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 = 256` defined in both `shekyl_ffi.h` and `shekyl-ffi/src/lib.rs`, replacing all magic literal 256 values. - **Consensus `mask=1` placeholder.** `check_commitment_mask_valid()` wired into `check_tx_outputs` for all v3 transactions. Returns accept-all now; PR-construct will flip to reject `zeroCommit` form for non-coinbase. - **HKDF label registry.** `docs/POST_QUANTUM_CRYPTOGRAPHY.md` now 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`).** New `shekyl-crypto-pq/src/output.rs` implements `construct_output` (KEM encapsulation + HKDF → two-component key `O = ho*G + B + y*T`, Pedersen commitment `C = z*G + amount*H`, encrypted amount, view tag, PQC leaf hash) and `scan_output_recover` (KEM decapsulation + HKDF → recovered spend key `B' = O - ho*G - y*T` for 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 via `shekyl_fcmp_build_witness_header` with a typed `ProveInputFields` struct, replacing 8 raw `memcpy` calls in C++. - **`construct_miner_tx` and `construct_tx_with_tx_key` rewired to Rust.** Both v3 output construction paths now call `shekyl_construct_output` per output in a unified loop. KEM ciphertexts and PQC leaf hashes are written to `tx_extra`. The legacy `derivation_to_y_scalar` path is retired on all construction paths. - **Wallet scanner uses `scan_output_recover`.** `wallet2::process_new_transaction` has a v3-specific scanning path that calls `shekyl_scan_output_recover` for 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_keys` removed for v3.** `need_additional_txkeys` is false for `tx.version >= 3`. The `additional_tx_public_keys` field is no longer populated or consumed in v3 construction or scanning. - **Real Pedersen commitments for coinbase (`RCTTypeNull`).** `outPk` and `enc_amounts` are now serialized for `RCTTypeNull` transactions. `blockchain_db.cpp` uses the on-chain `outPk[i].mask` for v3+ coinbase instead of computing `zeroCommit(amount)`. - **`check_commitment_mask_valid` enforced.** Rejects trivial commitment masks (`z = 0` or `z = 1`) for all non-coinbase v3 outputs. Called from both `check_tx_outputs` and `prevalidate_miner_transaction`. - **PQC salt consolidation.** All per-output PQC key derivation now uses the unified `OutputSecrets.ml_dsa_seed` from salt B (`shekyl-output-derive-v1`). The legacy `HKDF_SALT_PQC_DERIVE` salt A is deleted. **Testnet reset required** — invalidates all existing `h_pqc`. - **Chaingen test infrastructure updated for v3.** `init_output_indices`, `fill_tx_sources`, `init_spent_output_indices`, and `construct_fcmp_tx` now use `shekyl_scan_output_recover` for HKDF-based output ownership detection, mask recovery, and key image computation. - **`genRctFcmpPlusPlus` uses HKDF commitment masks.** The function now accepts pre-computed HKDF `z` scalars (`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 while `scan_output` expected HKDF-derived values. The old `amount_keys` parameter 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's `create_stake_claim_tx` now constructs outputs via the unified Rust HKDF path, producing correct output keys, view tags, KEM ciphertexts, leaf hashes, and `enc_amounts` with `amount_tag`. BP+ blinding factors remain constrained by the `zeroCommit` pseudo-out balance equation (sum to N). - **Chaingen PQC signing via `shekyl_sign_pqc_auth`.** Core test `construct_fcmp_tx` now uses the high-level FFI that derives, signs, and wipes the ML-DSA secret key entirely inside Rust. The raw `shekyl_pqc_sign` call (which accepted the secret key as a C++ byte pointer) is replaced. - **`zeroCommit` dead code removed from DB layer.** `blockchain_db.cpp` and `db_lmdb.cpp` no longer fall back to `zeroCommit(amount)` for output commitments. All outputs (including coinbase) use on-chain `outPk[i].mask`. The `pre_rct_outkey` branch in LMDB now throws for `amount != 0` (Shekyl has no pre-RCT outputs). - **RCTTypeNull round-trip serialization test.** New test in `tests/unit_tests/serialization.cpp` verifies that `RCTTypeNull` transactions with populated `outPk` and `enc_amounts` (8-byte amount + 1-byte `amount_tag`) survive binary serialize/deserialize round-trip. - **libFuzzer harness for `construct_output`.** New fuzz target `fuzz_construct_output` in `rust/shekyl-crypto-pq/fuzz/` exercises `construct_output` + `scan_output` round-trip with arbitrary spend keys, amounts, corrupted `enc_amount`, and wrong `amount_tag`. - **libFuzzer harness for malformed KEM keys.** New fuzz target `fuzz_construct_output_malformed_kem` feeds arbitrary bytes as X25519 and ML-KEM-768 encapsulation keys to `construct_output`. Exercises wrong-length, oversized, and garbage KEM public key inputs to ensure the function returns `Err`, never panics. - **PQC leaf hash known-answer test.** New JSON fixture `docs/test_vectors/PQC_LEAF_HASH_KAT.json` (8 vectors) pins the output of `derive_pqc_leaf_hash(combined_ss, output_index)`. Rust KAT test validates byte-for-byte against the fixture. - **Coinbase `check_commitment_mask_valid` hardened.** For `RCTTypeNull` (coinbase) outputs, the consensus check now rejects commitments that equal `zeroCommit(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)` and `else if (miner_tx)` branches that fell back to `derivation_to_y_scalar` are removed. Shekyl is v3 from genesis; all matched outputs must succeed the HKDF scan path. A hard `wallet_internal_error` is thrown if `v3_hkdf_scanned` is false, preventing silent domain fallback that would produce unspendable outputs. - **Legacy coinbase construction path removed.** `construct_miner_tx` now 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 Keccak `derive_public_key` / `derive_view_tag` which would produce an invalid (unscannable, missing `outPk`/`enc_amounts`) coinbase. All Shekyl addresses carry PQC keys from genesis. - **Genesis coinbase builder uses `shekyl_construct_output`.** `build_genesis_coinbase_from_destinations` now constructs outputs via the Rust HKDF path, producing correct HKDF-derived output keys, view tags, commitments, encrypted amounts with `amount_tag`, KEM ciphertexts, and PQC leaf hashes. The legacy Keccak derivation path is removed. - **Legacy `additional_tx_public_keys` dead code removed.** The `need_additional_txkeys` logic, `additional_tx_public_keys` vector, and pre-v3 output derivation loop in `construct_tx_with_tx_key` are deleted. V3 replaces per-output additional tx keys with KEM ciphertext (tag 0x06). ### 🔄 Changed - **`transfer_details::m_mask` type changed.** `rct::key` → `crypto::secret_key` for automatic zeroization on drop. All RCT call sites use explicit `rct::sk2rct()` / `rct::rct2sk()` conversion. Binary-compatible (same 32-byte layout). - **`ecdhInfo` replaced by `enc_amounts`.** The per-output encrypted amount format changes from `ecdhTuple` (64 bytes: 32 mask + 32 amount) to `std::array` (8 bytes XOR-encrypted amount + 1 byte amount tag). Affects `rctSigBase`, all serialization paths (binary, boost, JSON), and transaction construction (`genRctFcmpPlusPlus`, `fill_construct_tx_rct_stub`, wallet claim construction). - **`ecdhEncode` removed.** The ECDH encoding function is deleted from `rctOps`, `device.hpp`, and `device_default`. Transaction construction now writes `enc_amounts` directly via Rust HKDF-based output construction. `ecdhDecode` is retained as a scanner shim until the wallet migrates to Rust `scan_output`. `ecdhHash` and `genCommitmentMask` have been fully removed from `rctOps`, the device interface chain, and tests. - **FROST SAL deferred to V4.** Per-output HKDF-derived `y` is incompatible with DKG group-shared `y`. FROST SAL section in `docs/PQC_MULTISIG.md` marked as deferred with V4 resolution path (Carrot-style address scheme). ### 🐛 Fixed - **`sc_check()` signed left-shift undefined behavior.** `signum(...) << k` on `int64_t` in `crypto-ops.c` is UB when the result is negative. Introduced `signed_lshift()` helper that uses multiplication on non-GCC compilers. Ported from monero@c5be4dd. - **`wallet2::verify_password()` logic inversion.** Background wallet detection used `HasParseError() && 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 empty `cnonce` with `qop=auth`, weakening the digest exchange against replay attacks. Now generates a random 16-byte cnonce via `RAND_bytes` and includes it in the response hash and Authorization header. Ported from monero@3d6b9fb. - **Critical: SAL `y` / commitment mask `z` conflation in FCMP++ prover.** `wallet2.cpp` passed `td.m_mask` (Pedersen commitment mask) as `spend_key_y` to the FCMP++ prover, but SAL requires `y` such that `O = xG + yT`. Since legacy outputs had `y = 0` and `z != 0`, `OpenedInputTuple::open` always failed. Fixed by migrating to two-component output keys (`O = xG + yT`) where `y = Hs_y(derivation || i)`, and passing `z` as a separate `commitment_mask` field. 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_sources` in `chaingen.cpp` set `ts.mask = rct::zero()` for coinbase, but `zeroCommit(amount) = G + amount*H` has mask = scalar 1. Fixed to `rct::identity()`. - **Critical: u64 saturation in `total_weighted_stake` (Bug 7).** The in-memory cache and LMDB `staker_accrual_record` used `uint64_t` for 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 gains `total_weighted_stake_hi` field (32→40 bytes), FFI `shekyl_calc_per_block_staker_reward` accepts lo/hi parameters, and Rust `AccrualRecord`/`StakeRegistry::total_weighted_stake()` return u128. - **Critical: back-dating exploit on first claim (Bug 3).** `check_stake_claim_input` only enforced `from_height == watermark` when watermark > 0. For the first claim (no watermark), `from_height` was unconstrained. An attacker could stake at block N, then submit a claim with `from_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 requiring `from_height >= creation_height` when no watermark exists. - **Critical: inter-tx pool sufficiency race within a block (Bug 4).** The per-tx pool balance check in `check_tx_inputs` reads the pre-block pool balance, so five claim txs each claiming 1000 against a pool of 3000 all individually pass. The silent-skip path in `add_transaction_data` then lets over-claimed txs through without decrementing the pool. Fixed with two changes: a block-level aggregate pool check in `handle_block_to_main_chain` that sums all claim amounts across ALL txs and rejects the block if the total exceeds the pool, plus converting the silent-skip path in `add_transaction_data` to a hard throw (dead code if validation is correct, fatal if not). - **Reorg watermark restoration loses data (Bug 5).** `remove_transaction` used `from_height == 0` as the signal for "first claim, remove watermark." But `from_height` for 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_blockchain` unconditionally subtracted accrued inflow from `pool_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's `total_weighted_stake`: if zero, subtract from `total_burned` instead of `pool_balance`. - **Empty-staker-set accrual audit trail.** The `actually_destroyed` field in the persisted accrual record did not reflect the no-staker burn because the record was written before the burn decision. Fixed by moving `add_staker_accrual` to after the no-staker burn path, so the record captures the full `actually_destroyed` value. - **Dandelion++ relay timestamp rollback.** `get_relayable_transactions` in `tx_pool.cpp` was missing `lock.commit()`, causing all stem/forward timestamp updates to be silently rolled back by the `LockedTXN` destructor. 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_blockchain` relied 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 in `db_wtxn_guard`. - **Critical: weighted denominator bug in staker reward accrual.** The per-block `total_weighted_stake` was 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 in `blockchain.h`/`blockchain.cpp`. - **Claim timing: lock conflated with claimability.** `check_stake_claim_input` incorrectly rejected claims when `lock_until > current_height`, making rewards unclaimable during the lock period. Fixed by removing the lock-based rejection and adding `to_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_balance` indefinitely. Fixed to burn these amounts when `total_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_height` to `transfer_details` (C++ wallet) and `TransferDetails` (Rust scanner) with serialization. FFI layer now calls `stage_claim_watermarks()` after broadcasting claim transactions. - **Critical: stake tx only mineable in exact creation block (Bug 13).** `handle_block_to_main_chain` validated staked outputs with strict equality `staked.lock_until == blockchain_height + lock_blocks`. Since the wallet signed `lock_until = current_height + lock_blocks`, any mempool latency made every honest stake tx permanently unminable. Fixed by removing `lock_until` from the on-chain `txout_to_staked_key` struct entirely. The effective lock expiry is now computed dynamically as `creation_height + tier_lock_blocks` at 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_until` but 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-chain `lock_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 accepted `to_height <= lock_until`. A staker could claim a one-block reward at `lock_until` against a denominator that didn't include their weight. Fixed by changing the accrual scan to `effective_lock_until < eval_height` (inclusive upper bound) and scheduling unlock subtraction at `effective_lock_until + 1`. `lock_blocks = N` now means exactly N blocks of accrual. - **Medium: unstake forfeits unclaimed rewards (Bug 8).** `create_unstake_transaction` jumped straight to `create_transactions_from` without 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 has `m_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` (now `stage_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 in `m_pending_claim_watermarks` at broadcast, committed by `confirm_claim_watermarks` when the tx appears in a confirmed block during scan, and expired by `expire_pending_claim_watermarks` after 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-chacha` crate) using the NCC-audited RustCrypto `chacha20` crate. `CHACHA_IV_SIZE` increased 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-separated `y` component along generator `T`, satisfying the FCMP++ SAL proof's `OpenedInputTuple::open` constraint. Previously, outputs were single-component (`O = xG + 0·T`) and the wallet incorrectly passed the Pedersen commitment mask `z` as the SAL `y`, causing proof generation to fail. The y-scalar uses the `"shekyl_y"` domain separator in `crypto.cpp`. The commitment mask `z` is now passed separately in the 256-byte witness header at offset 192. `transfer_details` stores `m_y` (boost serial v14). Two regression tests in `proof.rs` verify 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_extra` payloads (hybrid KEM ciphertexts ~1120 B per output, PQC leaf hashes, pubkey/nonce). Construction of v3 spends failed once PQC fields were appended; the pool and `construct_tx` checks now allow the larger bound. - **`construct_tx` RCT/PQC stubs.** v3 spends require `|pqc_auths| == |vin|` for binary serialization, and `RCTTypeFcmpPlusPlusPqc` needs BP+, ECDH, and pseudo-out vectors sized to inputs/outputs. `construct_tx` now assigns stub `pqc_authentication` entries and calls `rct::fill_construct_tx_rct_stub()` (dummy Bulletproofs+, ECDH encoding, Pedersen pseudo-outs) so `get_transaction_hash` and JSON/blob round-trips succeed before the wallet replaces the RCT payload with `genRctFcmpPlusPlus()`. ### 🗑️ Removed - **`shekyl_fcmp_derive_pqc_keypair` FFI 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 by `shekyl_derive_pqc_leaf_hash` (returns only h_pqc) and `shekyl_derive_pqc_public_key` (returns only the public key). - **`derive_pqc_keypair`, `derive_hybrid_pqc_keypair`, `DerivedPqcKeypair`, `DOMAIN_PQC_OUTPUT` from `shekyl-crypto-pq`.** These legacy derivation functions used the old salt A (`shekyl-pqc-derive-v1`) and returned secret key material. All callers now use `derive_output_secrets` (salt B) + `keygen_from_seed` or the higher-level `sign_pqc_auth_for_output`. - **`derived_pqc_secret_keys`, `derived_pqc_public_keys`, `claim_signing_sks` vectors in `wallet2.cpp`.** These C++ vectors held PQC secret keys in wallet memory. All 4 call sites migrated to `shekyl_derive_pqc_leaf_hash` + `shekyl_sign_pqc_auth`, which derive and zeroize internally in Rust. - **`pqc_secret_keys` from `native_sign_state` (`wallet2.h`).** The deferred native-signing path no longer stores PQC secret keys. The Rust tx-builder receives `combined_ss` + `output_index` and derives keys internally. - **`SpendInput::pqc_secret_key` from `shekyl-tx-builder`.** Replaced with `combined_ss: Vec` (64 bytes) and `output_index: u64`. The Rust `sign_pqc_auths` function now calls `sign_pqc_auth_for_output` internally. - **4 legacy Monero fixture tests in `serialization.cpp`.** Removed `portability_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_short` through `cache_matches_true_value`). Monero-specific weight baselines do not apply to Shekyl economics. - **`chacha.c` (C ChaCha implementation).** Replaced by the Rust `shekyl-chacha` crate via FFI. The C implementation had a strict aliasing violation in its `U8TO32_LITTLE`/`U32TO8_LITTLE` macros (pointer cast to `uint32_t*`). - **ChaCha8 dead code.** All `crypto::chacha8()` call sites in `wallet2.cpp` were 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) or `sign_pqc_auth_for_output` (Rust tx-builder), which derive the keypair from `combined_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.mdc` rule across Rust and C++ codebases: - `shekyl_buffer_free` now uses `zeroize` crate instead of `std::ptr::write_bytes`, preventing the compiler from optimizing away the secret-wiping write. - `native_sign_state::clear()` in `wallet2.h` now `memwipe`s 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, and `wallet2_ffi_create()` to prevent core dumps containing key material on Linux. - Passwords, seeds, spend keys, and view keys in `wallet2_ffi.cpp` JSON-RPC dispatch now use `memwipe` scope guards to wipe temporary `std::string` buffers after use. - New `shekyl_madvise_dontdump` FFI function (`MADV_DONTDUMP` on Linux, no-op elsewhere) declared in `shekyl_secure_mem.h`. - PQC long-lived secret keys (`m_pqc_secret_key`) are now `mlock`ed and `madvise(MADV_DONTDUMP)`ed after generation and decryption, and `memwipe`d + `munlock`ed on `forget_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. Fixed `create_from_keys` and `set_null` to 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)]` from `HybridSecretKey`, `HybridKemSecretKey`, and `SharedSecret`. All now implement manual `Debug` printing `[REDACTED]` to prevent log leakage. - **Proof generation panic removal (Tier 1).** Replaced 12 `ScalarDecomposition::new(...).unwrap()` calls in `proof.rs` with `?`-propagated `ProveError::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_MULTISIG` blocks have been removed from `shekyl_ffi.h`, `wallet2.h/cpp`, and `wallet2_ffi.cpp` — FROST multisig is now consumed exclusively through the Rust wallet crates. - **CString unwrap removal (Tier 2).** Replaced all `CString::new().unwrap()` in `shekyl-wallet-rpc` with `to_cstring()` helper returning `WalletError`. Fixed `Mutex::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 in `Zeroizing<[u8; N]>` for automatic cleanup. - **hex_to_key temp buffer wiped (Tier 2).** Added `memwipe` scope guard to `hex_to_key` in `wallet2_ffi.cpp`. - **PQC verify debug gated (Tier 2).** `shekyl_pqc_verify_debug` now only compiled with `debug_assertions` or `debug-verify` feature to prevent use as a signature oracle in production. - **Free-string wipe (Tier 2).** `wallet2_ffi_free_string` now wipes the buffer before freeing, protecting against secret-bearing JSON residue. - **Buffer free contract documented (Tier 2).** `shekyl_buffer_free` len safety contract documented in both Rust doc-comment and C header. - **Claim builder silent wrong index (Tier 2).** `position(...).unwrap_or(0)` replaced with explicit `TransferNotFound` error in `claim_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 = true` added to 11 Shekyl-first crates for consistent Clippy enforcement. - **Legacy naming cleanup (Tier 4).** Renamed `MONERO_DEFAULT_LOG_CATEGORY` to `SHEKYL_DEFAULT_LOG_CATEGORY` across 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 `.env` to `.gitignore`, created explicit CodeQL workflow targeting both `dev` and `main` branches, added `permissions: contents: read` to `build.yml`. - **Base58 overflow and non-canonical encoding fix (monero-oxide fork).** `shekyl-base58::decode()` now uses `checked_add` to 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 = true` in both the monero-oxide fork `Cargo.toml` and the Shekyl `rust/Cargo.toml`. Dev and release profiles additionally set `panic = "abort"`. - **HKDF domain-separated salts for PQC key derivation.** All HKDF-SHA-512 calls in `shekyl-crypto-pq` now use explicit fixed salts (`shekyl-pqc-derive-v1`, `shekyl-master-derive-v1`) instead of `None`. Strengthens domain separation and prevents cross-protocol seed reuse if the same combined shared secret appears in other contexts. - **`FrostSalSession` secret deduplication.** Removed the redundant `x` (spend secret scalar) from `FrostSalSession` struct fields. Previously the secret was stored both in the struct and inside `SalAlgorithm`, 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_message` now checks `LEVIN_PACKET_COMPRESSED` in the input header before compressing. Prevents double-compression of already-compressed messages in future refactors. - **Divisor degree underflow assertions.** `Divisor::div` now asserts that `self.a.degree >= rhs.degree` and `self.b.degree >= rhs.degree` before `usize` subtraction, converting silent wraparound into a clear panic with diagnostic context. - **Interpolator allocation bounds fix.** `Interpolator::interpolate` now allocates the output coefficient vector using the domain size (`self.lagrange_polys.len()`) instead of `evals.len()`, preventing trailing zeros from inflating the vector when callers provide excess evaluations. - **`member_of_list` witness construction hardened.** Replaced `next_eval.unwrap()` with `carry_eval.zip(next_eval)` in the FCMP++ circuit gadget, eliminating a potential panic if evaluation invariants change. ### ✨ Added - **`shekyl-tx-builder` crate.** 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 in `zeroize::Zeroizing` and wiped on drop. - **`shekyl_sign_transaction` FFI export.** New C ABI function in `shekyl-ffi` wrapping `shekyl-tx-builder::sign_transaction()`. Accepts JSON-serialized inputs/outputs, returns a `ShekylSignResult` with either JSON proofs or a structured error code and message. Declared in `shekyl_ffi.h`. - **Wallet RPC `native-sign` feature.** `shekyl-wallet-rpc` gains an optional `native-sign` Cargo feature that enables `transfer_native()` — a pure-Rust transfer path using `shekyl-tx-builder` directly, eliminating C++ proof FFI round-trips. The split pipeline uses `wallet2_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_transfer` implemented.** Full C++ implementation of the split transfer pipeline. `prepare_transfer` activates native-sign mode in `transfer_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 Rust `SpendInput`/`OutputInfo`/`TreeContext` types. `finalize_transfer` receives the Rust-generated `SignedProofs` JSON, manually reconstructs the BP+ struct from the Rust blob (handling the V-field format difference), inserts all proofs into `tx.rct_signatures`, performs PQC signing using stored secret keys, and commits/broadcasts the transaction. Fee estimation uses `shekyl_fcmp_proof_len()` to pad the stub FCMP++ proof to the correct estimated size. - **Native-sign mode in `wallet2::transfer_selected_rct`.** New `m_native_sign_mode` flag and `native_sign_state` struct on `wallet2`. When enabled, `transfer_selected_rct` skips `genRctFcmpPlusPlus` and 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-builder` types.** All `[u8; 32]`, `Vec`, and `Vec<[u8; 32]>` fields on `SpendInput`, `OutputInfo`, `TreeContext`, `SignedProofs`, `LeafEntry`, and `PqcAuth` now 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.mdc` codifying project-wide conventions for cryptographic secret zeroization in both Rust (`Zeroizing`, `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/`: added `shekyl-primitives` (Keccak-256, Pedersen commitments), `shekyl-bulletproofs` (BP+ range proofs), the root `shekyl-oxide` crate (transaction/block types, FCMP module), `shekyl-rpc` (daemon RPC trait, `ScannableBlock`), and `shekyl-simple-request-rpc` (HTTP transport). Resolved the `shekyl-address` naming 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-scanner` crate.** 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) - `TransferDetails` struct with FCMP++ path precompute, combined PQC shared secret, and spend tracking fields - `WalletState` for in-memory transfer management with key image dedup, spend detection, and reorg handling - `BalanceSummary` with staking-aware breakdown (total, unlocked, timelocked, staked matured/locked, frozen) - **Split RPC routing (`rust-scanner` feature).** `shekyl-wallet-rpc` now supports a `rust-scanner` feature 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 via `shekyl-scanner`, while all mutation methods continue through the C++ FFI. Added `ScannerState`, `dispatch_with_scanner()`, and typed scanner handlers. - **GUI wallet scanner integration.** Updated `wallet_bridge.rs` in `shekyl-gui-wallet` to include a `ScannerState` alongside the FFI `Wallet2` handle. Added `get_scanner_balance()`, `get_scanner_staked_outputs()`, and `get_scanner_height()` bridge methods for future scanner-backed queries. - **`shekyl-encoding` crate.** 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-address` crate.** New standalone Rust crate (`rust/shekyl-address/`) for network-aware segmented Bech32m address encoding. Defines `Network` enum (Mainnet, Testnet, Stagenet) with HRP lookup tables for classical (`shekyl`, `tshekyl`, `sshekyl`) and PQC (`skpq`/`skpq2`, `tskpq`/`tskpq2`, `sskpq`/`sskpq2`) segments. `ShekylAddress` supports `encode()`, `decode()`, and `decode_for_network()`. - **Generic Bech32m blob FFI.** `shekyl_encode_blob()` and `shekyl_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()` and `shekyl_address_decode()` now accept/return a `network` parameter (0=mainnet, 1=testnet, 2=stagenet) for HRP-based network discrimination. - **Shekyl-first development rule.** Added `.cursor/rules/shekyl-first-development.mdc` codifying 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_sal` module in `shekyl-fcmp` wraps upstream `SalAlgorithm` for threshold Spend-Auth-and-Linkability proofs. `FrostSalSession` manages 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++. The `multisig` feature flag enables FROST dependencies (`modular-frost`, `transcript`, `rand_chacha`). - **FROST DKG key management.** New `frost_dkg` module in `shekyl-fcmp` provides `SerializedThresholdKeys` for `ThresholdKeys` 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_prove` FFI now accepts a single `witness_ptr`/`witness_len` blob containing per-input fixed headers, leaf chunk Ed25519 output data, and Helios/Selene branch layers. `genRctFcmpPlusPlus` in `rctSigs.cpp` serializes the full witness. - **Daemon RPC `chunk_outputs_blob`.** `get_curve_tree_path` response 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_MULTISIG` blocks have been removed from `wallet2.h/cpp`, `wallet2_ffi.cpp`, and `shekyl_ffi.h`. - **`FrostSigningCoordinator` for multi-input nonce aggregation.** New coordinator in `shekyl-fcmp/src/frost_sal.rs` manages per-input preprocess collection, nonce sum computation, share collection, and final aggregation into `SpendAuthAndLinkability` pairs for `prove_with_sal()`. - **Full FROST DKG ceremony via `MultisigDkgSession`.** New wallet-level wrapper in `shekyl-wallet-core/src/multisig/dkg.rs` drives the `dkg-pedpop` `KeyGenMachine` state 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). - **`MultisigSigningSession` for wallet-level FROST orchestration.** New session in `shekyl-wallet-core/src/multisig/signing.rs` wraps per-input `FrostSalSession` instances and a `FrostSigningCoordinator`, providing hex-encoded preprocess/share exchange for transport-agnostic signing. - **`MultisigGroup` with PQC keypair management.** New type in `shekyl-wallet-core/src/multisig/group.rs` stores 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.rs` for 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). - **`SalLegacyAlgorithm` and `legacy_multisig` removed from shekyl-oxide.** Deleted the legacy Monero multisig SAL algorithm and test module from the vendored `shekyl-oxide/fcmp/fcmp++` crate. Only the modern `SalAlgorithm` (used by `FrostSalSession`) is retained. - **16+ new Rust tests for FROST.** 4 `frost_sal` unit tests (session creation, pseudo-out distinctness, identity rejection, field roundtrip), 6 `FrostSigningCoordinator` tests (wrong preprocess count, shares before nonces, duplicate shares, nonce sums timing, point addition, bytes roundtrip), 2 `FrostSalSession` negative tests, 4 `frost_dkg` unit tests (serialization roundtrip, group key extraction, parameter validation, byte-level roundtrip), 8 FFI lifecycle tests (null safety, invalid data rejection, session handle management), 5 `shekyl-wallet-core` multisig 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()` in `rust/shekyl-fcmp/src/proof.rs` exercises 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_code` warning for `InconsistentWitness` variant in `generalized-bulletproofs` (only constructed under `debug_assertions`) with `#[cfg_attr(not(debug_assertions), allow(dead_code))]`. Fixed deprecated `GenericArray::as_slice()` in `helioselene` ciphersuite by replacing with `as_ref()`. - **Stake-claim vs `verRctSemanticsSimple` conflict.** Stake-claim transactions use `RCTTypeFcmpPlusPlusPqc` but have no FCMP++ membership proof (they prove ownership via PQC auth on public amounts). `ver_non_input_consensus` now excludes stake-claim-only transactions from the RCT semantics batch that rejects empty `fcmp_pp_proof`. - **`genRctFcmpPlusPlus` hard-fail on proof failure.** Previously logged and returned an `rctSig` with an empty proof when `shekyl_fcmp_prove` failed; now throws `CHECK_AND_ASSERT_THROW_MES` so 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_key` and `hash_pqc_public_key` previously truncated Blake2b-512 to 32 bytes and cleared bit 255, which could produce non-canonical values exceeding the Selene base field modulus. Now uses `HelioseleneField::wide_reduce` on the full 64-byte hash for unbiased, canonical field elements. - **Deterministic PQC keygen stability.** Replaced `rand::rngs::StdRng` with `rand_chacha::ChaCha20Rng` for ML-DSA-65 keypair derivation. `StdRng`'s underlying algorithm is not a stability guarantee across `rand` versions, which could break wallet-restore-from-seed. - **Bech32m variant enforcement.** `decode_blob` now strictly enforces the Bech32m checksum variant instead of accepting both Bech32 and Bech32m. Removed unused `EncodingError::EmptyData` variant. ### 🔒 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 `FrostSalSession` secret deduplication (see Changed), the secret lives solely inside the `SalAlgorithm` and is zeroized through its `Drop` impl. - **RELEASE-BLOCKER resolved in circuit gadgets.** The `incomplete_add_pub` function in the FCMP++ circuit already receives parameters typed as `OnCurve`, which guarantees the on-curve constraint. Replaced the `RELEASE-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, so `tx_prune_next_block` cannot advance on partial pruning. - **FCMP++ compile-path compatibility fixes.** Updated wallet/core-test FCMP++ construction callsites for the current `genRctFcmpPlusPlus` leaf-chunk API, and added explicit cached-chunk to `rct::fcmp_chunk_entry` conversion in wallet construction to keep GCC 14 builds green. - **CI portability and fuzz gate hardening.** Replaced GNU-only `xargs -r` usage 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_deserialize` and `fuzz_tx_deserialize_fcmp_type7` now pass the required `signable_tx_hash` 7th argument to `verify()`. `fuzz_block_header_tree_root` rewritten for the current `ProveInput` struct and 4-arg `prove()` signature. - **`prune_tx_data` miner output lookup.** When storing output-pruning metadata, RCT coinbase outputs are keyed under amount `0` in LMDB (same as `add_transaction`); pruning now uses that amount for `get_output_key` instead of the plaintext `vout.amount`, avoiding `OUTPUT_DNE` during 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_picker` class and `GAMMA_SHAPE`/`GAMMA_SCALE` constants, `transfer_selected` (non-RCT overload), `wallet2::get_outs` decoy-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_size` preference, the entire `ringdb.h`/`ringdb.cpp` subsystem (LMDB ring database), ring commands in simplewallet, spend proof RPC endpoints and FFI dispatch, `boroSig` struct from `rctTypes.h`, unreachable `hf_version < HF_VERSION_FCMP_PLUS_PLUS_PQC` branch in `cryptonote_tx_utils.cpp`, `blockchain_blackball` utility, and `output_selection.cpp` unit test. Removed LMDB link dependency from wallet CMake target. - **Decoy and ring_size removal from Rust RPC.** Removed `ring_size: u32` parameter from `shekyl-wallet-rpc` transfer API (`types.rs`, `wallet.rs`, `ffi.rs`), from the C++ FFI boundary (`wallet2_ffi.h`/`.cpp`), and from the C++ wallet RPC `estimate_tx_size_and_weight` command definition. Deleted `Decoys` struct, `MAX_RING_SIZE` constant, `DecoyRpc` trait and blanket implementation, `OutputInformation` struct, `rpc_point` helper, and `test_decoy_rpc` test from `shekyl-oxide`. Removed `/get_output_distribution.bin` route from `shekyl-daemon-rpc`. - **Bulletproof v1 ("Original") deletion.** Deleted the entire `original/` module tree and its tests from `shekyl-bulletproofs`. Removed `Bulletproof::Original` enum variant, v1 `prove()`/`read()` functions, v1 match arms in `verify`/`batch_verify`/`write_core`, and the standalone `BulletproofsBatchVerifier` struct. Cleaned up dead `inner_product` and `mul_vec` methods that were only used by v1 code. - **Light wallet support removed.** Deleted all `m_light_wallet` state, `set_light_wallet`, `light_wallet_login`, `light_wallet_get_outs`, `import_outputs`, `get_unspent_outs`, `submit_raw_tx`, and all `if (m_light_wallet)` branches from `wallet2.cpp`/`.h`. Deleted `wallet_light_rpc.h` entirely. Removed light wallet API from `wallet2_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_hash` to `get_tx_prehash`, `mlsag_prehash`/`mlsag_prepare`/`mlsag_hash`/`mlsag_sign` to `tx_prehash`/`tx_prepare`/`tx_hash`/`tx_sign` across the device interface hierarchy (`device.hpp`, `device_default.hpp`/`.cpp`, `device_ledger.hpp`/`.cpp`), `rctSigs.cpp`/`.h`, and `protocol.cpp`. Renamed Ledger `INS_MLSAG` constant to `INS_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. Removed `CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX`, `CRYPTONOTE_PUBLIC_INTEGRATED_ADDRESS_BASE58_PREFIX`, and `CRYPTONOTE_PUBLIC_SUBADDRESS_BASE58_PREFIX` constants from all network namespaces and `config_t`. No code path accepts or produces Base58 strings. - **Legacy address structs removed.** `integrated_address`, `legacy_account_public_address`, and `legacy_integrated_address` structs removed from `cryptonote_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_verify` doc comment (referenced non-existent `shekyl_pqc_verify_multisig_with_group_id`, now points to `shekyl_pqc_multisig_group_id`). Renamed Windows `SystemInfo.dw_page_size` to `page_size` (drop Hungarian notation). Renamed `shekyl-wallet-rpc-rs` binary to `shekyl-wallet-rpc` (drop `-rs` suffix per Rust API Guidelines). - **Address encoding migrated to Bech32m.** `get_account_address_as_str()` and `get_account_address_from_str()` now call Rust FFI (`shekyl_address_encode`, `shekyl_address_decode`) for network-aware Bech32m encoding. The `subaddress` parameter is retained for API compatibility but ignored. `address_parse_info` fields `is_subaddress` and `has_payment_id` are 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_blob` FFI. Version headers (`SpendProofV1`, `InProofV2`, etc.) removed; the HRP now serves as the type discriminator. - **`shekyl-crypto-pq` re-exports `shekyl-address`.** The `address` module in `shekyl-crypto-pq` is now a re-export of the standalone `shekyl-address` crate. The old `shekyl-crypto-pq/src/address.rs` has been deleted. - **Tx-data prune watermark.** `prune_tx_data` now stores `tx_prune_next_block` (exclusive next height) instead of ambiguous `last_pruned_tx_data_height` values; legacy keys migrate on read/write. LMDB unit tests live in `tests/unit_tests/tx_data_pruning_lmdb.cpp` (minimal block builder only; does not link `tests/core_tests/chaingen.cpp` into `unit_tests`, avoiding duplicate object code and macOS linker unwind/diagnostic issues in CI). - **FCMP++ Rust dependency source moved in-repo.** `shekyl-fcmp` now consumes vendored `shekyl-oxide` crates via path dependencies under `rust/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-blockchain` transaction-data pruning.** LMDB v6 adds `txs_pqc_auths` (split from `txs_pruned` at `pqc_auths_offset`), implements `prune_tx_data` (batch 256 blocks, output metadata, watermark, TOCTOU height check), default depth `CRYPTONOTE_TX_PRUNE_DEPTH` (5000), `pop_block` guard when verification data is gone, continuous pruning via `update_blockchain_pruning`, RPC `get_transactions.pruned` and `get_info.tx_prune_height`. - **Staking FFI and config-driven tier parameters.** `shekyl-staking` now generates tier lock durations, yield multipliers, and max stake-claim range from `config/economics_params.json` at build time (aligned with `shekyl-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 in `blockchain.cpp`, `core_rpc_server.cpp`, and `simplewallet` instead of duplicating tier strings or inline `mul128`/`div128_64` reward math. - **FCMP++ transaction construction helper (`construct_fcmp_tx`).** New chaingen helper in `tests/core_tests/chaingen.cpp` that builds fully valid FCMP++ transactions during core test replay: tree path assembly from the live LMDB curve tree, `genRctFcmpPlusPlus` proof generation, KEM decapsulation for per-input PQC keypair derivation, and PQC auth signing. This unblocks 30+ disabled core tests that relied on the old `construct_tx_rct` stub. - **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 replay - `gen_fcmp_tx_double_spend`: second FCMP++ spend of the same output rejected - `gen_fcmp_tx_reference_block_too_old`: stale referenceBlock rejected - `gen_fcmp_tx_reference_block_too_recent`: too-recent referenceBlock rejected - `gen_fcmp_tx_timestamp_unlock_rejected`: timestamp-based `unlock_time` rejected - **Verification caching unit tests.** Six new GTest cases in `tests/unit_tests/fcmp.cpp` validating `compute_fcmp_verification_hash` determinism, 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.cpp` with 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.cpp` with randomized stress test (100 random leaves, multi-height draining), add/remove round-trip, drain journal CRUD, and leaf removal correctness. - **`fuzz_tx_deserialize_fcmp_type7` Rust fuzz target.** New cargo-fuzz target in `rust/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 covering `txin_stake_claim` and `txout_to_staked_key` serialization round-trips, reward integer math (including `mul128`/`div128_64` vs `double` divergence 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_leaves` table 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)). The `pending_staked_*` identifiers were renamed to `pending_tree_*` across all database interfaces. The drain journal (`pending_tree_drain`) now stores full 136-byte entries (maturity_height + leaf_data) for exact `pop_block` reversal instead of just a drain count. `pop_block` restores 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_time` rejected (Decision 13).** Transactions with `unlock_time >= CRYPTONOTE_MAX_BLOCK_HEIGHT_SENTINEL` (500M) are now rejected in `check_tx_outputs`. Only height-based lock times are accepted. - **`prune_tx_data` status clarification.** The output-metadata pruning loop in `db_lmdb.cpp` is a plumbing-only stub (`TODO(phase6f)`). The `store_output_metadata`, `get_output_metadata`, and `is_output_pruned` interfaces are live, but the block-iteration pruning loop does not execute. ### 🗑️ Removed - **Vestigial hard fork constants.** Removed `HF_VERSION_CLSAG` and `HF_VERSION_MIN_V2_COINBASE_TX` from `cryptonote_config.h`. All test references replaced with literal `1`. - **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`: Disabled `gen_simple_chain_001`, `gen_simple_chain_split_1`, `gen_chain_switch_1`, `gen_ring_signature_1`, `gen_ring_signature_2`, all `txpool_*` tests, all `gen_double_spend_*` tests, `gen_block_reward`, all `gen_bpp_*` Bulletproofs+ tests, and several `gen_tx_*` tests whose setup required valid user transactions. These tests construct transactions via `MAKE_TX`/`construct_tx_rct` which produce `RCTTypeFcmpPlusPlusPqc` stubs with empty `pqc_auths`, rejected by `check_tx_inputs` even in FAKECHAIN mode. - `tests/unit_tests/bulletproofs.cpp`: All three weight tests (`weight_equal`, `weight_more`, `weight_pruned`) prefixed with `DISABLED_` and hex blobs removed. Shekyl's `rctSigBase` serialization rejects any type other than `RCTTypeFcmpPlusPlusPqc` (type 7), so old `RCTTypeBulletproofPlus` (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.toml` and 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.lock` advanced from pin `92af05e` to `416d8d1` which includes the complete `monero-oxide/` → `shekyl-oxide/` directory and package rename. - **`shekyl-fcmp` crate cleanup.** Removed unused `sha2` and `shekyl-crypto-pq` dependencies from `rust/shekyl-fcmp/Cargo.toml`. Renamed the misleading `ProveError::InputCountMismatch` variant to `ProveError::PqcHashMismatch` with a clear `input_index` field indicating which input has a mismatched leaf `h_pqc` vs `pqc_auth` commitment. ### 🐛 Fixed - **Private member access in pending tree unit tests.** Fixed 18 compile errors in `pending_tree_fuzz.cpp` and 4 in `deferred_insertion.cpp` on macOS CI where calls to `add_pending_tree_leaf`, `drain_pending_tree_leaves`, `add_pending_tree_drain_entry`, `get_pending_tree_drain_entries`, `remove_pending_tree_drain_entries`, and `remove_pending_tree_leaf` were calling private overrides on `BlockchainLMDB`. Changed all test methods to use `BlockchainDB&` 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 missing `fill_tx_sources` declaration to `chaingen.h` and moved `Blockchain::check_stake_claim_input` from the private section to the public API so core tests can call it without `IN_UNIT_TESTS`. - `tests/unit_tests/fcmp.cpp`: Fixed serialization calls to use `do_serialize(ar, v)` instead of non-existent `v.serialize(ar)` member; replaced `binary_archive(istringstream&)` with the correct `binary_archive(span)` constructor; fixed `shekyl_pqc_verify` call to include the required `scheme_id` first argument and corrected parameter order. - `tests/unit_tests/staking.cpp`: Same `binary_archive` constructor fix — replaced `istringstream` with `epee::span` in all four serialization round-trip tests. - macOS CI: Added `zstd` to Homebrew dependencies and fixed CMake to use `PkgConfig::ZSTD` imported target instead of bare library name, resolving `ld: library 'zstd' not found` on 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_reward` RPC handler used `double`-precision arithmetic for reward estimation, which diverges from the consensus `mul128`/`div128_64` path when `total_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_rct` and multisig proof prep now read tree depth from RPC metadata (`tree_depth`) instead of `path_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_inputs` now routes pure `txin_stake_claim` transactions through the claim-specific input checks before generic FCMP++ `txin_to_key` validation, preventing incorrect rejection of valid stake-claim transactions that use `RCTTypeFcmpPlusPlusPqc`. - **Stake-claim reward math overflow defense.** Added a defensive `q_hi != 0` check after `div128_64` in 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_auths` public key and signature generation. - **Curve-tree path RPC returns spendable reference block.** `get_curve_tree_path` now returns a `reference_block` at least `FCMP_REFERENCE_BLOCK_MIN_AGE + 1` behind 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 both `H(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_transaction` now fully reverses `txin_stake_claim` state 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 handle `txin_stake_claim` inputs alongside `txin_to_key`. Previously they used `CHECKED_GET_SPECIFIC_VARIANT(..., txin_to_key, ...)` which caused immediate false-return on any stake-claim input, breaking mempool bookkeeping for claim transactions. - **`remove_transaction_keyimages` no 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_amount` and `check_inputs_overflow` now handle both `txin_to_key` and `txin_stake_claim` input variants instead of failing on the latter. These are called unconditionally for all transactions (via `check_money_overflow`), so the old hard-cast to `txin_to_key` would reject any transaction containing a stake claim. ### 🔒 Security - **FFI buffer zeroization before free.** `shekyl_buffer_free` now 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 generates `HybridX25519MlKem` KEM keypairs via `shekyl_kem_keypair_generate()` instead of `HybridEd25519MlDsa` signing 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_rct` now builds transactions using full-chain membership proofs instead of ring signatures: - Inputs contain only the real output (no decoy selection). - `genRctFcmpPlusPlus` generates 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_key` adds KEM encapsulation (tag 0x06) and `H(pqc_pk)` leaf hashes (tag 0x07) for each output, and skips wallet-level PQC signing. - **KEM decapsulation during wallet scanning.** `process_new_transaction` now extracts hybrid KEM ciphertexts from `tx_extra` tag 0x06, calls `shekyl_kem_decapsulate` with the wallet's KEM secret keys, and stores the resulting 64-byte combined shared secret in `transfer_details::m_combined_shared_secret`. This enables per-output PQC key derivation at spend time. - **FCMP++ fee estimation.** `estimate_rct_tx_size` now 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`, and `clsag_sign` virtual methods from `device.hpp` and all implementations (`device_default.cpp`, `device_ledger.cpp`). Shekyl never supported CLSAG; the device interface now only exposes FCMP++ methods. - **`get_outs` / `get_outs.bin` RPC 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_TIME` from `cryptonote_config.h`. These were defined but never referenced in production code. `HF_VERSION_CLSAG` and `HF_VERSION_MIN_V2_COINBASE_TX` are 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_COMPRESSED` flag (0x10) in the Levin header marks compressed frames. Peers negotiate compression via `P2P_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 to `get_outs.bin` and `get_curve_tree_root`; corrected endpoint counts and cutover test steps. ### 🐛 Fixed - **`rct::key` missing `operator!=`.** Added `operator!=` to the `key` struct in `rctTypes.h`. The operator was present for cross-type comparisons (`rct::key` vs `crypto::public_key`) but not for `rct::key` vs `rct::key`, causing compilation failures on all platforms when comparing pseudo-outs to expected zero-commitments in the stake claim verification path. - **MSVC `binary_archive` constructor mismatch.** Fixed `wallet2.cpp` to use `epee::strspan` instead of `std::istringstream` for constructing `binary_archive`, which MSVC could not resolve. - **Memory leak on exception in PQC auth signing.** Added RAII scope guard for `ShekylPqcKeypair` buffers in `transfer_selected_rct` Phase C, ensuring Rust-allocated key material is freed even if `THROW_WALLET_EXCEPTION_IF` throws mid-loop. - **Secret key material not wiped on KEM decapsulation failure.** The stack buffer in `process_new_transaction` KEM decapsulation is now wiped unconditionally (success or failure), preventing partial key material from lingering on the stack. - **Shadowed `tx_extra_fields` variable in KEM decapsulation.** Removed redundant inner `tx_extra_fields` reference that shadowed the outer one in `process_new_transaction`, using the already-resolved outer reference instead. ### 🔄 Changed - **Decoy selection functions are dead code.** `get_outs`, `tx_add_fake_output`, and `light_wallet_get_outs` in `wallet2.cpp` are 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 `RCTTypeFcmpPlusPlusPqc` with Bulletproofs+ range proofs instead of `RCTTypeNull` with 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_extra` under tag `0x06` and `H(pqc_pk)` leaf hashes under new tag `0x07`. - 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. - **Consensus rejects `RCTTypeNull` for non-coinbase v3 transactions.** `check_tx_inputs` now enforces that only coinbase (`txin_gen`) may use `RCTTypeNull`. All other v3 transactions (including stake claims) must use `RCTTypeFcmpPlusPlusPqc` with 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 to `txin_stake_claim` inputs). ### ✨ Added - **`TX_EXTRA_TAG_PQC_LEAF_HASHES` (`0x07`).** New `tx_extra` field (`tx_extra_pqc_leaf_hashes`) stores per-output `H(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)` from `tx_extra`.** The `collect_outputs` / `make_leaf` path in `blockchain_db.cpp` now extracts `H(pqc_pk)` values from the `0x07` tag, 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_tx` now derives per-output PQC keypairs via KEM shared secrets and includes their `H(pqc_pk)` values in the `0x07` `tx_extra` field alongside the existing KEM ciphertexts in `0x06`. ### 🔒 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`) in `check_stake_claim_input` to 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 in `check_tx_inputs` that 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_claim` now verifies that the `H(pqc_pk)` stored in the curve tree leaf (bytes 96–128) matches `shekyl_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_transaction` in `blockchain_db.cpp` now handles `txin_stake_claim` key images in addition to `txin_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_inputs` now covers both `txin_to_key` and `txin_stake_claim` key images, ensuring consistent ordering rules across all input types. - **Third-party headers treated as SYSTEM includes.** `external/`, `external/rapidjson`, `external/easylogging++`, and `external/supercop` are now `-isystem` in CMake, suppressing `-Wsuggest-override` and other warnings from third-party code while keeping strict warnings for first-party code. ### 🗑️ Removed - **Dead `check_ring_signature` function.** Removed unused ring signature verification from `blockchain.cpp` and its declaration from `blockchain.h`. Shekyl uses FCMP++ from genesis; ring signatures are never validated. - **Dead `expand_transaction_2` function.** Removed the no-op transaction expansion function from `blockchain.cpp` and its declaration from `blockchain.h`. FCMP++ does not use mixRing expansion. - **Dropped `serde_json` dev-dependency from `shekyl-fcmp`.** Replaced the JSON round-trip test with a byte-level serialization check, reducing the dev-dep surface. ### 📚 Documentation - Synced `docs/FCMP_PLUS_PLUS.md` curve-tree text with consensus: outputs are indexed at creation; maturity is enforced via `referenceBlock` and other rules, not by delaying leaf insertion. - Clarified `docs/POST_QUANTUM_CRYPTOGRAPHY.md` to use `pqc_auths` (per-input) terminology consistently. - Documented mempool FCMP verification-cache id: `compute_fcmp_verification_hash` binds proof + `referenceBlock` + key images (comment in `blockchain.cpp`). - Noted the monero-oxide commit pin in `rust/shekyl-fcmp/Cargo.toml` comments (lockfile remains authoritative). - Updated `docs/STAKER_REWARD_DISBURSEMENT.md` with 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_inputs` skips the expensive `shekyl_fcmp_verify` FFI call (~35ms/input) while still running all structural checks (referenceBlock, depth, key images, PQC auth). - **`construct_leaf` now accepts PQC key hash parameter.** The Rust FFI function `shekyl_construct_curve_tree_leaf` takes a 4th `h_pqc_ptr` argument (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 by `lock_until_height` with 128-byte leaf values) and `pending_staked_drain` (block_height → drain count) tables to the blockchain database layer. Five new methods on `BlockchainDB`: `add_pending_staked_leaf`, `drain_pending_staked_leaves`, `set_pending_staked_drain_count`, `get_pending_staked_drain_count`, and `remove_pending_staked_drain_count`. This enables staked outputs whose `effective_lock_until > block_height` to 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-fuzz` targets across `rust/shekyl-fcmp/fuzz/` (proof deserialization, curve tree leaf hashing, block header tree root mismatch) and `rust/shekyl-crypto-pq/fuzz/` (Bech32m address decoding, KEM decapsulation with corrupted ciphertexts). Extended Rust unit tests in `proof.rs`, `tree.rs`, `leaf.rs`, `kem.rs`, `address.rs`, and `derivation.rs` covering prove/verify round-trips, hash grow/trim inverse properties, boundary values, and cross-crate consistency. Extended C++ unit tests in `tests/unit_tests/fcmp.cpp` with 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. Includes `config.yaml` with load profiles, `load_generator.py` for synthetic transaction submission, and `monitor.py` for real-time metric collection, consensus checking, and daily report generation. - **Security audit scope document (Phase 9).** Added `docs/AUDIT_SCOPE.md` defining the scope for a third-party security review of the 4-scalar leaf circuit modification. Covers soundness, zero-knowledge, and completeness verification for the `H(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.md` with "Stressnet stable for 4 consecutive weeks" and "4-scalar leaf circuit audit completed" as hard prerequisites for mainnet launch. ### 🔄 Changed - **Renamed `src/ringct/` to `src/fcmp/` for naming consistency.** Shekyl does not use ring signatures; the directory now reflects the actual FCMP++ confidential transaction system. CMake targets renamed from `ringct`/`ringct_basic` to `fcmp`/`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. The `rct::` namespace is preserved for now as a separate future rename. - **Unified coinbase transaction version to v3.** `construct_miner_tx` and `build_genesis_coinbase_from_destinations` now emit `tx.version = 3`, matching regular FCMP++ transactions. All `miner_tx && tx.version == 2` checks have been widened to `>= 2` across `blockchain_db`, `blockchain`, `wallet2`, and test infrastructure. The `pqc_auths` serialization 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.cpp` still referenced the undefined `fake_outs_count` variable and called `estimate_fee` with the old 12-argument signature. Replaced `fake_outs_count` with `0` (FCMP++ has no decoys) and updated `estimateTransactionFee` to use the simplified 8-argument `estimate_fee` signature with hardcoded `use_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`, and `rct::genRctSimple` from: `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 drop `RangeProofType`/`bp_version` parameters. ### 🗑️ Removed - **Dead verification cache code (`verRctNonSemanticsSimple`, `ver_rct_non_semantics_simple_cached`).** Removed the stub `verRctNonSemanticsSimple` from `rctSigs.cpp/.h` (returned `true` unconditionally), the `ver_rct_non_semantics_simple_cached` wrapper and its `ver_rct_non_sem` helper from `tx_verification_utils.cpp/.h`, the unused `rct_ver_cache_t` type alias and `m_rct_ver_cache` member from `Blockchain`, and the dead `RCT_CACHE_TYPE` constant from `check_tx_inputs`. Real FCMP++ verification lives in `check_tx_inputs` (blockchain.cpp) and the mempool uses `compute_fcmp_verification_hash` for caching. ### 🔒 Security - **CRITICAL: PQC signed payload now binds to prunable FCMP++ data (Phase 4c).** `get_transaction_signed_payload` now includes `H(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 in `check_tx_inputs` rejected all `RCTTypeNull` transactions, which includes pure stake-claim txs. The gate now allows `RCTTypeNull` transactions through when all inputs are `txin_stake_claim`. Additionally, the `RCTTypeNull` switch case now calls `check_stake_claim_input` for each claim input and checks key image double-spend — previously it `break`ed without any validation. - **HIGH: Bound all inputs' H(pqc_pk) hashes into PQC signed payload.** `get_transaction_signed_payload` now appends `H(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_input` now verifies the staked output's leaf is present in the curve tree by checking `staked_output_index < get_curve_tree_leaf_count()` and reading the leaf with `get_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_version` and `flags` consensus enforcement.** `verify_transaction_pqc_auth` now rejects `auth_version != 1` and `flags != 0`, enforcing spec steps 6a/6c. Previously these fields were serialized and signed over but never validated. - **LOW: Single-signer `hybrid_public_key` size enforcement.** `verify_transaction_pqc_auth` now verifies single-signer key blobs are exactly `HYBRID_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_authentication` blobs.** `hybrid_public_key` and `hybrid_signature` vectors are now rejected during deserialization if they exceed `PQC_MAX_PUBLIC_KEY_BLOB` or `PQC_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 *after* `remove_block()`, using the post-pop height instead of the removed block's height. This caused a mismatch with `add_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 `pseudoOuts` serialization mismatch in generic `rctSigBase`.** The generic `BEGIN_SERIALIZE_OBJECT()` path in `rctSigBase` unconditionally included `pseudoOuts`, even for `RCTTypeFcmpPlusPlusPqc` where pseudo-outs live in the prunable section. Now gated with `if (type != RCTTypeFcmpPlusPlusPqc)` to match the custom serializer. - **MEDIUM: `get_curve_tree_path` RPC now fails on missing layer hashes.** Previously, a failed `get_curve_tree_layer_hash()` silently inserted zeros into the proof path, potentially generating invalid proofs from inconsistent DB state. Now returns `CORE_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 with `existing_child = 0` instead 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 through `updated_chunk_t` and passes the previous cycle-scalar to `hash_grow`. - **CRITICAL: Replaced O(N) `trim_curve_tree` with incremental `hash_trim`.** Reorgs previously read all remaining leaves, cleared the tree, and rebuilt from scratch — a liveness risk at scale. The new implementation uses `hash_trim_selene`/`hash_trim_helios` FFI 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 least `CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW` (60) blocks behind the tip. Added `static_assert`s in `cryptonote_config.h` to 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_depth` validation 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. The `else` branch 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) = 0` in the curve tree — a distinguishable pattern. - **RPC: Replaced hardcoded chunk widths with FFI calls.** `get_curve_tree_path` now calls `shekyl_curve_tree_selene_chunk_width()` and `shekyl_curve_tree_helios_chunk_width()` instead of using static constants. - **RPC: Added `reference_height` and `leaf_count` to `get_curve_tree_path` response.** 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** to `get_curve_tree_path` to prevent abuse from unbounded requests. ### ✨ Added - **RPC: `get_curve_tree_info` endpoint** returns root hash, depth, leaf count, and chain height for the current curve tree state. - **RPC: `get_curve_tree_checkpoint` endpoint** retrieves a stored checkpoint (root, depth, leaf_count) at a given block height, needed for fast-sync. ### 📚 Documentation - Documented `verRctNonSemanticsSimple` stub 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 `tx.version = 2` rationale~~ — superseded: coinbase is now version 3, unified with regular transactions. - Documented LMDB post-delete cursor contract (`MDB_GET_CURRENT` after `mdb_cursor_del` returns the next item) in pruning and GC loops. - Added `ct_layer_chunk_key` bit-layout comment explaining the 8-bit layer / 56-bit chunk index encoding for LMDB integer keys. - Documented `construct_leaf` zero 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_root` empty-tree return semantics (returns `hash_init`, callers should check `leaf_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`: Removed `estimate_fee` mixin/bulletproof/clsag params, `adjust_mixin()`, `default_mixin()` getter/setter, `m_default_mixin` member, `rct_config` from `pending_tx` and `transfer_selected_rct`. - `wallet2.cpp`: Removed mixin from `estimate_rct_tx_size`, `estimate_tx_size`, `estimate_tx_weight`, `estimate_fee` signatures and all call sites. Removed `adjust_mixin()` definition, JSON serialization of `default_mixin`, constructor initialization. Removed `const bool clsag/bulletproof/bulletproof_plus = true` patterns. - `wallet_errors.h`: Removed `mixin_count` field from `not_enough_outs_to_mix` error struct. - `wallet2_ffi.cpp`: Replaced `adjust_mixin` calls with constant `0`. - `wallet_rpc_server.cpp`: Replaced `adjust_mixin` calls with constant `0`. - `wallet2_api.h`, `wallet.h`, `wallet.cpp`: Removed `mixin_count` parameter from `createTransaction` and `createTransactionMultDest`. - `unsigned_transaction.cpp`: Simplified `mixin()` and `minMixinCount()` to always return 0 (FCMP++ has no explicit mixin). - `simplewallet.cpp`: Removed ring-size parsing, `adjust_mixin` calls, and `default_mixin` display. 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`, and `low_mixin` from device drivers, Trezor protocol, RPC handlers, blockchain verification, transaction utilities, wallet, and serialization: - `device_ledger.cpp`: Removed `INS_CLSAG` define, legacy type branches in `mlsag_prehash`, replaced `clsag_prepare`/`clsag_hash`/`clsag_sign` with FCMP++ TODO stubs. - `protocol.cpp`/`protocol.hpp` (Trezor): Removed `rct::Bulletproof` variant, `is_simple()`/`is_req_bulletproof()`/`is_bulletproof()`/ `is_clsag()` helpers, `mixRing` resize, CLSAG deserialization in `step_final_ack`. Added `is_fcmp_pp()` helper. - `core_rpc_server.cpp`/`core_rpc_server_commands_defs.h`: Removed `low_mixin` field and its assignment from send_raw_tx response. - `daemon_handler.cpp`: Removed `m_low_mixin` error branch. - `verification_context.h`: Removed `m_low_mixin` from `tx_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`: Removed `rct::RCTConfig` parameter from `construct_tx_with_tx_key` and `construct_tx_and_get_tx_key`. Replaced `genRctSimple` call with FCMP++ proof generation stub. Removed `mixRing` construction. - `cryptonote_format_utils.cpp`: Removed `is_rct_bulletproof`/ `is_rct_clsag` calls, simplified BP+ weight calculations. - `cryptonote_boost_serialization.h`: Removed serialization functions for `rct::rangeSig`, `rct::Bulletproof`, `rct::mgSig`, `rct::clsag`, `rct::RCTConfig`, `rct::boroSig`. Simplified `rctSigBase` and `rctSigPrunable` serialization to only handle FCMP++. - `tx_verification_utils.h`/`.cpp`: Removed `mix_ring` parameter from `ver_rct_non_semantics_simple_cached`. Removed `expand_tx_and_ver_rct_non_sem`, `calc_tx_mixring_hash`, and `is_canonical_bulletproof_layout`. - `json_object.h`/`.cpp`: Removed JSON serialization for `rct::rangeSig`, `rct::Bulletproof`, `rct::boroSig`, `rct::mgSig`, `rct::clsag`. Removed legacy prunable fields from `rctSig` JSON output. - `wallet2.h`: Removed `rct_config` field from `tx_construction_data` serialization and the version-gated `RangeProofPaddedBulletproof` defaults in Boost serialization. - `wallet2.cpp`: Fixed `construct_tx_and_get_tx_key` call site that still passed `{}` where the removed `rct_config` parameter was. - `bulletproofs.h`/`.cc`: Gutted non-plus Bulletproof PROVE/VERIFY functions — the `rct::Bulletproof` struct was already removed from `rctTypes.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), and `RCTTypeBulletproofPlus` (6) from the enum. Only `RCTTypeNull` (0) and `RCTTypeFcmpPlusPlusPqc` (7) remain. - Deleted structs: `mgSig`, `clsag`, `rangeSig`, `Bulletproof` (non-plus), `RangeProofType` enum, and `RCTConfig`. - Removed `mixRing` member from `rctSigBase` and `mixin` parameter from `serialize_rctsig_prunable`. - Removed from `rctSigPrunable`: `rangeSigs`, `bulletproofs` (non-plus), `MGs`, `CLSAGs` vectors 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`, and `HASH_KEY_TXHASH_AND_MIXRING` from `cryptonote_config.h`. - Removed VARIANT_TAG entries for `mgSig`, `rangeSig`, `Bulletproof`, and `clsag`. - Simplified `get_pre_mlsag_hash` to only handle `RCTTypeFcmpPlusPlusPqc`. - Simplified `verRctSemanticsSimple` and `verRctNonSemanticsSimple` to only accept FCMP++ transactions (no CLSAG/ring verification path). ### 🔄 Changed - **FCMP++ Phase 3: Per-input PQC authorization vector.** Replaced `std::optional pqc_auth` with `std::vector pqc_auths` on `cryptonote::transaction` (one `pqc_authentication` per input). Updated binary, Boost, and JSON serialization, transaction hash (`cn_fast_hash` of serialized `pqc_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-fcmp` crate wrapping upstream `monero-fcmp-plus-plus` with 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 = 7` with `referenceBlock`, `curve_trees_tree_depth`, and `fcmp_pp_proof` fields. `curve_tree_root` commitment 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_hash` in `txpool_tx_meta_t`). Staked output curve-tree leaves. - **Curve tree database (Phase 2):** Full `get_curve_tree_path` RPC implementation assembling real Merkle paths (leaf scalars + per-layer sibling hashes with position encoding). Selective pruning of intermediate tree layers between checkpoints, wired into `add_block` after `save_curve_tree_checkpoint`. Old checkpoint garbage collection. - **Wallet integration (Phase 5):** `genRctFcmpPlusPlus()` proof construction. `get_curve_tree_path` RPC. 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_t` and `m_output_metadata` LMDB 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.md` for the full specification. - **FCMP++ Phase 3: KEM ciphertext `tx_extra` and coinbase self-encapsulation.** - `tx_extra_pqc_kem_ciphertext` with tag `TX_EXTRA_TAG_PQC_KEM_CIPHERTEXT` (`0x06`): payload `blob` is 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_tx` performs 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_path` struct to `wallet2.h` caching per-output tree path, root hash at precompute time, and precompute height. - Added `m_fcmp_precomputed_paths` runtime cache (not serialized) and `m_fcmp_last_precompute_height` watermark to `wallet2`. - `precompute_fcmp_paths()` fetches tree paths for all unspent outputs via the `get_curve_tree_path` daemon 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. - **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)` calls `shekyl_fcmp_derive_pqc_keypair` via 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 callback `on_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_block` previously counted all `tx.vout` entries when computing how many leaves to trim, but `add_block` skips 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_layers` was 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's `leaf_count` and 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_metadata` now uses `mdb_put` directly with `m_write_txn` instead of the `CURSOR()` macro which required `m_cursors` to be in scope. - `get_output_metadata` and `prune_tx_data` now use `m_txn` (from `TXN_PREFIX_RDONLY`) instead of `txn_ptr` (from `TXN_PREFIX`). - Removed unused `m_txc_output_metadata` cursor field and `m_cur_output_metadata` macro from `db_lmdb.h`. - **Wallet FCMP++ path precomputation: fixed undefined behavior.** - Replaced `reinterpret_cast` on `std::vector` with a proper intermediate `std::string` copy in both `precompute_fcmp_paths` and `update_fcmp_paths_incremental`. - **FCMP++ Phase 6c: CI pipeline updates.** - Added x86_64 architecture verification step to the `rust-audit-and-test` CI job in `.github/workflows/build.yml`. - Added explicit `cargo build --locked -p shekyl-fcmp` step to verify the FCMP++ crate builds as part of the Rust workspace. - Added dedicated Bech32m address encoding test step that runs `shekyl-crypto-pq` address tests with visible CI output. - The monero-oxide git dependency is cached via `~/.cargo/git` in the existing Cargo cache key (`rust-${{ hashFiles('rust/Cargo.lock') }}`). - Determinism check (build twice, diff `libshekyl_ffi.a` hashes) and `cargo audit` remain in place. - **FCMP++ Phase 6f: Transaction pruning mode (skeleton).** - Added `output_pruning_metadata_t` packed struct to `blockchain_db.h` storing 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_metadata` LMDB table (keyed by `global_output_index`) in `db_lmdb.h` and `db_lmdb.cpp` with cursor, rflag, and DBI member. - LMDB implementation: `store_output_metadata` and `get_output_metadata` are fully wired; `is_output_pruned` delegates to `get_output_metadata`; `prune_tx_data` validates depth against `CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE` and reads/writes a `last_pruned_tx_data_height` watermark in the properties table to skip already-processed blocks on subsequent runs. The block-iteration pruning loop is documented as a TODO skeleton. - `--prune-blockchain` CLI flag now also triggers `prune_tx_data()` in `cryptonote_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. - **FCMP++ Phase 4b: Mempool verification caching.** - Added `fcmp_verification_hash` (32-byte `crypto::hash`) and `fcmp_verified` (1-bit flag) to `txpool_tx_meta_t` in `src/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 from `hash(proof || referenceBlock || key_images)`. - `tx_memory_pool::add_tx` stores the cache hash on successful FCMP++ verification. - `tx_memory_pool::is_transaction_ready_to_go` checks the cached hash via `is_fcmp_verification_cached()` and seeds `m_input_cache` to skip re-running `shekyl_fcmp_verify()` for previously-verified mempool transactions. - Added `static_assert` guards at the `memcmp` site on `txpool_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. - **FCMP++ Phase 4e: Staking consensus rules for FCMP++.** - `collect_outputs` in `blockchain_db.cpp::add_block` now handles `txout_to_staked_key` outputs 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 the `pending_staked_leaves` DB table and inserted into the curve tree when they mature (see deferred staked leaf insertion entry below). - `check_stake_claim_input` validates claims against the staked output's `effective_lock_until` (`creation_height + tier_lock_blocks`) and enforces `to_height <= min(current_height, effective_lock_until)`. - **FCMP++ Phase 5: Wallet transaction construction skeleton.** - Added `rct::genRctFcmpPlusPlus()` in `src/fcmp/rctSigs.cpp` — builds an FCMP++ `rctSig` with `RCTTypeFcmpPlusPlusPqc`, Bulletproofs+ range proofs, balanced pseudo-outputs, and invokes `shekyl_fcmp_prove()` via FFI to generate the membership proof. - Declared the new function in `src/fcmp/rctSigs.h`. - Added `COMMAND_RPC_GET_CURVE_TREE_PATH` RPC command in `src/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_path` JSON-RPC endpoint in `src/rpc/core_rpc_server.h` and `src/rpc/core_rpc_server.cpp`. - Added TODO scaffolding in `src/wallet/wallet2.cpp` at 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. - **FCMP++ Phase 6a: Hardware device stubs.** - Added `fcmp_prepare`, `fcmp_proof_start`, and `fcmp_proof_add_input` virtual methods to `hw::device` (base class) with default `return false` implementations for unsupported devices. - Software device (`device_default`) returns `true` (scaffolding for Rust FFI delegation). - Ledger device (`device_ledger`) logs an informative error and returns `false`, guiding users to software wallets until Ledger firmware gains FCMP++ support. - Trezor inherits the base-class defaults (unsupported) without code changes. - Updated `RELEASE_CHECKLIST.md` to document hardware wallet readiness status. - **FCMP++ Phase 4a: Verification in `check_tx_inputs`.** - Added `RCTTypeFcmpPlusPlusPqc` verification path in `Blockchain::check_tx_inputs` (`src/cryptonote_core/blockchain.cpp`). - `referenceBlock` age validation: confirmed within `[tip - MAX_AGE, tip - MIN_AGE]` using DB block lookup. - `curve_trees_tree_depth` validated 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_auths` verification left as documented TODO pending the per-input auth field migration. - **FCMP++ Phase 4a-pre: PQC auth binding specification.** - New `docs/FCMP_PLUS_PLUS.md` formally documents the dual-layer binding model, per-input signed payload layout, and 7-step consensus verification order for `RCTTypeFcmpPlusPlusPqc` transactions. - **FCMP++ Phase 3.5: Curve tree root in block header (consensus-critical).** - Added `curve_tree_root` (`crypto::hash`) field to `block_header` in `src/cryptonote_basic/cryptonote_basic.h`, initialized to `null_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`) verifies `curve_tree_root` matches the locally-computed tree root after `add_block` grows the tree; rejects the block on mismatch. - RPC `block_header_response` now includes `curve_tree_root` hex string. - Test generator (`chaingen.cpp`) sets `curve_tree_root` to `null_hash` in `construct_block` and `construct_block_manually`. - **FCMP++ Phase 3: Transaction format for FCMP++ PQC.** - Added `RCTTypeFcmpPlusPlusPqc = 7` to the RCT type enum in `src/fcmp/rctTypes.h` — Shekyl's only non-coinbase transaction type. - Added `referenceBlock` (block hash anchoring the curve tree snapshot) to `rctSigBase`, serialized only for the new type. - Added `curve_trees_tree_depth` and `fcmp_pp_proof` (opaque FCMP++ proof blob) to `rctSigPrunable`, replacing CLSAG ring signatures for the new type. - Added `TX_EXTRA_TAG_PQC_KEM_CIPHERTEXT` (0x06) to `tx_extra.h` for per-output ML-KEM-768 ciphertexts. - Added `key_image_y_normalize()` to `crypto.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 to `rctTypes.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. - **FCMP++ Phase 2e: Curve tree checkpoint strategy.** - New `BlockchainDB` virtual 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_checkpoints` table (MDB_INTEGERKEY), storing root[32] + depth[1] + leaf_count[8] per checkpoint. - Automatic checkpoint every `FCMP_CURVE_TREE_CHECKPOINT_INTERVAL` (10 000) blocks during `add_block`, enabling fast-sync resumption. - Configurable interval via `cryptonote_config.h` constant. - **FCMP++ Phase 2f: Curve tree pruning strategy.** - `prune_curve_tree_intermediate_layers` removes 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 upstream `monero-fcmp-plus-plus` (from `Shekyl-Foundation/monero-oxide` fork, `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) in `shekyl-crypto-pq/src/kem.rs` with HKDF-SHA-512 shared-secret combination and master-seed key derivation. - Implemented Bech32m segmented address encoding (`shekyl1/skpq1/skpq21`) in `shekyl-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-ffi` for 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.cmake` with `--locked` flag for reproducible builds. - **FCMP++ Phase 1a.1: Security review of forked monero-oxide crates.** - `cargo audit`: 226 crate dependencies scanned, zero vulnerabilities found. - `unsafe` block audit: zero `unsafe` in first-party monero-oxide workspace code (helioselene, ec-divisors, generalized-bulletproofs, fcmps, monero-oxide). Only 4 `unsafe` blocks exist in helioselene benchmarks (`_rdtsc()` for cycle counting, not in library code). `dalek-ff-group` (crates.io dependency) also has zero `unsafe` blocks. - 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 `92af05e0` is 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.lock` pins all git dependencies to exact commit hash `92af05e0`. - Double-build determinism verified: `libshekyl_ffi.a` hash identical across consecutive builds on x86_64. - Added CI job `rust-audit-and-test` to `.github/workflows/build.yml` with 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 = 100` counter) with 240 s deadline + 0.25 s interval polling, matching the pattern already used in `test_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=bti` on macOS, `standard` elsewhere). Adapted from upstream Monero #9858. - **Linker dead-code stripping.** Added `-ffunction-sections -fdata-sections` to 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.md` with 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_reward` core test with Shekyl economics.** Rewrote `check_block_rewards()` in `block_reward.cpp` to verify miner outputs against Shekyl's four-component economics formula (release multiplier + emission split + fee burn) instead of legacy Monero fixed expectations. Updated `construct_miner_tx_by_weight` to pass explicit economics parameters. Fixed `construct_block` and `construct_block_manually` in `chaingen.cpp` to pass `circulating_supply=already_generated_coins` to `construct_miner_tx`, preventing parameter mismatch between test generator and validator. 80 core_tests now pass (was 79). - **MSVC C4334: 23 `1 << n` sites widened to `1ULL << n` in consensus code.** Fixed potential undefined behavior (signed 32-bit overflow if shift amount ever reaches 32) in `cryptonote_format_utils.cpp` (3), `bulletproofs.cc` (6), `bulletproofs_plus.cc` (6), `rctTypes.cpp` (5), `rctSigs.cpp` (2), and `multiexp.cc` (2). - **MSVC C4333 right-shift warning in UTF-8 helpers.** Changed `wint_t cp` to `uint32_t cp` in `src/common/util.cpp` `get_string_prefix_by_width()`, and added an explicit `static_cast` on the transform result in `src/common/utf8.h` `utf8canonical()`. On MSVC, `wint_t` is 16-bit `unsigned short`, so `cp >> 18` shifted by more than the type's width. - **Remaining HF17 references corrected to HF1.** Fixed stale Monero-era `HF17` / `HF_VERSION_SHEKYL_NG = 17` references in `POST_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), and `STAKER_REWARD_DISBURSEMENT.md`. Also corrected `HF18` references to `HF2` in multisig V4 rollout tables. The source code constant `HF_VERSION_SHEKYL_NG` was already correctly defined as `1` in `cryptonote_config.h`; only documentation was affected. - **CMake Boost detection on CMake 3.30+**: The built-in `FindBoost.cmake` module was removed in CMake 3.30. Restructured Boost detection to try CONFIG mode first (finding `BoostConfig.cmake` installed by b2), falling back to MODULE on older CMake. Fixes `contrib/depends` builds 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. Removed `multisig_txset` fields from transfer and sweep response structs. Removed the `CHECK_MULTISIG_ENABLED` macro and `multisig/multisig.h` dependency. Classical secret-splitting multisig is replaced by PQC-only authorization (`scheme_id = 2`); see `docs/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 all `mms` subcommands. Removed `--generate-from-multisig-keys` and `--restore-multisig-wallet` CLI flags. Removed `enable-multisig-experimental` wallet setting. Removed `wallet/message_store.h` dependency. The `transfer_main`/`called_by_mms` indirection was collapsed into a single `transfer` method. - **Classical multisig test and device_trezor remnants.** Removed stale multisig references from test infrastructure: `m_multisig*` wallet resets in `wallet_tools.cpp`, `multisig_sigs.clear()` in Trezor tests, `multisig_txset` assertion in `cold_signing.py`, and deleted `tests/functional_tests/multisig.py`. Removed `multisig` from the functional test default list. Cleaned up device_trezor protocol: removed `translate_klrki`, `MoneroMultisigKLRki` alias, `m_multisig` member, and multisig cout decryption in `Signer::step_final_ack`. Removed `mms_error`, `no_connection_to_bitmessage`, and `bitmessage_api_error` error classes from `wallet_errors.h`. - **Classical multisig wallet API layer.** Removed all classical multisig code from the public wallet API: `MultisigState` struct, 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_MAGIC` constants, `m_multisig`/`m_multisig_threshold`/`m_multisig_rounds_passed`/ `m_enable_multisig`/`m_message_store`/`m_mms_file` member initializations, `num_priv_multisig_keys_post_setup`, `get_multisig_seed`, multisig restore path in `generate()`, `make_multisig`, `exchange_multisig_keys`, `get_multisig_first_kex_msg`, `multisig()`, `has_multisig_partial_key_images`, `frozen(multisig_tx_set)`, all `save/parse/load/sign_multisig_tx` overloads, the multisig transaction builder path in `transfer_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 scattered `m_multisig` guard branches. - **Classical multisig `m_key_image_partial` remnants.** Removed the `m_key_image_partial` bitfield from `exported_transfer_details` and all code references in `wallet2.cpp` and `simplewallet.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 only `m_key_image_known`. Removed the dead `old_mms_file` cleanup block from `wallet2::store_to`. ### ✨ Added - **Daemon RPC migrated to Rust/Axum (Phase 1).** The daemon HTTP RPC transport is now served by the `shekyl-daemon-rpc` Rust crate using Axum, replacing `epee::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_server` handler logic is unchanged and accessed via a `core_rpc_ffi` C ABI facade. Enabled by default; `--no-rust-rpc` falls 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 at `tests/rpc_comparison/compare_rpc.sh`; test data in `shekyl-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_verify` to accept `scheme_id` and dispatch between single-signer (1) and multisig (2) paths. Added `shekyl_pqc_verify_debug` for diagnostic error codes and `shekyl_pqc_multisig_group_id` for group identity computation. - **Scheme downgrade protection.** New `tx_extra_pqc_ownership` tag (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_5` test to `shekyl-crypto-pq` verifying 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.json` with canonical encoding sizes, wire-format sizes, verification pipeline checks, the 10-check pipeline, size regression data, and adversarial test cases for `scheme_id = 2`. - **MSVC wallet-core build path**: `BuildRust.cmake` now selects the `x86_64-pc-windows-msvc` Rust 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 `gitian` workflow is now the sole release pipeline, replacing the separate `release-tagged` workflow. Gitian builds produce reproducible binaries; a new `package-and-publish` job creates `.deb`/`.rpm` packages, a Windows NSIS installer, source archive, and `SHA256SUMS`, 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.gz` containing the full source tree with all submodules, attached to each release alongside the binaries. ### 🔄 Changed - **`shekyl_pqc_verify` FFI signature change.** Now requires `scheme_id` as first parameter for scheme dispatch. - **`depends.yml` demoted 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.yml` disabled.** The Gitian pipeline now handles all release artifacts. The old workflow is preserved as `.disabled` for 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`, and `dockrun.sh` now 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, glibc `math-finite.h` hack). Adds `linux-libc-dev:i386` for 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), unused `addressof` template specialization (crypto test), unused `max_block_height` variable (protocol_handler). - Fixed `oaes_lib.c`: replaced deprecated `ftime()` with `gettimeofday()`, corrected transposed `calloc` argument order (5 call sites). - Fixed `rx-slow-hash.c`: added `(void)` to K&R-style function definitions. - Suppressed GCC false positive `-Wstringop-overflow` in `tree-hash.c`. - Replaced deprecated `strand::wrap()` with `boost::asio::bind_executor()` in `levin_notify.cpp`. - Suppressed GCC `-Wuninitialized` for safe circular-reference constructors in `cryptonote_core.cpp` and `long_term_block_weight.cpp`. - Added default member initializers to `BulletproofPlus` (rctTypes.h), `transfer_details` and `payment_details` (wallet2.h) to silence `-Wmaybe-uninitialized`. - Fixed Windows: removed unused variables in `windows_service.cpp`, eliminated `-Wcast-function-type` in `util.cpp` via `void*` intermediate cast, fixed `-Wtype-limits` in `utf8.h` by using `uint32_t` instead of `wint_t` for code points. - Suppressed intentional uninitialized read in `memwipe.cpp` test. - Set `MACOSX_DEPLOYMENT_TARGET` for native Darwin Cargo builds in `BuildRust.cmake` to eliminate 672 linker warnings from `ring` crate. - **CI link errors: separated `shekyl-daemon-rpc` from `shekyl-ffi`.** The daemon RPC Axum crate was bundled into `libshekyl_ffi.a`, causing `undefined 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 new `ffi_exports.rs` within the daemon-rpc crate, which now produces its own `libshekyl_daemon_rpc.a` staticlib. Only the daemon target links both libraries. `BuildRust.cmake` updated with a second cargo build step and `SHEKYL_DAEMON_RPC_LINK_LIBS`. - **Wallet: `--daemon-port` help text referenced Monero port 18081.** Updated to Shekyl's default RPC port 11029. - **Wallet: `account_public_address` equality after PQC.** Destination and change-address checks used `memcmp` on the whole struct; `m_pqc_public_key` is a `std::vector`, so equality was wrong when keys matched but allocations differed. All such sites now use `operator==` / `!=`. Added a `static_assert` that the type is not trivially copyable to discourage raw `memcmp` regressions. - **Wallet / Ledger: constant-time comparison for 32-byte secrets.** `wallet2::is_deterministic` and Ledger HMAC secret lookup now use `crypto_verify_32` instead of `memcmp`. - **MSVC: add `` and POSIX guards in `util.cpp`.** Added `` for `_open_osfhandle`/`_close`, expanded MinGW conditionals to cover MSVC for `setenv`→`putenv`, `mode_t`/`umask`, and `closefrom`→no-op. - **MSVC: replace `__thread` with `thread_local` in `perf_timer.cpp` and `threadpool.cpp`.** GCC's `__thread` is not supported by MSVC. - **MSVC: rename `xor` parameter in `slow-hash.c` to `xor_pad`.** MSVC treats `xor` as a reserved keyword in C mode. Both the x86/SSE and ARM/NEON variants of `aes_pseudo_round_xor()` were affected. - **MSVC: fix iterator-to-pointer cast in `http_auth.cpp`.** MSVC `boost::as_literal()` iterator is a class, not a raw pointer. Used `&*data.begin()` to obtain the address. - **MSVC: guard `unbound.h` include and usage in `util.cpp`.** The include and `unbound_built_with_threads()` function/call were not wrapped in `HAVE_DNS_UNBOUND`, causing a missing-header error. - **MSVC: guard `unistd.h` in easylogging++.** The third-party logging library unconditionally included `` which does not exist on MSVC. - **MSVC: add `` include for `_isatty` in `mlog.cpp`.** The WIN32 code path uses `_isatty`/`_fileno` which require `` on MSVC. - **MSVC: fix `boost::iterator_range` conversion in `http_auth.cpp`.** Boost 1.90 `as_literal()` returns an iterator type that does not implicitly convert to `iterator_range` on MSVC. Changed to `auto` deduction. - **MSVC: add `` include for `std::towlower` in `language_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_val` and `insert_next_value` from strict rvalue-reference parameters (`t_entry_type&&`) to pass-by-value, allowing lvalue forwarding from `portable_storage::insert_first_value` / `insert_next_value` to work correctly under MSVC template deduction. - **MSVC: force-include `` for C++ alternative tokens.** The codebase uses `not`, `and`, `or` extensively (hundreds of sites). MSVC does not recognise these as keywords by default. Added `/FIiso646.h` to 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 the `THROW_ON_RPC_RESPONSE_ERROR` macro chain, causing `throw_wallet_ex` template deduction failures. Added `/Zc:preprocessor` to MSVC compile flags and removed the obsolete Boost.Preprocessor-based `throw_wallet_ex` fallback in favour of the standard variadic template version. - **Gitian: enable `universe` repository and remove apt proxy in Docker base image.** The `ubuntu:jammy` Docker image only enables `main restricted` by default; `gitian-build.py` now patches the base image after `make-base-vm` to add `universe` and remove the `apt-cacher-ng` proxy configuration (`/etc/apt/apt.conf.d/50cacher`). The proxy routes all apt traffic through `172.17.0.1:3142` which is unreliable on ephemeral CI runners, causing persistent 503 failures during package installation. Uses `docker 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`'s `docker build` step) along with passwordless `sudo` for the `ubuntu` user, allowing `linux-libc-dev:i386`, `gcc-multilib`, and `g++-multilib` to be installed normally via the descriptor's `packages:` section. - **Gitian macOS: add `libtinfo5` and `python-is-python3`, remove `python` from `FAKETIME_PROGS`.** The pre-built Clang 9 cross-compiler requires `libtinfo.so.5`. The `python` faketime wrapper broke CMake's `FindPythonInterp` version detection in the `native_libtapi` build (empty `PYTHON_VERSION_STRING`); removing `python` from the faketime wrappers fixes this while preserving timestamp reproducibility for `ar`, `ranlib`, `date`, `dmg`, and `genisoimage`. - **Gitian Android: add `python-is-python3`.** Android NDK r17b scripts use `#!/usr/bin/env python` which does not exist on Jammy without this package. - **Gitian macOS: fix Rust `ring` crate cross-compilation.** `BuildRust.cmake` incorrectly overrode the macOS cross-compiler with the Linux system `clang` when cross-compiling for Darwin, causing the `ring` crate to include Linux-only `cet.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@8` symbol against MinGW's `ws2_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 ` in the `ring` crate's assembly, but `cet.h` does 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; the `cc-rs` crate respects this env var, fixing the `ring` build for `aarch64-apple-darwin`. - **Gitian Docker base image: install `sudo` before creating sudoers entry.** The `/etc/sudoers.d/` directory does not exist in the minimal Ubuntu image until the `sudo` package is installed. ### 🔄 Changed - **Replace all `BOOST_FOREACH` / `BOOST_REVERSE_FOREACH` with range-for loops.** 31+ call sites across test and utility code replaced with standard C++11 range-based for. Adds `/DNOMINMAX` to MSVC definitions to prevent Windows `min`/`max` macro collisions. - **Replace hardcoded `-fPIC` with `POSITION_INDEPENDENT_CODE`.** The CMake property works across all compilers (GCC, Clang, MSVC). Applied to `liblmdb` and `easylogging++` CMakeLists. - **Guard/remove unguarded `#include `.** POSIX header guarded behind `#ifndef _WIN32` in `blockchain_import.cpp`; unused include removed from `crypto.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`, and `wallet2.cpp`. GCC/Clang accepted these as extensions; MSVC rejects them. - **Replace all `__thread` with `thread_local`.** Covers `easylogging++.cc`, `perf_timer.cpp`, and `threadpool.cpp`. The `__thread` qualifier is GCC/Clang-specific; `thread_local` (C++11) is portable across GCC, Clang, and MSVC. - **Centralize `ssize_t` typedef in `src/common/compat.h`.** Replaces duplicate `#if defined(_MSC_VER)` guards in `util.h` and `download.h` with 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. The `src/multisig/` directory and `src/wallet/message_store.h` are deleted; `wallet2.h` no longer depends on those headers. All multisig uses PQC-only authorization (`scheme_id = 2`) via the `pqc_auth` layer. - **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 the `dpkg --add-architecture i386` workaround. Simplifies the Docker base image patching to only enable the `universe` repository. ### 📚 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.md` for the design. ### ✨ Added - **Rust wallet RPC server (`shekyl-wallet-rpc`)**: New Rust crate that replaces the C++ `wallet_rpc_server` with an axum-based JSON-RPC server. Calls the existing C++ `wallet2` library 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. See `docs/WALLET_RPC_RUST.md`. - **C++ wallet2 FFI facade (`wallet2_ffi.cpp/.h`)**: Opaque-handle C API over `wallet2` with JSON serialization at the boundary. Includes a generic `wallet2_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 child `shekyl-wallet-rpc` process and communicating via HTTP. Eliminates process management, port allocation, and HTTP overhead. Removed `wallet_process.rs` and `wallet_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`) thread `hf_version=1` and `use_view_tags=true`. Coinbase outputs are indexed under `amount=0` for 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_1` now decrypt ecdhInfo amounts using the recipient's view key instead of relying on the plaintext `o.amount` field (always 0 for RCT outputs). - **Recalibrated economic constants for Shekyl**: Test constants (`TESTS_DEFAULT_FEE`, `FIRST_BLOCK_REWARD`, `MK_COINS`) match Shekyl's `COIN = 10^9`, `EMISSION_SPEED_FACTOR = 21`, and staker/burn splits. `construct_miner_tx_manually` in 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.cpp` that exercised legacy Borromean range proofs were removed. Shekyl HF1 rejects Borromean proofs at the `genRctSimple` level. Retained 9 non-Borromean tests (CLSAG, HPow2, d2h, d2b, key_ostream, zeroCommit, H, mul8). - **Updated transaction construction helpers to Bulletproofs+**: The `test::make_transaction` helper (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 obsolete `bulletproof` parameter. Consolidated three JSON serialization tests (RegularTransaction, RingctTransaction, BulletproofTransaction) into one `BulletproofPlusTransaction` test. Fixes all 8 zmq_pub/zmq_server test failures. - **Updated serialization round-trip test to BP+**: Changed `Serialization.serializes_ringct_types` from `bp_version 2` (throws "Unsupported BP version") to `bp_version 4` (Bulletproofs+). Updated assertions from MGs to CLSAGs and from `bulletproofs` to `bulletproofs_plus`. - **Removed legacy Monero-era core/perf test executions**: Stopped running deprecated Borromean/pre-RCT/fork-transition test generators in `core_tests` and 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_weight` comparison now enforces deterministic `H/BW/LTBW` parity 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 of `FINAL_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 install` with `apt 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::filesystem` support in the cross-compiled libc++. - Fixed Boost discovery in depends builds by setting `Boost_NO_BOOST_CMAKE` and forcing MODULE mode, preventing `BoostConfig.cmake` variant-check failures on cross-compiled Darwin libraries. - Made `boost_locale` a conditional dependency (Windows only), since it is only used within `#ifdef WIN32` blocks and was unavailable for Darwin cross-builds. - Added per-target `CC_/AR_/CFLAGS_` environment variables in `BuildRust.cmake` so the `ring` crate 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 `ring` 0.17 requires clang features unavailable in Clang 9 (macOS 11 version strings, `-fno-semantic-interposition`). - Guarded `-fno-semantic-interposition` behind `check_c_compiler_flag()` so it is only added when the compiler supports it (Clang 9 does not). - Fixed OSX SDK cache key in `depends.yml` to include the SDK version and skip the cache step for non-macOS builds. - **FreeBSD cross-compilation (depends CI)**: Fixed multiple build failures for the x86_64 FreeBSD target: - Switched Boost's b2 toolset from `gcc` to `clang` for FreeBSD, fixing C++ standard library header resolution (`` 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)_cxxflags` overrides the host flags (previously broke zeromq, sodium, and other packages). - Fixed compiler wrapper argument quoting: replaced the broken `echo "...$$$$""@"` pattern with `printf '..."$$$$@"'` 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_FUNCTION` to both Boost's FreeBSD cxxflags and the CMake toolchain, restoring `std::unary_function` compatibility needed by Boost 1.74's `container_hash/hash.hpp` under FreeBSD's strict C++17 libc++. - Removed the unsupported `no-devcrypto` option from OpenSSL's FreeBSD configure flags (the devcrypto engine was removed in OpenSSL 3.0). - Added `threadapi=pthread runtime-link=shared` to Boost's FreeBSD config options for correct threading and linking behavior. - **Linux static release build (libudev linking)**: Added `libudev-dev` to the `release-tagged.yml` CI package list. Static `libusb-1.0.a` and `libhidapi-libusb.a` depend on `libudev` for USB hotplug support; without the dev package installed, `find_library(udev)` failed and the final link produced undefined `udev_*` references, preventing the "Publish GitHub Release" step from running. - **Win64 build failure (ICU generator expression)**: Replaced broken CMake generator expressions `$<$:${ICU_LIBRARIES}>` with `if(WIN32)` blocks in `simplewallet`, `wallet_api`, and `libwallet_api_tests` CMakeLists. Generator expressions cannot contain semicolon-separated lists; the old pattern passed literal fragments like `$<1:icuio` to the linker on MinGW cross-compilation. - **Linux static build (libunbound linking)**: Fixed `FindUnbound.cmake` scoping bug where `list(APPEND UNBOUND_LIBRARIES ...)` created a local variable shadowing the `find_library` cache entry. The transitive static deps (libevent, libnettle, libhogweed, libgmp) were silently dropped, causing undefined reference errors in `release-static-linux-x86_64` builds. - **JSON serialization of v3 (PQC) transactions**: Added missing `pqc_auth` field to the RapidJSON `toJsonValue`/`fromJsonValue` roundtrip for `cryptonote::transaction`. V3 transactions created under `HF_VERSION_SHEKYL_NG` include a `pqc_authentication` envelope; without JSON support the field was silently dropped, causing `get_transaction_hash` to fail with "Inconsistent transaction prefix, unprunable and blob sizes" after a JSON roundtrip. Fixes the `JsonSerialization.BulletproofPlusTransaction` unit test failure. ### GUI Wallet - New project: Shekyl GUI Wallet (`shekyl-gui-wallet`) at [Shekyl-Foundation/shekyl-gui-wallet](https://github.com/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 to `wallet2_api.h` for 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.yml` runs ESLint, TypeScript type-check, Vitest, Rustfmt, Clippy, and cargo test on every PR; `release.yml` builds multi-platform binaries (Linux x64, Windows x64, macOS ARM64 + Intel) via `tauri-action` and 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 uses `DIFFICULTY_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.md` to mark the 120s block-time drift item as resolved (`code_fix_required` completed). ### 📚 Documentation - Updated `docs/V3_ROLLOUT.md` to reflect HF1 (genesis) activation instead of the stale HF17 references. Added v3-first test strategy section. - Updated `docs/POST_QUANTUM_CRYPTOGRAPHY.md` scheme_id status table and deferred-items section from HF17 to HF1. - Updated `docs/PQC_MULTISIG.md` V3 signature list heading from HF17 to HF1. - Updated `docs/STAKER_REWARD_DISBURSEMENT.md` to reference HF1 activation. - Updated `docs/ECONOMY_TESTNET_READINESS_MATRIX.md` HF naming drift label from `doc_correction` to resolved. - Added `core_tests` section to `docs/COMPILING_DEBUGGING_TESTING.md` documenting the v3-from-genesis test approach and how to run/filter tests. ### Genesis initialization compatibility - Regenerated `GENESIS_TX` for 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, including `FAKECHAIN`. - Fixed genesis reward validation to accept the hardcoded `GENESIS_TX` amount at `height == 0` while 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 emit `tx.version = 2` with view-tagged outputs for current HF1 expectations. - Added canonical root build command `make genesis-builder` (using the main release build dir with `GENESIS_TOOL_SRC_DIR`) to avoid split/ambiguous genesis-builder binaries across multiple build trees. ### Testnet economy readiness checks - Added `docs/ECONOMY_TESTNET_READINESS_MATRIX.md` to 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.py` to verify multi-node testnet tuple consistency (`height 0 block hash`, `miner tx hash`, `tx hex`) and optional economy field presence in `get_info`. - Added Rust parity/invariant tests: - `shekyl-economics-sim`: validates `SimParams::default()` against `config/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.py` and included it in `functional_tests_rpc.py` default test list to assert required economy fields are exposed by `get_info`. - Corrected documentation errors without changing design intent: - Clarified `DESIGN_CONCEPTS.md` Section 2 as historical baseline. - Removed duplicate heading in `GENESIS_TRANSPARENCY.md`. - Linked `RELEASE_CHECKLIST.md` testnet section to the rehearsal runbook/checklist and deterministic tuple check command. ### BREAKING: Second-pass rebrand (wallet, URI, serialization) - **URI scheme**: Wallet URI generation and parsing now use `shekyl:` only. The legacy `monero:` 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 `Monero` to `Shekyl`: - `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_SIGNING` changed 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.stderr` to `shekyl.daemon.stdout.stderr`. - **Log file names**: All blockchain utility log files renamed from `monero-blockchain-*` to `shekyl-blockchain-*`. - **DNS seed/checkpoint domains**: Replaced `moneroseeds.*` and `moneropulse.*` lookups with 5-domain consensus set: `shekyl.org`, `shekyl.net`, `shekyl.com`, `shekyl.biz`, `shekyl.io`. Majority threshold is 3 of 5. See `shekyl-dev/docs/DNS_CONFIG.md` for the full infrastructure reference. - **Update check**: Software name comparison for macOS `.dmg` extension switched from `monero-gui` to `shekyl-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_CATEGORY` build-internal macros, and `MakeCryptoOps.py` build artifacts. #### Operator migration checklist 1. Delete old wallet cache files (`.keys` files are unaffected). 2. Re-export any key-image, multisig, or output export files. 3. Re-export and re-sign any unsigned/signed transaction sets. 4. Regenerate all `monero:` QR codes/payment URIs as `shekyl:` URIs. 5. Update any scripts or integrations that parse URI scheme or file magic. 6. Verify message signatures were not created with the old signing domain. 7. Update log rotation configs if they reference `monero-blockchain-*` paths. 8. Update DNS infrastructure to serve records under all 5 TLDs (`.org`, `.net`, `.com`, `.biz`, `.io`). See `shekyl-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), and `cryptonote_core.cpp` (2 sites). Since all `HF_VERSION_*` constants are 1, every `hf_version >= HF_VERSION_*` was always true and every `hf_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, and `use_fork_rules()` call sites. Removed ~500-800 lines of dead conditional logic. - **Dropped v1 transaction support entirely**: - **Consensus**: `check_tx_outputs` now rejects `tx.version == 1` outright. `check_tx_inputs` sets `min_tx_version = 2` unconditionally; unmixable output counting and ring-size exemptions removed. v1 ring signature verification code and threaded v1 signature checking removed from `check_tx_inputs`. `expand_transaction_2` only 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`, and `decodeRctSimple` only accept `RCTTypeCLSAG` and `RCTTypeBulletproofPlus`. 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 use `genRctSimple` (CLSAG path). - **Tx verification utils**: Removed `RCTTypeSimple`, `RCTTypeFull`, `RCTTypeBulletproof`, `RCTTypeBulletproof2` from 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`). Replaced `verRct` with `verRctNonSemanticsSimple` in `check_tx_signature.h`. Removed `decodeRct` else-branches from `rct.cpp`, `rct2.cpp`, `bulletproofs.cpp`, `bulletproof_plus.cpp`. In `unit_tests/ringct.cpp`: removed Borromean, MLSAG, and RCTTypeFull-only tests; rewrote `make_sample_rct_sig` to use `genRctSimple`; replaced all `verRct` calls with `verRctSimple`. - **Wallet v1 cleanup**: Removed unmixable sweep functions, v1 fee/amount paths, v1 coinbase optimization, dead non-RCT creation branches, and replaced `RangeProofBorromean` defaults with `RangeProofPaddedBulletproof`. `sweep_dust` RPC returns error; `createSweepUnmixableTransaction` API returns empty result with error status. - **Trezor Shekyl rebrand**: Renamed all include guard macros from `MONERO_*_H` to `SHEKYL_*_H` in 8 `device_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 in `net_ssl.cpp` with Rust's `rcgen` crate (ECDSA P-256) via FFI. Eliminates all `RSA_new`, `RSA_generate_key_ex`, `EC_KEY_new`, `EC_KEY_generate_key`, and other OpenSSL 3.0-deprecated API calls. The `create_rsa_ssl_certificate` and `create_ec_ssl_certificate` functions are replaced by a single `create_ssl_certificate` that delegates to `shekyl_generate_ssl_certificate` in 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 classical `X25519:P-256:P-384` when the OpenSSL build lacks PQ support. Also added explicit TLS 1.3 ciphersuite configuration. Removed deprecated `SSL_CTX_set_ecdh_auto` call. - **Secure memory wiping migrated to Rust (`zeroize`)**: Replaced the platform-specific `memwipe.c` implementation (memset_s / explicit_bzero / compiler-barrier fallback) with a single call to the Rust `zeroize` crate via `shekyl_memwipe` FFI. The `zeroize` crate uses `write_volatile` which is guaranteed not to be optimized away, replacing the fragile compiler barrier tricks. - **Memory locking migrated to Rust (`libc`)**: Replaced the GNUC-only `mlock`/`munlock`/`sysconf` calls in `mlocker.cpp` with Rust FFI functions (`shekyl_mlock`, `shekyl_munlock`, `shekyl_page_size`) backed by the `libc` crate. Adds Windows `VirtualLock`/`VirtualUnlock` support that was previously missing (`#warning Missing implementation`). The `mlocked` and `scrubbed` C++ template wrappers are preserved unchanged. - **New Rust FFI dependencies**: Added `rcgen = "0.14"`, `zeroize = "1"`, `libc = "0.2"` to `shekyl-ffi/Cargo.toml`. - **C-compatible FFI header**: Added `src/shekyl/shekyl_secure_mem.h` with C-linkage declarations for the secure memory primitives, usable from both C (`memwipe.c`) and C++ (`mlocker.cpp`) translation units. - **CMake wiring**: `epee` library now links `${SHEKYL_FFI_LINK_LIBS}` and includes `${CMAKE_SOURCE_DIR}/src` for 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 `.so` paths that don't exist on rolling-release distros (e.g. Arch Linux with Boost 1.90). Added a shim in the root `CMakeLists.txt` that remaps all `Boost_*_LIBRARY` variables to `Boost::*` imported targets when CONFIG mode is active. Fixes linker failures on Arch. - **Removed duplicate `parse_amount` test**: Two identical `TEST_pos(18446744073709551615, ...)` entries in `tests/unit_tests/parse_amount.cpp` caused 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_LOCATION` files exist on disk. Gives a clear `FATAL_ERROR` with remediation steps instead of a cryptic linker failure minutes into the build. - **Arch Linux CI**: Added `boost-libs` to the Arch pacman install to provide shared `.so` files alongside the `boost` headers/cmake-config package. - **Ubuntu 24.04 test matrix**: Added Ubuntu 24.04 to the `test-ubuntu` CI 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.txz` had 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 match `hosts/freebsd.mk`. Added `-stdlib=libc++` to CXXFLAGS and LDFLAGS since FreeBSD uses libc++ and the Ubuntu host's clang-14 defaults to libstdc++. Also added `libc++-14-dev` and `libc++abi-14-dev` to 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_locale` reports "No suitable build variant"). `find_package(Boost ... CONFIG)` is now skipped when `DEPENDS` is true (set by the depends toolchain), falling back to the more robust MODULE mode (`FindBoost.cmake`). - **OpenSSL: disabled `devcrypto` engine for FreeBSD**: Added `no-devcrypto` to FreeBSD OpenSSL configure options. The `/dev/crypto` engine requires the `crypto/cryptodev.h` kernel 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()`, and `get_clsag_fork()` from `wallet2.cpp`. These Monero-era version ladders had no call sites; Shekyl activates all features from HF1. - **Removed dead variable**: Deleted unused `bool refreshed` in `wallet2::refresh()`. - **Removed legacy `result_type` typedefs**: Deleted `using result_type = void` from `add_input` and `add_output` visitor structs in `json_object.cpp`. These were required by `boost::static_visitor` but are unused by `std::visit`. - **Fixed uninitialized-variable warning**: Zero-initialized `local_blocks_to_unlock` and `local_time_to_unlock` in `wallet2::unlocked_balance_all()`. - **Fixed aliasing cast in wallet serialization**: Replaced C-style cast of `m_account_tags` from `pair` to `pair&` with direct `.parent()` accessor, eliminating formal undefined behavior. - **Suppressed epee warnings**: Added targeted `#pragma GCC diagnostic` guards for `-Wclass-memaccess` (memcpy into `mlocked>` in `keyvalue_serialization_overloads.h`) and `-Wstring-compare` (type_info comparisons in `portable_storage.h`). - **Renamed test target**: `monero-wallet-crypto-bench` renamed to `shekyl-wallet-crypto-bench`. - **Trezor Protobuf fixes**: Added `std::string()` wrapping for `GetDescriptor()->name()` calls in `messages_map.cpp/.hpp` to handle Protobuf 22+ returning `absl::string_view`/`std::string_view`. Added missing `` include to `exceptions.hpp`. ### Rust crypto infrastructure - **New `shekyl-crypto-hash` crate**: Implements `cn_fast_hash` (Keccak-256 with original padding, not SHA3) and `tree_hash` (Merkle tree) in Rust using `tiny-keccak`. Both functions produce byte-identical output to the C implementations in `src/crypto/hash.c` and `src/crypto/tree-hash.c`. - **FFI exports**: `shekyl_cn_fast_hash` and `shekyl_tree_hash` exposed through `shekyl-ffi` with C-ABI declarations in `shekyl_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.mdc` establishing 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 named `HF_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_subaddress` and `unlocked_balance_per_subaddress` skip staked outputs. - **Unstake transaction fixed**: `create_unstake_transaction` now passes matured staked output indices directly to `create_transactions_from`, properly using the actual staked UTXOs as transaction inputs with standard ring signatures. - **Claim reward validation fixed**: `check_stake_claim_input` now looks up the real staked output from the blockchain DB to get the actual amount and tier, replacing the hardcoded `shekyl_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. Wallet `estimate_claimable_reward` now calls this RPC instead of returning a hardcoded zero. - **CLI improvements**: `balance` command now shows staked balance alongside liquid and unlocked balances. New `staking_info` command shows wallet staking overview (locked/matured output counts with tier and remaining lock blocks). `stake`, `unstake`, and `claim_rewards` commands now include daemon connectivity guards. - **Wallet RPC fixes**: `unstake` response changed from single `tx_hash` to `tx_hash_list` array to support multi-transaction unstaking. `stake` request now accepts `account_index` parameter. New `get_staked_balance` RPC 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`. Fixed `create_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.md` field naming: `hybrid_ownership_material` renamed to `hybrid_public_key` to 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 in `rust/shekyl-crypto-pq/tests/negative_vectors.rs`. - Reconciled `POST_QUANTUM_CRYPTOGRAPHY.md` Open Items: resolved Rust crate selection, `RctSigningBody` layout, ownership binding, and max tx size; only `scheme_id` registry extension remains open. - Added tentative V4 PQC Privacy Roadmap to `POST_QUANTUM_CRYPTOGRAPHY.md` with four phases (V4-A Research, V4-B Prototype, V4-C Testnet, V4-D Activation) and explicit KEM composition decision milestone (`X25519 + ML-KEM-768` via `HKDF-SHA-512`). - Added payload limit guidance section to `V3_ROLLOUT.md` with recommended minimum mempool/ZMQ/relay buffer sizes for post-PQC transactions. ### Economics and simulation - Added `rust/shekyl-economics-sim` workspace crate: reproducible 8-scenario simulation harness driven from `config/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 in `docs/economics_sim_results.json`. - Provisionally locked `tx_baseline` (50) and `FINAL_SUBSIDY_PER_MINUTE` (300,000,000) in `DESIGN_CONCEPTS.md` after simulation validation; pending final testnet confirmation. - Wired live chain-health RPC fields in `get_info`: `release_multiplier` now computed from rolling `tx_volume_avg`, `burn_pct` from current chain state, `total_burned` persisted in LMDB and accumulated per block. - Wired `total_staked` in `get_staking_info` via new `Blockchain::get_total_staked()` accessor backed by existing stake cache. - Added `total_burned` LMDB persistence: `set_total_burned`/`get_total_burned` on `BlockchainDB`, with rollback support via extended `staker_accrual_record` (`actually_destroyed` field). ### Privacy and anonymity networks - Updated `ANONYMITY_NETWORKS.md` with 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.md` wire 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.md` covering claim timing, amount correlation, and staked output visibility. - Added reward-driven privacy/mixing research appendix to `DESIGN_CONCEPTS.md` evaluating 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_STANDARD` changed from 14 to 17 in both the main `CMakeLists.txt` and the macOS cross-compilation toolchain (`contrib/depends/toolchain.cmake.in`). This unblocks `std::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 in `src/`, `contrib/epee/`, and `tests/`. Replaced `boost::optional` with `std::optional`, `boost::none` with `std::nullopt`, `boost::make_optional` with `std::make_optional`, and `.get()` accessor calls with `*` / `->`. Added a `std::optional` Boost.Serialization adapter in `cryptonote_boost_serialization.h` so PQC auth fields serialize correctly. Replaced `BOOST_STATIC_ASSERT`/`boost::is_base_of` with `static_assert`/`std::is_base_of` in Trezor `messages_map.hpp`. - **`boost::filesystem` → `std::filesystem` (wallet/RPC layer)**: Migrated `wallet_manager.cpp`, `wallet_rpc_server.cpp`, `core_rpc_server.cpp`, and `wallet_args.cpp` from `boost::filesystem` to `std::filesystem`. Combined with the earlier utility-file migration, this covers all filesystem usage outside of `net_ssl.cpp` (epee, deferred due to permissions API coupling). - **`boost::format` removal (wallet/RPC layer)**: Replaced all `boost::format` calls in `wallet2.cpp` (4), `wallet_rpc_server.cpp` (8), and `wallet_args.cpp` (1) with stream output or string concatenation. `simplewallet.cpp` (106 uses, i18n-sensitive) remains deferred. - **`boost::chrono`/`boost::this_thread` in daemonizer**: Replaced with `std::chrono`/`std::this_thread` in `windows_service.cpp` (PR #9544 equivalent). - **Medium-effort Boost removals (completed earlier)**: - `boost::algorithm::string` (trim, to_lower, iequals, join) replaced with `tools::string_util` helpers in `src/common/string_util.h`. - `boost::format` replaced with `snprintf`, stream output, or string concatenation in `util.cpp`, `message_store.cpp`, `gen_ssl_cert.cpp`, `gen_multisig.cpp`. - `boost::regex` replaced with `std::regex` in `simplewallet.cpp` and `wallet_manager.cpp`. - `boost::mutex`, `boost::lock_guard`, `boost::unique_lock`, and `boost::condition_variable` replaced with `std::mutex`, `std::lock_guard`, `std::unique_lock`, and `std::condition_variable` in `util.h`, `util.cpp`, `threadpool.h`, `threadpool.cpp`, and `rpc_payment.h`/`rpc_payment.cpp`. - `boost::thread::hardware_concurrency()` replaced with `std::thread::hardware_concurrency()`. - **Filesystem migration (utility files, completed earlier)**: - `boost::filesystem` replaced with `std::filesystem` in `blockchain_export.cpp`, `blockchain_import.cpp`, `cn_deserialize.cpp`, `util.cpp`, `bootstrap_file.h`/`.cpp`, and `blocksdat_file.h`/`.cpp`. - Eliminated `BOOST_VERSION` preprocessor conditional in `copy_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 from `boost::variant` to C++17 `std::variant` across the entire codebase (~100+ replacements in ~40 files): - **Serialization layer rewrite** (`serialization/variant.h`): Replaced Boost.MPL type-list iteration with C++17 `if constexpr` recursion for deserialization and `std::visit` lambda for serialization. Removed all `boost::mpl`, `boost::static_visitor`, and `boost::apply_visitor` usage. - **Archive headers**: Replaced `boost::mpl::bool_` with `std::bool_constant` in `binary_archive.h`, `json_archive.h`, and `serialization.h`. Replaced `boost::true_type`/`false_type` and `boost::is_integral` with `std` equivalents. - **Core typedefs**: Changed `txin_v`, `txout_target_v`, `tx_extra_field`, `transfer_view::block`, and Trezor `rsig_v` from `boost::variant` to `std::variant`. - **Boost.Serialization shim**: Added a local ~45-line `std::variant` serialization adapter in `cryptonote_boost_serialization.h` (save/load with index + payload, wire-compatible with old `boost::variant` format). Removed dependency on ``. - **Mechanical replacements** across all `src/` and `tests/` files: `boost::get(v)` → `std::get(v)`, `boost::get(&v)` → `std::get_if(&v)`, `v.type() == typeid(T)` → `std::holds_alternative(v)`, `v.which()` → `v.index()`, `boost::apply_visitor(vis, v)` → `std::visit(vis, v)`. - **P2P layer**: Updated `net_peerlist_boost_serialization.h` to use `std::false_type`/`std::true_type` instead of `boost::mpl` equivalents. - `tests/unit_tests/net.cpp` retains `boost::get` for `boost::tuple` access via `boost::combine` (not variant-related). - **Remaining deferred Boost areas**: ASIO deep plumbing, multi-index containers, Spirit parser, multiprecision, `net_ssl.cpp` filesystem, `simplewallet.cpp` format strings, `boost::thread::attributes` (stack size). Tagged with `TODO(shekyl-v4)` in source. See `DOCUMENTATION_TODOS_AND_PQC.md` section 1.11 for the full backlog. ### CI/CD and build system - **Boost minimum bumped to 1.74**: `BOOST_MIN_VER` in `CMakeLists.txt` raised from 1.62 to 1.74. The `contrib/depends` system 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`, and `release-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 canonical `SHEKYL_*` names (`SHEKYL_VERSION`, `SHEKYL_VERSION_TAG`, `SHEKYL_RELEASE_NAME`, `SHEKYL_VERSION_FULL`, `SHEKYL_VERSION_IS_RELEASE`) in `src/version.h` and `src/version.cpp.in`. The old `MONERO_*` 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 the `gitian.yml` GitHub Actions workflow. The workflow now passes `--url` to ensure the correct repository is cloned. Added checkout error handling with an actionable message when a tag/branch is missing. - Tag-driven versioning: `GitVersion.cmake` now extracts the version string from git tags (e.g. `v3.0.2-RC1` → `3.0.2-RC1`). The hardcoded version in `version.cpp.in` is replaced with the CMake-substituted `@SHEKYL_VERSION@`; a default (`3.1.0`) is used for development builds not on a tag. `Version.cmake` centralises the fallback default in `SHEKYL_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.18` to Shekyl `3` series. - Added `release/tagged` GitHub Actions workflow: builds static Linux x86_64 binaries, cross-compiles Windows x64 via MinGW, and produces `.tar.gz`, `.deb`, `.rpm`, `.zip`, and NSIS `.exe` installer artifacts on every `v*` tag. - Added `BuildRust.cmake` cross-compilation support: detects `CMAKE_SYSTEM_NAME` and `CMAKE_SYSTEM_PROCESSOR` to 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 for `libshekyl_ffi.a` linking. - Fixed Gitian `gitian-build.py` to fetch tags explicitly (`--tags`) during repository setup, preventing checkout failures for tag-based builds. - Enhanced `gitian-build.py` error handling: robust `lsb_release` detection, auto-correction of stale clone origins when `--url` changes, and detailed diagnostics on checkout failure (lists available remote tags and suggests the push command). - Added `workflow_dispatch` trigger to `gitian.yml` with configurable `tag` and `repo_url` inputs, allowing manual re-runs and testing against forks without retagging. - Fixed Doxygen project name from `Monero` to `Shekyl` in `cmake/Doxyfile.in`. - Replaced bundled Google Test 1.7.0 (2013) with CMake `FetchContent` for GoogleTest v1.16.0. Fixes `GTEST_SKIP` compilation 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/checkout` to v5, `actions/cache` to v5, `actions/upload-artifact` to v6, and `actions/download-artifact` to v7 to resolve the Node.js 20 deprecation warnings. - Trimmed `depends.yml` cross-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) and `contrib/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_priority` enum + utility functions), improved subaddress lookahead logic, `set_subaddress_lookahead` RPC 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_connections` column width, ZMQ IPv6 support, dynamic base fee estimates via ZMQ, `getblocks.bin` start height validation, CryptoNight v1 error reporting, batch key image existence check, blockchain prune DB version handling, removed `COMMAND_RPC_SUBMIT_RAW_TX` (light wallet deprecated). - **P2P/Network**: Removed `state_idle` connection state, fixed inverted peerlist ternary, removed `#pragma pack` from protocol defs, connection patches for reliability, dynamic block sync span limits. - **Crypto/Serialization**: Fixed invalid `constexpr` on hash functions, added `hash_combine.h`, aligned container pod-as-blob serialization, fixed `apply_permutation()` for `std::vector`. - **Build system**: Removed iwyu/MSVC/obsolete CMake targets, added `MANUAL_SUBMODULES` cache option, Trezor protobuf 30 compatibility, fixed `FetchContent`/`ExternalProject` cmake 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, `constexpr` `cn_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 for `shekyld`, `shekyl-wallet-cli`, `shekyl-wallet-rpc`, blockchain utilities, and debug tools. ### Operations - Added `utils/systemd/shekyld.service` for Shekyl-native daemon service deployment (`/usr/local/bin/shekyld` + `/etc/shekyl/shekyld.conf`). - Updated `docs/INSTALLATION_GUIDE.md` related-doc references to include seed operations documentation in the companion `shekyl-dev` docs set. - Added `docs/BLOCKCHAIN_NETWORKS.md` with 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), and `13025` (ZMQ), and aligned test/docs references so `--testnet` workflows use `12029` while scripts support overrideable network/daemon variables. - Updated libwallet API helper scripts to call `shekyl-wallet-cli` (not `monero-wallet-cli`) so test tooling matches Shekyl binary names. ### Staking (end-to-end claim-based system) - Added `txout_to_staked_key` output target type for locking coins at a chosen tier (short/medium/long). Outputs carry `lock_tier` field enforced at the consensus layer. (Note: `lock_until` was originally stored on-chain but was removed in a subsequent fix — see Bug 13 under Unreleased.) - Added `txin_stake_claim` input 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_accrual` and `staker_claims` tables plus a `staker_pool_balance` property 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) use `RCTTypeNull` signatures, cleanly separated from ring-signature transaction validation. - Extended `tx_destination_entry` with `is_staking` and `stake_tier` fields. `construct_tx_with_tx_key` emits `txout_to_staked_key` outputs when `is_staking` is set. - Extended `transfer_details` with `m_staked`, `m_stake_tier`, and `m_stake_lock_until` for wallet-side staking metadata tracking. (`m_stake_lock_until` is computed locally from `creation_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 `, `unstake`, `claim_rewards`. - Added wallet RPC endpoints: `stake`, `unstake`, `get_staked_outputs`, `claim_rewards`. - Added daemon RPC endpoint: `get_staking_info` returning current staking metrics (height, stake ratio, pool balance, emission share, tier lock blocks). - Wired `stake_ratio` and `staker_pool_balance` in `/get_info` to live blockchain state. - No minimum stake amount enforced (matches design doc). - Fixed compilation errors from `txin_stake_claim` missing in exhaustive `boost::static_visitor` patterns: added `operator()` overloads to the double-spend visitor (`blockchain.cpp`) and the JSON serialization visitor (`json_object.cpp`), added JSON deserialization branch for `"stake_claim"` inputs, added `toJsonValue`/`fromJsonValue` declarations and implementations for `txin_stake_claim`, and added Boost.Serialization `serialize()` 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`, and `stake_ratio` to `construct_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_avg` reward path for consistency - Added `Blockchain::get_tx_volume_avg(height)` and `Blockchain::get_stake_ratio(height)` (stubbed to `0` until staking state is consensus-tracked). ### Modular PoW - Added pluggable PoW schema abstractions: - `IPowSchema` interface - `RandomX` and `Cryptonight` schema implementations - PoW registry-based selection preserving existing behavior by block version - Refactored `get_block_longhash` to 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).