Deckard: Helios verified reads + walkaway, Railgun shield, signer daemon → main - #9
Merged
Conversation
Freeze the shared wire so the agent surface, custody/daemon, and test harness can build in parallel before the real signer daemon exists. New crate `crates/deckard-contract` (std + serde, zero key material): - Frozen types: Intent (carries chain_id; daemon owns the nonce), IntentKind, Decision, Policy, ApprovalMode, and the SignerRequest/ SignerResponse/ExecuteResult/ApprovalStatus/BalanceReport RPC enums. - A sync `Signer` trait + an in-memory `MockSigner`: deterministic and pinned (address 0x11.., tx_hash 0xAB.., request_ids 0x01,0x02,..), implementing the full policy decision matrix incl. the TOCTOU revoke guard at execute time. `Box<dyn Signer>` works. - Tests: serde_json + ciborium (CBOR) byte-stable round-trip per type; the decision matrix; and the daemon-free slice of 30-mcp-shape T1-T8. - Normal deps are only alloy-primitives + serde; ciborium/serde_json are dev-deps. id minting panics loudly on >255 proposals instead of wrapping. Workspace: root becomes a virtual manifest (members deckard-app/-core/ -contract, default-members deckard-app so `cargo run` still launches the GUI; [profile.release] + [workspace.dependencies] at the root). The app moves to `crates/deckard-app` via `git mv` (binary stays `deckard`). justfile + CI build/test `--workspace`; `just bundle` bundles the prebuilt icon.icns from crates/deckard-app/assets (cargo-bundle 0.11 can't convert the 1024px png, and resolves icon paths against the CWD).
These were authored via the /spec process but were only ever untracked working-tree files. deckard-contract's README and crate docs point at docs/build/30-mcp-shape.md as the contract's owner, so land the set the links resolve to: - docs/build/ — the parallelizable v1 build specs (30-mcp-shape owns the Intent/Decision/daemon-socket contract; 00/10/20 reference it). - docs/research/ — the research KB + v1-demo-plan the build specs cite. Additive markdown only. If another PR already owns these, drop this commit.
Pin the real Helios API against a16z/helios@0.11.1 (depend on helios-ethereum, git-only; alloy unifies to one alloy-primitives 1.6.0), prove the walkaway beat on mainnet, and rewrite 20-helios-sidecar.md with measured numbers, the failover design, provider/privacy findings, and an app-integration section. spikes/helios-walkaway/ (standalone crate) embeds Helios, serves a verified mainnet balance, and headless-PASSes two scenarios: - availability: cut the primary EL on camera, keep serving verified reads via a second EL (the cut primary's own head still returns from the CL cache, proving head is EL-independent) - integrity: point at a lying RPC (tampered eth_getProof balance) and refuse the read (invalid account proof) where a centralized wallet would display the lie Measured (M-series, mainnet): cold ~11s, warm ~2s, cut->failover <=1 block. CL finding: only Nimbus + dRPC actually drive a Helios sync (Lodestar/PublicNode return 200 but fail). Decisions: Kurtosis deferred off the v1 critical path; public CLs for the hero. Cross-doc notes added to 00/30/README. Reviewed by a Codex adversarial pass: all API claims confirmed against source; overclaims scoped and two spike bugs fixed (fake EL-independent head proof; lie scenario now asserts a proof rejection specifically).
…less #3) Self-contained brief a parallel agent can run to prove/disprove that Helios's localhost JSON-RPC server satisfies Kohaku railgun's IntoEip1193Provider and serves every method the read/sync path calls. Standalone, read-only, does not touch the app crates.
…icy gate (#4) The operator spine: a separate process owns the decrypted key, runs the real policy gate, signs + broadcasts Sends, and answers STOP. The app (and the future deckard-mcp) become key-less clients over a same-uid UDS. Contract (extends the frozen wire, in lockstep with MockSigner + 30-mcp-shape.md): - SignerRequest += Unlock{passphrase}/Lock/Resolve{request_id,approved}; SignerResponse += Unlock(UnlockOutcome{Unlocked{address}|BadPassphrase|NoVault}). - New pure policy::evaluate(&Intent,&Policy)->Decision — the ONE decision function; MockSigner now calls it (no duplicated logic; parity is unit-asserted). deckard-signerd (AGPL-3.0-or-later), lib + bin: - UDS server: length-delimited CBOR frames (4-byte BE len, max 1 MiB); SO_PEERCRED/ LOCAL_PEERCRED same-uid auth; socket 0600 in a 0700 dir; single-instance flock. - Locked<->Unlocked{vault} state machine over deckard-core's keystore (reused, not rebuilt). Unlock decrypts + holds the key (passphrase moved into Zeroizing, frame scrubbed, never logged); Lock/RevokeAll zeroize -> Locked + deny in-flight; re-unlock re-arms with a clean session. - Send-only propose/execute: process pre-checks (locked/chain_mismatch/unsupported_v1/ erc20) then evaluate; EIP-1559 sign + broadcast via config RPC; deterministic keccak(intent) request id so an Allow is executable; Resolve approval loop; TTL. - TOCTOU + spend guards at sign time: STOP denies a pre-approved request; an auto-allow is re-capped against current spend (daily cap can't be bypassed by batching); a human-approved overage is honored; Pending AND Allowed expire; broadcast is bounded by a timeout so a hung RPC can't wedge the daemon. Re-propose is idempotent. - Signer version bridge: extract the version-stable B256 scalar from core's alloy-signer-local 2.0.5 signer and reconstruct it in the daemon's 1.8.3 alloy stack. App: spawns + supervises the daemon (restart-on-crash, kill-on-exit), unlocks OVER the socket (no in-process UnlockedVault/PrivateKeySigner for signing); onboarding still seals + writes the vault, then unlocks via the daemon. Shared config dir resolver in deckard-core so app + daemon never drift. Tests (75 workspace; anvil broadcast tests skip if foundry absent, run in CI): unlock outcomes, propose matrix, off-allowlist, resolve/TTL, STOP zeroize + re-arm, TOCTOU, socket perms, mock<->evaluate parity, peer-cred check, passphrase Zeroizing, + anvil-fork sign/broadcast/receipt and the daily-cap-at-execute regression. Closes #4
…gun's EIP-1193 provider (T-Trustless #3) Proves the v1 seam end to end (against kohaku@618c53f): Helios's localhost JSON-RPC server (.rpc_address) is the EIP-1193 provider Railgun reads through. - IntoEip1193Provider is Kohaku's OWN 7-method trait (eip-1193-provider crate); an alloy DynProvider satisfies it via Kohaku's shipped Alloy adapter — no custom adapter for v1. Read/sync path = exactly eth_blockNumber + eth_getLogs + eth_call, all in Helios's served set. - One required fix: alloy's Provider::call defaults to the `pending` tag, which Helios (light client) can't serve; build the provider with ProviderBuilder::with_default_block(BlockId::latest()). One line, Deckard-side. - Tier-2 (default) drives the adapter + logs methods via a pass-through proxy; Tier-1 (--features railgun) links the full railgun ZK crate and drives the real RpcSyncer/RailgunBuilder through Helios (414 SyncEvents). railgun compiles standalone (retires 10's R1c). Loopback hop ~0.3 ms/call (release). Findings written into 20-helios-sidecar.md (Integration + Measured + open-questions).
…erd (#1+#2) Route both raw-RPC read paths through an embedded Helios light client and tag every read with a trust label. #1 deckard-contract: new ReadStatus { Verified | Degraded{reason} | Unsynced{reason} } + a read_status field on BalanceReport (round-trips JSON + CBOR). #2 deckard-core: new helios.rs launcher (Helios localhost JSON-RPC server + the required with_default_block(latest) consumer-provider fix); EthProvider reads now go through it and return Read<T>{ value, status }. deckard-signerd: read_balance + the daemon Balance handler route through Helios the same way. App status line surfaces the tag (verified / NOT VERIFIED). The heavy helios-ethereum dep is gated behind a default-on `verified-reads` feature with an honest raw-RPC fallback: never claims Verified without a fresh Helios-backed read (locked / Helios-down / feature-off all map to Unsynced; removed the old unwrap_or(ZERO)-as-truth). v1 runs two independent Helios instances (app + daemon); the failover supervisor and reads-consolidation are deferred (// TODO post-v1). Adversarial review (Codex invoked + manual pass) fixed one P1: the daemon held its global mutex across the multi-second Helios bootstrap, which could block the STOP/Lock brake — Helios moved into an off-lock HeliosCell. Plus 3 P2 honesty fixes (timestamp-based head freshness vs 60s, value-then-status ordering, no_std doc). Verified green with the real cargo (rtk cache bypassed): build verified-reads ON+OFF, deckard-app compiles; tests — signerd 15 lib + 9 daemon_e2e + 1 parity + 3 anvil_e2e (STOP/zeroize + TOCTOU intact), contract 32, core 13.
Standalone spike (spikes/shield-railgun/) ports kohaku's transact_utxo.rs (rev 618c53f) against an anvil Sepolia fork @ 10822990 through a plain alloy provider, and runs the full shield→sync→balance→transfer→unshield with the EXACT upstream numeric asserts — all GREEN from our own dep edge: shield 997_500 · shield_native 1_097_250 · transfer 1_092_250/5_000 · unshield 1_091_250/5_000 + EOA WETH 998. (independently re-run, 1 passed, 32.8s) R1d answered (the "is instant auto-shield honest?" question): YES, and for the right reason. Railgun SHIELD does NO client ZK proof — ~2–13 ms (note-encrypt + ABI-encode); the contract verifies the commitment on-chain. Groth16 proving lives entirely on the SPEND: transfer ~11.7s / unshield ~9.6s cold-debug, ~halved by the `parallel` feature (~5.4 / ~4.3s). So "instant shield, prove-on-spend" is a faithful UX; the slow path (unshield) is off-camera fast-follow. Retires R1c (railgun standalone-consumable — full crate links + runs from our edge). Gotchas captured: the `railgun/testing` feature is required for external consumers (gates SubsquidSyncer::with_latest_block); `.with_poi()` constructs but credits None on a local fork (defaults to no-POI, WITH_POI=1 opt-in). Adversarial review (Codex + manual) caught + fixed a P1: two SHIELD timers were mislabeled "proof build" when shields do no proving — relabeled, R1d framing corrected. Asserts verified genuine (hard assert_eq! vs upstream constants, not weakened). Doc 10-kohaku-shield.md updated (status + R1d/proving resolved).
…box anvil test (#1) Beat-2 shield is now a first-class write in Deckard's own path. #1a deckard-core: `shield::build_shield_native_intent(chain_id, recipient, value) -> Intent{kind:Shield, to, value, calldata}` — KEY-LESS (a deposit needs only the recipient 0zk address, no spending key), behind a default-on `shield` Cargo feature. Feature-off compiles without railgun and returns an honest "shield unavailable" error. #1b deckard-signerd: `broadcast_intent` carries calldata (Shield/ContractCall); the execute path broadcasts Intent calldata+value+to. Native sends stay byte-identical (broadcast_native_send is now a thin &[] wrapper). The daemon stays ZK-free — it only signs+broadcasts the bytes it's handed. #1c crates/deckard-signerd/tests/shield_e2e.rs: a repeatable BLACK-BOX integration test (#[ignore], fresh anvil Sepolia fork @ 10822990) that drives Deckard's path end to end (core builder -> daemon propose/execute -> on-chain -> railgun sync) and hard-asserts the privacy property: private +997_500 (exact 25bps on 1_000_000), public down by value+gas. shield-only (fast, no proving/artifacts). Adversarial review (Codex + manual) fixed two P1s: - signerd `shield` feature was leaky (deckard-core dep lacked default-features=false, so the off-switch didn't drop railgun) -> fixed; railgun now absent from signerd's normal tree when off. - SECURITY: `Intent{kind:Shield, calldata: empty}` would have broadcast as a plain native send to an arbitrary `to` under the "Shield" label -> `calldata_ok` now requires non-empty calldata for Shield/Unshield/ContractCall (Decision::Deny{undecodable}); daemon + contract tests cover it. vendor/eip-1193-provider: a native-only fork of kohaku's eip-1193-provider (rev 618c53f, verbatim .rs, `js` dropped) + a workspace [patch]. Required because upstream's `js` feature pins wasm-bindgen=0.2.108 exact while the GPUI app's web-sys pins 0.2.122 exact — irreconcilable in one workspace; `js` is wasm32-only and unused. Maintenance + license (no upstream license field, like railgun) to revisit before ship. Verified GREEN with real cargo (pinned 1.95.0), rtk cache bypassed: build default + feature-off (railgun absent) + whole workspace incl. GPUI app; daemon_e2e 9/9 + parity 1/1 (STOP/zeroize + TOCTOU + Shield-Allow/empty-Shield-Deny intact); deckard-contract 32+1; shield_e2e re-run by me on a fresh fork (private +997500). Deferred (// TODO post-v1): receive-watcher, MCP, railgun key-derivation for balance-display, production HeliosEip1193, Unshield/ContractCall (Deny v1).
…faces Tracking was scattered across 5 mostly-stale surfaces (root README "Status — v0", docs/build/README "spec ✓" table, specs/SPEC-v0-epic "pre-implementation", specs/HANDOFF "Built so far: src/wallet.rs…", roadmap). Consolidate: - NEW STATUS.md (repo root) = single source of truth: v1 demo beats, crates/tracks, spikes, v0 base, open risks — each with status + commit refs. - root README + docs/build/README: status sections now point to STATUS.md and are corrected to current reality (keystore/daemon/verified-reads/shield built, not "plaintext EOA / spec"). - docs/build/README: dropped the drifting per-doc "spec ✓" column (now a spec index), fixed the stale hero-spikes section (R1 proving measured = instant; R2 reads integrated). - specs/SPEC-v0-epic + HANDOFF: one-line "live status → /STATUS.md, fields below are stale" banners so they stop competing as trackers. No code change.
…e hero Independent Codex audit (read-only, high reasoning) over all 4 crates + tests found no lies/stubs/spike-confusion, but that STATUS oversold 'done + tested' for the hero: - shield mechanism is built (core builder + daemon broadcast, confirmed) but shield_e2e is #[ignore] (not default cargo test) and shield is NOT reachable from the app or an agent (no trigger/watcher/MCP) -> downgraded to 🟡. - added a top 'reality check': mechanisms built + security state machine real, but the recordable demo FLOW (receive->shield->walkaway, agent-driven) is unwired end-to-end. - added per-table test caveats (shield_e2e ignored; anvil_e2e skips w/o anvil; core reads mocked; app send uses a fake daemon; STOP/gate tests are real).
The mechanisms are de-risked + built; the gap is reachability + visible state. A) shield on screen (trigger + shielded-balance view — the one new build) B) in-app walkaway (cut control + ReadStatus badge flip) C) receive landing (balance refresh) D) agent spine (manual stand-in or deckard-mcp) E) one continuous take + polish. Critical path: A+B+C, D as narration.
hellno
pushed a commit
that referenced
this pull request
Jun 14, 2026
Second decision round (2026-06-14): - Ambition: harvest aligned attributes, do not climb the Stage ladder - Hardware-wallet signing (#4): out of scope (deliberate non-goal) - Browser integration (#25): deferred; MCP agent is the sole connection surface; app isolation (#11) reframed as per-agent/session isolation - Account abstraction (#26): EIP-7702 research spike (on-chain agent policy), batching (#27) contingent on it - Multi-identity (#8/#9): a post-v0 goal with non-correlation UX Rewrites the Stages section as reference-only.
hellno
pushed a commit
that referenced
this pull request
Jun 14, 2026
Validate the code-fit layer from a four-cluster read of the actual codebase: per-item crate placement, seam-vs-expansion, size, and risk, plus an EIP-7702 spike brief. Key findings: - deckard-core already has the right seams (multi-account derivation, reserved enclave flag, alloy/7702-ready builder-agnostic broadcast) - Un-gating Send is UI-only (daemon path tested) and the dependency hub - deckard-contract (frozen) is the higher-ceremony expansion point; batch RevokeApproval/Intent::Batch/session-token changes together - Re-tier #13 RPC-before-first-request UP to L (provider spawns with DEFAULT_RPC before the auth gate); re-tier #8/#9 multi-identity DOWN (core already derives accounts) Reworks the sequencing tiers with the measured sizes.
hellno
pushed a commit
that referenced
this pull request
Jun 14, 2026
Second decision round (2026-06-14): - Ambition: harvest aligned attributes, do not climb the Stage ladder - Hardware-wallet signing (#4): out of scope (deliberate non-goal) - Browser integration (#25): deferred; MCP agent is the sole connection surface; app isolation (#11) reframed as per-agent/session isolation - Account abstraction (#26): EIP-7702 research spike (on-chain agent policy), batching (#27) contingent on it - Multi-identity (#8/#9): a post-v0 goal with non-correlation UX Rewrites the Stages section as reference-only.
hellno
pushed a commit
that referenced
this pull request
Jun 14, 2026
Validate the code-fit layer from a four-cluster read of the actual codebase: per-item crate placement, seam-vs-expansion, size, and risk, plus an EIP-7702 spike brief. Key findings: - deckard-core already has the right seams (multi-account derivation, reserved enclave flag, alloy/7702-ready builder-agnostic broadcast) - Un-gating Send is UI-only (daemon path tested) and the dependency hub - deckard-contract (frozen) is the higher-ceremony expansion point; batch RevokeApproval/Intent::Batch/session-token changes together - Re-tier #13 RPC-before-first-request UP to L (provider spawns with DEFAULT_RPC before the auth gate); re-tier #8/#9 multi-identity DOWN (core already derives accounts) Reworks the sequencing tiers with the measured sizes.
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.
Brings the full current state to
main(this branch is the tip of the stacked work; supersedes the stacked PR #6 → hellno/deckard-contract).What's in here
EthProvider+ signerd) route through embeddedhelios-ethereumbehind a default-onverified-readsfeature;ReadStatusreaches the GUI.deckard-core+ daemon calldata broadcast + a black-box anvilshield_e2e. Shield is instant (no client proof); proving is on the spend.helios-walkaway,eip1193-railgun,shield-railgun.Honest state
Mechanisms are built + de-risked; the demo flow (receive → auto-shield → walkaway, agent-driven) is not yet wired end-to-end (no receive-watcher / MCP trigger / in-app live cut). See
STATUS.md"Remaining for a minimum recordable demo".Notes
vendor/eip-1193-provider(native-only fork) dodges a wasm-bindgen exact-pin conflict; license to confirm before ship.#[ignore](need anvil + an archive Sepolia RPC).