feat(bridge): EIP-0045 4-child migration + settlement host — verifyStark TRUE on-chain - #1
Draft
arkadianet wants to merge 27 commits into
Draft
feat(bridge): EIP-0045 4-child migration + settlement host — verifyStark TRUE on-chain#1arkadianet wants to merge 27 commits into
arkadianet wants to merge 27 commits into
Conversation
Byte-exact shared contract for the 5->4-child verifyStark migration: - raw-seal extraction recipe (SuccinctReceipt.seal Vec<u32> -> 222,668 B LE, confirmed against RawSealV1Decoder + the independent real-seal KAT) - ErgoStatementV1 KAT vectors reproduced byte-for-byte from ErgoStarkStatementSpec (empty/synthetic/final-B3 + claim chain) and extended to an AEGISPV1 payload - chainDomainId mechanics + W1 (ReductionContext does not expose the genesis id) - pinned-constants sheet (fixed vs per-devnet vs re-pin-at-cut)
Add ergo_statement_v1() + DOMAIN/version/prefix constants: the byte-exact 159-byte-prefixed statement the guest commits and the node opcode reconstructs (DOMAIN(26) || 0x01 || chainDomainId || profileId || programId || contractId || u32le(payload_len) || payload). The AEGISPV1 epoch journal is the payload. Oracle-pinned by a KAT-parity test against test-vectors/eip0045/ ergo_statement_v1.json vector 4 (aegis-epoch-single-withdrawal): the framed 451-byte statement must equal the reference-validated bytes.
The epoch guest now reads four framing ids (chainDomainId, profileId, programId, contractId) as its first env::read inputs, wraps its existing AEGISPV1 epoch journal as the statement applicationPayload, and commits the WHOLE ErgoStatementV1 (env::commit_slice). Everything the guest proves (epoch validity, anti-fabrication, aux-PoW) is unchanged — only the journal output is re-framed so the node opcode's SHA-256(statement) claim check binds it. Re-pins the image id (expected at devnet relaunch).
Replace the bincode InnerReceipt proof with the raw succinct seal (InnerReceipt::Succinct -> get_seal_bytes(), exactly 222,668 bytes / 55,667 LE-u32 words) — the transport the EIP-0045 stock profile decodes. Fail-closed build-time asserts: seal len == 222,668, word count == 55,667, seal[32] == 18 (outer recursion po2). Write raw_seal.bin instead of receipt_inner.bin. build_env writes the four ErgoStatementV1 framing ids first (guest read order); the committed journal is now the full statement, so the prefix/payload asserts check the ErgoStatementV1 framing. Drop the now-unused bincode dep.
The epoch PegVault now emits the EIP-0045 4-child verifyStark (Payload::Four): proofChunks, applicationPayload, programId, profileId. The node host-derives chainDomainId (its own genesis) + contractId (BLAKE2b-256(SELF.propositionBytes)) and rebuilds the 159-byte prefix, so the vault drops vmType/costParams and pins the FIXED stock profileId as the new 4th child (added to VaultSpec). The applicationPayload is journal_expr UNCHANGED — the R4/R6/R7 root chain survives the re-framing byte-identical. chunk_proof partitions the exact 222,668-byte raw seal into the canonical fixed [65535,65535,65535,26063] Coll[Coll[Byte]] (fail-closed on any other length). Tests: new four_child_shape module asserts the 4-child AST order + payload reconstruction + chunk partition round-trip (runs in the default gate); the reconstruct suite and reject-path binding tests still pass. The 3 happy-path binding tests + the real_verify tier are marked #[ignore] pending the node's 0xB9 Payload::Four dispatch (NODE-OPCODE agent deliverable).
…contractId)
The 4-child migration changed what the guest commits (an ErgoStatementV1, not
the bare AEGISPV1 journal), so the v6 image id 67837eaa… is dead: any receipt
it produced now frames differently and verifyStark would reject it. Re-derive
and re-pin.
programId fb29b593… aux-pow guest, AEGIS_EPOCH_AUXPOW=1, reproduced
across two independent CARGO_TARGET_DIRs
chainDomainId d6dcc221… relaunch devnet genesis (height-1) Header.id
profileId 23c4a123… unchanged (chain-independent stock B3)
contractId ea74c6f2… BLAKE2b-256 of the 537-byte vault propositionBytes
exec-epoch previously wrote a labelled placeholder for chainDomainId and
contractId. A placeholder there does not fail loudly — it produces a perfectly
valid receipt that the opcode silently rejects, hours later, because the node
rebuilds the prefix from its own genesis and SELF.propositionBytes. Both are now
the live values.
Add assert_pinned_image_id() on the prove path and to `image-id`: the driver
refuses to prove unless the guest linked into it hashes to EPOCH_IMAGE_ID.hex.
The pin is the aux-pow image, so this also catches a build that forgot
AEGIS_EPOCH_AUXPOW=1 — otherwise a lean E1/E3 guest silently proves a statement
no vault will accept.
Document (build spec §6) that the cascade converges only because contractId is
an env::read INPUT, not a compiled-in guest constant — the guest ELF, hence
programId, does not depend on it, so vault → contractId → host is a single pass.
F3's PINNED_VAULT_TREE_BYTES is the opposite case and does NOT converge: it is a
compiled-in constant whose required value (the vault ergoTree) embeds the image
id of the guest that contains it. Left empty (F3 fails closed, no peg-in can
mint) with the two design exits recorded.
Vector 4 of the statement KATs keeps its placeholders: it pins the framing
algorithm, and coupling it to per-devnet values would break the engine gate on
every re-pin without binding anything the deployment does not already bind.
…w converges
PINNED_VAULT_TREE_BYTES could never be pinned. The value it had to hold — the
vault's serialized ergoTree, which F3 compares every deposit box against —
embeds the guest image id, which is the id of the guest holding the constant.
Pinning it changed the ELF, which changed the image id, which changed the tree,
which changed the required constant. It sat empty, so F3 failed closed and no
peg-in could ever mint.
Make the tree a WITNESS and bind it in-guest instead: the guest already reads
contractId = BLAKE2b-256(SELF.propositionBytes), which the verifyStark opcode
derives HOST-side from the vault box actually being spent — nothing a script can
move. DepositParams::bind_to_contract accepts vault_tree_bytes only if it hashes
to that id, and F3 recognizes deposits by the accepted bytes.
This is strictly stronger than the pin (the deposit target is bound to the
opcode's own view of the spending vault, not to a build-time constant, so a
stale pin cannot outlive a re-deployed vault) and the self-reference is gone: the
guest holds a hash it receives, never bytes it must contain. The ELF no longer
depends on the vault tree, so the cascade is a single pass —
guest -> programId -> vault tree -> contractId -> host.
Re-derived and re-pinned (build spec §6):
programId 369c541939a4e89cc51bbb3bc74856050b167299c2c0c4cde59181f3e208a890
(was fb29b593…; reproduced across two independent CARGO_TARGET_DIRs)
contractId 28e4d346229b20a07f9e2e502aa2a6485e33d4d66b934f6d2ec03cb6f0b4b711
(was ea74c6f2…; BLAKE2b-256 of the new 537-byte tree)
chainDomainId and profileId are unchanged. The 537-byte tree is pinned verbatim
in exec-epoch as VAULT_TREE_HEX and is now the single source of truth: vault_pins()
recomputes contractId from it and asserts the recorded value, so a half re-pin
fails loudly at startup instead of burning a multi-hour prove on a statement the
opcode rejects. Derivation was oracle-checked by rebuilding the PREVIOUS tree
from image id fb29b593… and confirming it hashes to the recorded ea74c6f2….
PINNED_USE_TOKEN_ID keeps its pin and is set to the real minted devnet USE id
06c6af8414993a8933ba6e815b9ecd2de15eaf14c10d66e9f1de893e276391c8. The USE token
is an EIP-4 mint that exists before the guest is built, so it carries no
self-reference — a re-mint is an ordinary one-pass re-pin.
Also wire F3's host side, which was missing: the guest read a backing witness no
host ever wrote, so exec-epoch could not run an aux-pow guest at all. exec-epoch
now writes vault_tree_bytes + the backing wire in the guest's read order, and
dump_epoch dumps an explicit (empty, peg-in-free) backing.pc; artifact sets from
before F3 fall back to the empty witness rather than refusing to run.
Verified end-to-end on a freshly dumped 11-block / 2-withdrawal epoch: the guest
executes through the new read order (442.8 M cycles, F3 phase logged) and commits
a 497-byte ErgoStatementV1; with contractId flipped one byte the guest ABORTS at
the bind with VaultBindError instead of proceeding.
Test plan
cargo fmt --all -- --check (root, engine, engine/recursion,
settlement, guest-epoch) — clean
cargo clippy --workspace --all-targets --all-features -D warnings (engine) — clean
cargo clippy --workspace --all-targets -D warnings (settlement, RISC0_SKIP_BUILD=1;
--all-features needs the CUDA
container) — clean
cargo clippy --workspace --all-targets --features aux-pow,parallel -D warnings
(engine/recursion) — clean
cargo test --workspace --all-features (engine: 152+20+10+1) — pass
cargo test --release --features aux-pow,parallel (engine/recursion) — pass
cargo test --workspace (root: 20 binaries) — pass
exec-epoch image-id / execute --dir <fresh dump> — pass
A pre-existing doc-list clippy error in engine/recursion/src/lib.rs blocked -D
warnings for the crate whose dump harness this touches; rewrapped.
bridge-tools could not be built at all: its `real-verify` feature forwarded to `ergo-sigma/stark-verify`, and the 4-child branch compiles the EIP-0045 STARK verifier unconditionally, so that feature no longer exists. Cargo validates the whole feature table, so naming a missing feature broke EVERY build rather than just the real-verify tier. Drop the forward; the tier is now a plain marker. That alone is not enough to build, because the crate path-depends on `../../../../ergo` — the shared checkout, which is normally parked on some unrelated branch and must not be moved out from under whoever else is using it. Add scripts/bridge-tools-shadow.sh, which reproduces that relative walk inside a throwaway mirror pointed at the branch's own worktree. Only bridge-tools/ Cargo.toml is copied rather than symlinked: cargo resolves dependency paths against the directory physically holding the manifest, so the walk has to start in the mirror. With the crate building again, two tests in the LEGACY batch vault suite fail. They assert a TRUE outcome under an "M1 stub verifier" premise that the branch retired: 0xB9 now verifies for real and only dispatches the 4-child Payload::Four, which that vault (still Payload::Five) does not emit. Mark those two #[ignore] with the reason — matching how the epoch suite already handles the same situation — and note on the tier that its remaining reject-path tests now pass vacuously, so the staleness is visible instead of reading as coverage.
…isters `deploy-vault` builds the BATCH vault and writes only R4/R5. That produces a vault the epoch predicate can never spend: it reads vault.R6 and vault.R7 into the journal it reconstructs, so a box missing either register fails every release. The registers therefore have to be set AT DEPLOY — there is no later opportunity, since spending the vault is what needs them. Add `epoch-deploy-vault` (and `epoch-vault-address`) building the 4-child vault_epoch tree and initialising R4 state root, R5 counter, R6 settled-burn set root and R7 sealed tip header id. The genesis values are not free. R7 is the sentinel BELOW genesis, so the first epoch's suffix starts at the hn genesis block, whose prev_root is the empty note-tree root — that is R4. R6 is the empty settled-set root because no burn has been settled. All four default to exactly those, computed from the engine rather than pasted as hex, so a genesis deploy is one command with nothing to get wrong; image id and profile id default to the pins for the same reason. The deploy prints BLAKE2b-256 of the tree it is about to deploy, using the node's own hash — the opcode derives contractId the same way from SELF.propositionBytes, so this is what lets the operator confirm the deployed vault is the one the prover pins.
…ecute` The ErgoStatementV1 check only ran in `prove`, and only checked the framing shape — domain tag, version, payload length. It never checked the four ids the statement actually names. Those ids reach the guest as env::read inputs, so a stale pin in build_env produces a perfectly well-formed statement that simply names the wrong chain, profile, program or vault. The node opcode rebuilds the statement host-side and compares SHA-256, so such a receipt is rejected — after a multi-hour GPU prove, with nothing before it having objected. Factor the check into assert_statement, compare all four ids against the pins, and call it from `execute` as well. The whole framing is now verified on CPU, before any GPU time is committed. `execute` also asserts the pinned image id, which it previously skipped.
The epoch release had a tx builder (`build_release_epoch`) but no way to call it: the only `release` subcommand still went through the single- withdrawal batch path. Worse, that command takes every journal-bound field (`new_root`, `counter`, `amount`, `recipient_tree`) as an operator flag, so a typo assembles a tx whose reconstructed journal silently disagrees with the proof it carries. `epoch-release` instead reads the `exec-epoch prove` out-dir and derives ALL of them from the committed `ErgoStatementV1`: successor R4/R6/R7 and the R5 advance are the journal's `new`/`out`/`tip_new` endpoints, and the recipient outputs are its entry list, in journal order. Nothing about the tx is left for the operator to get wrong. What the operator CAN still get wrong is what lives outside the proof, so the preflight checks exactly that and fails closed: the vault's R4/R6/R7 must equal the journal's `prev` endpoints (the state chain welds there, and a wrong input box can't be fixed downstream), and the journal's `ergo_ref` must still occupy the `CONTEXT.headers(ANCHOR)` slot the contract splices — an anchor that has slid out of the recent-header window forces a re-prove, and that is far cheaper to learn before submitting than after.
…ild receipt Three things kept this test from ever passing on a real receipt, all of them consequences of the 4-child migration that landed after it was written. `journal.bin` is now the WHOLE ErgoStatementV1, not the bare AEGISPV1 journal, so parsing it as a journal tripped the tag assert on the statement's own domain tag before any verification could happen. Strip the 159-byte prefix — and reuse `EpochJournal` rather than keeping a second copy of the layout that can drift from it. The other two are the same root cause: under the 4-child interface the opcode derives `chainDomainId` and `contractId` host-side instead of reading them from the script, and both enter the statement the guest committed. So a receipt is only verifiable against the chain and the vault tree it was proved for, and the fixture's arbitrary NFT/USE ids (wrong tree -> wrong contractId) plus `minimal()`'s zeroed chain domain guaranteed a false. Pin both to the relaunch deployment. Because those pins are now load-bearing, the test asserts the receipt's own committed ids agree with them first, so a future re-pin fails as "wrong deployment" instead of looking like the verifier rejected a sound proof.
R4/R6/R7 were checked but R5 was not, and it is bound the same way: the journal's `counter_next` was proved as `vault.R5 + n`, so a vault at any other count fails both the structural advance and the byte-exact journal match. Leaving it unchecked meant the one endpoint that is a number rather than a digest could still surprise the operator at submit time.
…in settlement host Captures the full state at the point the migration was validated end-to-end at the crypto level (real GPU proof -> raw seal -> ported Rust verifier TRUE -> full vault predicate TRUE in the real interpreter) and the one thing that blocks an on-chain release: no EpochWitness/AnchorWitness builder exists for the LIVE chain, only the synthetic dump_epoch fixture. Records the converged pins, the live devnet/Aegis coordinates, the off-limits systems, the path-bound image-id trap and the other time-sinks, and the document map.
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…ath defects The handoff assumed a live-chain settlement host was missing plumbing. It is not. Records the evidence for each blocker and splits the work into a stage that lands an on-chain release now and a stage that makes the suffix node-sourced. B1 the node's spend proofs are uni-stark `Proof<HidingEngineConfig>`, while the recursion tree consumes `BatchProof<RecursionHidingConfig>` + the prover's salted `SpendCommonData` — not re-derivable, and the witness to re-prove is not in a submitted `Tx`. No host-side fix; a live withdrawal cannot settle until the node's proof system moves, which is chain-id-breaking. B2 the deployed vault's R7 is all-zero, and F1's seam demands a real header PREIMAGE whose Poseidon2 id equals it. Unsettleable; the box is abandoned. B3 the live chain is at height 7,859 and a suffix block costs ~24 M cycles, so a genesis-rooted settlement is ~189 G cycles against a proven 442 M. Not blockers: E2 shares (`verify_share` never requires the share's Ergo header to be on the devnet, so the host can mine them) and the guest's pinned USE token id (read only by F3, which a peg-in-free epoch never reaches — the proven image id is reused as-is). Also records two defects with one root cause: `CONTEXT.headers` is TIP-first, so the vault's anchor-index doc is inverted and `epoch-release`'s preflight compares `ergo_ref` against the oldest header while the contract splices the newest.
`Devnet::last_headers` walked `first..=tip` and documented the result as "OLDEST FIRST — the same ordering the vault's ANCHOR_HEADER_INDEX indexes into". It is not. The evaluator presents `CONTEXT.headers` tip-first, `[0]` being the parent of the block under validation (`ergo-validation`'s `CandidateValidationContext::last_headers`; `ergo-node`'s mempool-admission `build_tip_context` builds the same window from `block_context_headers()[0]`, so admission and inclusion agree). So `epoch-release`'s preflight compared the journal's `ergo_ref` against the OLDEST header in the window while the contract splices the NEWEST. That is the worst shape a preflight can have: it passes a release the node then rejects, and blocks a correct one. Reverse the window and print it with slot indices, since the whole check is only as good as the ordering. `ANCHOR_HEADER_INDEX` stays 0, but for the real reason rather than the stated one: the contract reads a single slot, so `ergo_ref` is an exact header at any index, and binding the parent is the slot a driven miner can hit deterministically — freeze at tip T, prove against id(T), submit, mine one.
The only artifact set that existed came from `engine/recursion/tests/dump_epoch.rs`, a measurement fixture whose journal describes a chain that does not exist: a fabricated `ergo_ref`, a 33-byte blob where a recipient ErgoTree belongs, and endpoints unrelated to any deployed vault. `epoch-release` fails its preflight on it and `--force` dies at tx assembly, which is why the proven GPU receipt could never become a release. `epoch-host` emits the same five artifacts for a settlement that can: a real recipient `ErgoTree`, a real amount, real `prove_spend_batch` proofs through the real recursion aggregation, E2 shares mined over a live devnet header, and an E4 anchor walked over real devnet blocks. Split into `plan` and `emit`, because the deploy has to happen BETWEEN them: the settlement shape is note-determined, not proof-determined, so every register a vault must be deployed with is derivable in milliseconds without proving. `plan` prints them, the operator deploys, `emit` re-derives the identical shape, verifies it against the LIVE vault box, and only then spends minutes proving. Getting that backwards costs a whole prove. Each stage fails closed before the expensive one: the suffix must satisfy `verify_epoch` natively; the shape must be reproducible from its inputs; the vault's R4/R5/R6/R7 must equal the settlement's starting endpoints; the mined shares must pass the guest's own `verify_suffix_shares` (F6b included); the anchor must pass `verify_anchor_linkage_min_depth`; and the aggregation root's surfaced digest must equal `epoch_spend_root` over the suffix. Served devnet extensions are re-hashed against each header's PoW-committed `extension_root` before any inclusion proof is built over them — the REST server is not trusted to decide which leaf the guest will check. The suffix is host-originated, not read from the hn node, and the module says so at the top: the node's spend proofs are uni-stark and cannot enter the recursion tree at all (settlement-host-stage1.md §1 B1). Stage 2 replaces `local_chain` behind the same seam once the node's proof system moves.
…er the deploy Two failures found by driving a real settlement end to end. The aggregation tree is 2-to-1 and needs at least one node, so a single spend has nothing to fold into a root — `aggregate_settlement_sha` panics. Reaching that panic in `emit` is far too late: by then a vault has been deployed against registers no receipt can ever be proved for, and since the vault holds the NFT and the whole USE supply behind `verifyStark`, it is stuck for good. Enforce `MIN_WITHDRAWALS` at shape-build time, which is where `plan` runs. The anchor walk took the FIRST committing block found walking back from `ergo_ref`, i.e. the newest — but the commitment is sourced from a file the miner re-reads per candidate, so a run of consecutive blocks carries the same id, and the newest gives depth 0 and fails F5 for no reason. Take the deepest of the run instead: every one of them is a valid `H_anchor` (the guest checks linkage and `depth >= A_MIN`, nothing more), and the deepest is the strongest burial claim available. Withdrawals are plural throughout as a result: `--amount` and `--recipient-tree` repeat, with a single tree reusable for every amount.
…deploy The old vault (`16b2772b…`) is unsettleable: its R7 is all-zero, and F1's seam needs a real header preimage whose Poseidon2 id equals it. It holds the NFT and the whole USE supply behind `verifyStark`, so the tokens are abandoned and the redeploy mints fresh ones — NFT `15e61fd3…`, USE `10237e1c…` — with R4/R7 taken from the settlement the host actually builds. Only the tree and its `contractId` move. The guest image id does not: the vault tree embeds the image id, but `contractId` reaches the guest as an `env::read` input (the opcode derives it host-side from `SELF.propositionBytes`), which is exactly the property that keeps this cascade one-pass. `exec-epoch image-id` confirms built == pinned == `369c5419…` after the change. The guest's `PINNED_USE_TOKEN_ID` is deliberately left stale. It is read only by F3's `DepositParams`, and a peg-in-free epoch never reaches the token check — so re-pinning it would change the image id, and with it the vault tree, for nothing. It must be re-pinned before the first peg-in-carrying settlement.
Both were only findable by running a real settlement, and both are now refused at `plan` time rather than after money has been spent: a one-withdrawal epoch cannot be aggregated (the tree is 2-to-1 and needs a node), and the anchor walk has to take the deepest committing block, since the commitment file is re-read per candidate and a whole run of blocks carries the same id. The first is the expensive one — hitting it in `emit` leaves a vault deployed against registers no receipt can ever chain to, with the NFT and the entire USE supply locked behind `verifyStark`.
Release tx 386ebf54… mined at devnet height 797: the vault box spent through `verifyStark`, 150,000,000 USE released to a real P2PK, and the successor vault carrying exactly the R4/R5/R6/R7 the guest committed. GPU prove 894.8 s at PO2 20, 450 segments, raw seal 222,668 B. The first prove attempt died at segment verification ~13 minutes in. Recorded in the run log as prover-side rather than input-side, because `execute` had already run the guest to completion on CPU — so every segment IS a valid execution trace and a failing segment PROOF cannot be the artifact's fault. No overclock, no Xid, well inside VRAM; ~449 segments verified and one did not, and the identical rerun succeeded. The host-driver/container CUDA skew (595.71.05/13.2 vs 12.6) is noted as the first thing to check if it recurs. What this does NOT establish is written down next to what it does: the settled suffix is host-originated, not node-sourced, because the node's spend proofs cannot enter the recursion tree at all (§1 B1). That is Stage 2.
…, chain v6) Stage-1 established `verifyStark` on-chain but could not settle a LIVE withdrawal, and the reason was not plumbing: `SpendCircuit` emitted `Proof<HidingEngineConfig>` (uni-stark) while the settlement recursion tree verifies `BatchProof<RecursionHidingConfig>`. Different proof systems. Neither the preprocessed commitment nor the witness needed to re-prove is recoverable from a submitted `Tx`, so no host could bridge it — a recorded withdrawal was unsettleable by construction. Moves the wallet to `prove_spend_batch`. The spend AIR is untouched; only the wrapper differs. The load-bearing discovery is that the verifier needs nothing from the prover. `ProverData::from_instances` forwards to `from_airs_and_degrees(config, airs, log_ext_degrees)` — it consumes the trace's DEGREE and nothing else — so `CommonData` is a pure function of (config, AIR, degree). Committing it under the same fixed public preprocessed salt the uni-stark path used for its `vk` makes it a constant every party derives independently, and `spend_common()` is now that constant, memoized. That matters for soundness, not just ergonomics. `common` carries the preprocessed-schedule commitment; a verifier that accepts a prover-supplied one accepts proofs against a schedule the prover chose. The node derives it instead, so `Tx` keeps its exact shape — no `common` on the wire at all. Asserted directly: a `CommonData` built from scratch in a separate config verifies a proof it had no part in producing. The RNG split the uni-stark path documented is preserved and re-asserted: the preprocessed salt is fixed (public schedule, leaks nothing), the per-proof masks are fresh OS entropy, and two proofs of one spend still differ. Wire proof bytes change, so the chain id breaks: v5 -> v6, `0x484E_0006`. `epoch-host` tracks it, which retires the stage-1 vault — a v5 settlement can never be replayed under v6, by design.
Records why the batch-stark migration dissolved B1 rather than working around it, and what the node-backed chain source still has to reconstruct: the historical frontier at T_prev (replay the per-block leaf order and assert the root against the block's committed state_root, so a wrong order fails loudly), the header-only seam, and the settled-set paths. Also calls out that S2.2 is a deployment decision, not just a step: the chain-id break ends the running v5 chain's continuity and retires the stage-1 vault.
…ision The v6 chain runs in a fresh data dir on a separate port, so the chain-id break cost nothing — the v5 chain is still running and intact. Both spends on it were proved with the new batch-stark circuit and verified by the node at admission before being mined: a plain shielded tx and a real 100,000,000 USE peg-out to a real P2PK. That is the migration working end to end on a live chain, not just in tests. Records why it is one peg-out plus one plain tx rather than two peg-outs: the circuit is 2-in/2-out and mints no dummies, so a wallet's note count falls by one per spend and the 2 genesis faucet notes buy exactly one peg-out. The aggregation's constraint is >= 2 SPENDS, not >= 2 withdrawals — which makes `epoch-host`'s MIN_WITHDRAWALS the special case that happened to hold when every spend was a withdrawal, and something `NodeChain` must restate over spends. S2.3 is left at a decision rather than guessed: the REST route already serves complete blocks, but decoding them needs `HnBlock`, and pulling that from `aegis-node` drags the vendored Curve-Trees stack, tokio and arkworks into the settlement workspace for one wire struct. Lifting the block types into a light shared crate is the alternative.
`HnBlock` and friends are a node↔consumer CONTRACT — the bytes in the block log and on `/hn/v1/blocks` — but they lived in `aegis-node`, so decoding a block meant linking the whole node: tokio, arkworks, and the vendored Curve-Trees/bulletproofs stack via `aegis-crypto`, for one struct. The settlement host has to decode blocks to build an epoch witness (S2.3), so this was in the way. NOT `aegis-types`, despite the name being the obvious home: that crate depends on `aegis-crypto` and therefore drags in exactly the stack this split exists to keep out of a settler's graph. The new crate's only dependencies are serde, `aegis-hn-wallet` (which owns `Tx`) and the engine's digest width — so a consumer that already speaks the hash-native engine adds nothing new. Consensus logic (validate/apply/rollback/fork choice) stays in `aegis-node`, and every existing `hn::state::…` path still resolves via a re-export, so no caller changed. The postcard encoding is load-bearing: a live chain replays its log on every restart, so a reordered field would silently reinterpret history. Two things establish it is unchanged rather than assumed. The regression test decodes a real `/hn/v1/blocks` slice served by the node BEFORE the move and asserts it re-encodes byte-identically — the oracle is the chain, not this code, which a hand-written expected hex would not have been. And the refactored binary was restarted on the LIVE v6 chain's existing 15,504-block log: it replayed, kept the pot chain continuous, resumed producing, and still reports the recorded withdrawal `nf0 = c39ab686…ff21` unchanged.
…-types` `to_engine_block` is documented as the node↔guest parity point — the single mapping from a stored block to the view the epoch-validity guest re-derives a header id from — but it was `pub(crate)` in `aegis-node`, so a settlement host had no way to reach it and would have had to keep a second copy. Two copies of a parity point is exactly the drift that makes honest blocks fail the guest's header-chain check. It moves beside the wire types it converts, where both sides use one definition: the node's `hn_header_id` now delegates to it, and the standing `node_matches_engine_header_id` test still guards node-vs-engine agreement (it passes unchanged). Its dependencies are the same ones `aegis-hn-types` already had, so the crate stays light.
Records that the split did NOT go into `aegis-types` as the decision assumed: that crate depends on `aegis-crypto`, so it drags in the exact Curve-Trees stack the split exists to keep out of a settler's graph. `aegis-hn-types` is genuinely light, and also carries `to_engine_block` so the node↔guest parity point has one definition rather than a settler-side copy that could drift. Scopes the four remaining pieces of the chain source, including the one design change it forces: a spend slot that can be either a trace to prove or a proof already on-chain, which the migration makes possible because `SpendProofInput` needs no witness.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
EIP-0045
verifyStarkwas redesigned (5-child → 4-child, stock RISC0 profile,ErgoStatementV1framing, raw-seal transport). This branch migrates Aegis onto it, then takes it the rest of the
way: a bridge withdrawal confirmed on-chain, and the change that makes a live withdrawal
settleable at all.
Landed:
verifyStarkTRUE on-chainRelease tx
386ebf54cf3fec9ff0f0a5f73a014e00f92fbaf9bb624a975ab30d025556df94, mined at devnetheight 797: the vault box spent through
0xB9, 150,000,000 USE released to a real P2PK, and asuccessor vault carrying exactly the R4/R5/R6/R7 the guest committed. GPU prove 894.8 s at PO2 20
(450 segments), raw seal exactly 222,668 B.
Run log:
~/apps/aegis-eip45/RUN-LOG-stage1-settlement.md. Analysis and plan:dev-docs/sidechain/settlement-host-stage1.md.Why it needed more than plumbing
The migration was already proven at the crypto level, but no release could be assembled, and the
reasons were not oversights:
SpendCircuitemittedProof<HidingEngineConfig>(uni-stark) while the settlement recursion tree verifiesBatchProof<RecursionHidingConfig>. Different proof systems; neither the preprocessedcommitment nor the witness needed to re-prove is recoverable from a submitted
Tx. A recordedwithdrawal was unsettleable by construction.
Poseidon2 id equals it. No settlement could ever start from it.
has to anchor mid-chain.
What's here
settlement/epoch-host— the production artifact builder, replacing thedump_epochmeasurement fixture on the release path (its journal described a chain that does not exist: a
fabricated
ergo_ref, a 33-byte blob where a recipient ErgoTree belongs). Splitplan/emitbecause the vault deploy happens between them: the settlement shape is note-determined, so the
deploy registers derive in milliseconds without proving. Every stage fails closed before the
expensive one — native
verify_epoch, shape reproducibility, the live vault's registers, theguest's own
verify_suffix_sharesandverify_anchor_linkage_min_depth, and the aggregationroot's surfaced digest.
Client spend proofs migrated to batch-stark (chain id v5 → v6). The load-bearing discovery is
that the verifier needs nothing from the prover:
ProverData::from_instancesforwards tofrom_airs_and_degrees, consuming only the trace degree, soCommonDatais a pure function of(config, AIR, degree). Pinned under the same fixed public preprocessed salt the uni-stark path used
for its
vk, it becomes a constant every party derives independently — soTxkeeps its shape,with no
commonon the wire, and a verifier never accepts a preprocessed-schedule commitment theprover chose. The RNG split is preserved and re-asserted: fixed preprocessed salt, fresh OS masks,
two proofs of one spend still differ.
aegis-hn-types— the hn wire types (HnBlockand friends) plusto_engine_block, split outso a settlement host can decode a block without linking the node (tokio, arkworks, the vendored
Curve-Trees stack).
to_engine_blockis the node↔guest parity point, so it now has one definitionrather than a copy that could drift. The postcard encoding is unchanged, established two ways: a
regression test round-trips a real
/hn/v1/blocksslice served before the move, and therefactored binary replayed the live chain's existing 15,504-block log with the pot chain continuous.
Two release-path defects, one root cause.
CONTEXT.headersis TIP-first —[0]is the parentof the block under validation — so the vault's anchor-index doc was inverted, and
epoch-release'spreflight compared
ergo_refagainst the oldest header while the contract splices the newest.That preflight would have passed a release the node rejects.
Verified live
On a v6 chain (fresh data dir, v5 left intact), both proved with the new circuit and verified by the
node at admission before being mined: a plain shielded tx and a real 100,000,000 USE peg-out to a
real P2PK.
Not done
The settled suffix is host-originated, not node-sourced — the chain source over live blocks is
scoped in
settlement-host-stage1.md§5 (S2.3/S2.4). Everything the on-chain verification observesis real; what is not yet real is the provenance of the suffix.
Depends on
The E4 anchor needs a genuine merge-mined Ergo block carrying the hn tip id in its extension. See
the ergo-side discussion —
mainalready ships operator-configurable custom extension fields(#199); what merge-mining additionally needs is a value that changes per block.
Gate
cargo fmt --all --check,clippy --workspace --all-targets --all-features -D warnings, andcargo testgreen across the root workspace (529), engine, engine/recursion, settlement andbridge-tools (35).