Skip to content

Deckard: Helios verified reads + walkaway, Railgun shield, signer daemon → main - #9

Merged
hellno merged 12 commits into
mainfrom
hellno/deckard-signerd
Jun 7, 2026
Merged

Deckard: Helios verified reads + walkaway, Railgun shield, signer daemon → main#9
hellno merged 12 commits into
mainfrom
hellno/deckard-signerd

Conversation

@hellno

@hellno hellno commented Jun 7, 2026

Copy link
Copy Markdown
Owner

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

  • deckard-signerd — process-isolated signer daemon + policy gate + STOP/zeroize (real security state machine; daemon_e2e/parity green).
  • Helios verified reads (R2) — proven on mainnet (cut the EL, keep verified; refuse a lying RPC). Both read paths (EthProvider + signerd) route through embedded helios-ethereum behind a default-on verified-reads feature; ReadStatus reaches the GUI.
  • Railgun shield (R1, hero) — full shield→unshield proven from our dep edge; key-less shield-calldata builder in deckard-core + daemon calldata broadcast + a black-box anvil shield_e2e. Shield is instant (no client proof); proving is on the spend.
  • Spikes (de-risk, standalone): helios-walkaway, eip1193-railgun, shield-railgun.
  • STATUS.md — single tracker (reconciled to an independent Codex audit: mechanisms built + de-risked, the recordable demo flow is the remaining work).

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.
  • Several integration tests are #[ignore] (need anvil + an archive Sepolia RPC).

hellno added 12 commits June 5, 2026 20:13
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
hellno merged commit ba22793 into main Jun 7, 2026
1 of 2 checks passed
@hellno
hellno deleted the hellno/deckard-signerd branch June 7, 2026 20:17
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant