Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/contracts-sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: contract-address sync + doc-literal gate

# Issue #251: the chain profile (crates/agentkeys-core/chain-profiles/*.json,
# .contracts[] + contract_set_version) is the single machine source of truth
# for deployed contract addresses. This cheap gate (checkout + jq + grep,
# seconds) runs scripts/check-deployed-contracts-sync.sh, which verifies:
# 1. the profile ⟷ scripts/operator-workstation.env mirror agrees, and
# 2. no tracked markdown doc re-writes a literal address the profile owns
# (docs must ANCHOR — link + resolve command — never copy; historical/
# orphaned addresses pass naturally once out of the profile).
# Deliberately a SEPARATE tiny workflow: harness-ci.yml does not trigger on
# docs/**, and running the full rust-checks job for a doc edit would be waste.

on:
push:
branches: [main]
pull_request:
paths:
- "**/*.md"
- "crates/agentkeys-core/chain-profiles/**"
- "scripts/operator-workstation.env"
- "scripts/operator-workstation.test.env"
- "scripts/check-deployed-contracts-sync.sh"
- ".github/workflows/contracts-sync.yml"

permissions:
contents: read

concurrency:
group: contracts-sync-${{ github.ref }}
cancel-in-progress: true

jobs:
check:
name: chain profile ⟷ env sync + no doc address literals
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- run: bash scripts/check-deployed-contracts-sync.sh
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ Live contract addresses on each chain plus the prod/test EVM deployer wallets ar
**Two HARD rules when any contract changes:**

1. **Idempotency is by VERSION, not bytecode.** Solidity bytecode isn't reliably comparable (embedded metadata hash + immutables), so do NOT diff bytecode. A redeploy is warranted when `crates/agentkeys-chain/VERSION` ≠ the chain profile's `contract_set_version` (or there's no on-chain code). **Bump `VERSION` when you change a contract** → the next deploy redeploys + bumps the profile's `contract_set_version`. A `VERSION` mismatch while code is already live is a **hard stop** (the script prints the mismatch + asks for an explicit opt-in — orphaning state costs mainnet gas), not an auto-redeploy. `FORCE_DEPLOY=1 heima-bring-up.sh` is a **BLIND manual override**; for the #225 account-auth cutover use [`scripts/heima-cutover-account-auth.sh`](scripts/heima-cutover-account-auth.sh) (probes the live `setScope` selector `d8e9e3c6` + skips when already live).
2. **A new deploy auto-updates the two machine mirrors; YOU update only the prose + rebuild.** `heima-bring-up.sh` writes the chain profile (`contracts[]` + `contract_set_version`) + `operator-workstation.env` automatically. You ALSO touch `docs/spec/deployed-contracts.md` **only if the design/version changed** (the version line + any ABI/cutover note — no address table to edit), and since the profile is `include_str!`-compiled, **rebuild the broker/daemon/UI** (`setup-broker-host.sh --ref main`) so they serve the new addresses. `arch.md` §5 links to the registry (no literal addresses to edit). **Confirm locally — NOT a per-PR CI workflow (CI is reserved for heavier checks):** `bash scripts/check-deployed-contracts-sync.sh` (verifies the chain profile ⟷ `operator-workstation.env`).
2. **A new deploy auto-updates the two machine mirrors; YOU update only the prose + rebuild.** `heima-bring-up.sh` writes the chain profile (`contracts[]` + `contract_set_version`) + `operator-workstation.env` automatically. You ALSO touch `docs/spec/deployed-contracts.md` **only if the design/version changed** (the version line + any ABI/cutover note — no address table to edit), and since the profile is `include_str!`-compiled, **rebuild the broker/daemon/UI** (`setup-broker-host.sh --ref main`) so they serve the new addresses. `arch.md` §5 links to the registry (no literal addresses to edit). **Confirm locally AND in CI:** `bash scripts/check-deployed-contracts-sync.sh` — verifies the chain profile ⟷ `operator-workstation.env` mirror AND (#251) that no tracked `.md` re-introduces a literal contract address a chain profile owns (docs must **anchor** to the profile — link + jq/grep resolve command — never copy; historical/orphaned addresses pass since they're no longer in the profile). CI runs it via the cheap [`.github/workflows/contracts-sync.yml`](.github/workflows/contracts-sync.yml) on PRs touching markdown / chain profiles / the env mirror.

**COMMIT + PUSH the two machine mirrors BEFORE you redeploy the broker (HARD — real #225 split-registry incident).** The broker host deploys from `origin/<branch>` and compiles the chain profile in via `include_str!`. If the freshly-rewritten `heima.json` + `operator-workstation.env` are left uncommitted (or committed-but-unpushed), `setup-broker-host.sh --ref <branch>` rebuilds the broker on the **OLD** registry while the local daemon onboards into the **NEW** one. The broker then reads `operatorMasterWallet` from the orphaned registry, builds the accept UserOp for the wrong (stale) master account, and `handleOps` reverts **`SIG_VALIDATION_FAILED`** — an accept failure that looks like a "wrong passkey" bug but is actually a split registry. Order: deploy → **commit + push `heima.json` + `operator-workstation.env`** → `setup-broker-host.sh --ref <branch>` on the host. `heima-bring-up.sh`'s step-7 guard warns loudly if you skip the commit.

Expand Down
2 changes: 1 addition & 1 deletion docs/arch.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ S3 prefixes: bots/a8fde0c5…/ (master) bots/a152e6b3…/ (hermes)

Pinned to disambiguate the same value showing up under different labels across components. **Use the canonical column** in every new doc, runbook, CLI output, and commit message; the alias column lists every spelling that exists today so a reader chasing one of them can find their way back. Per `CLAUDE.md` → "Terminology-source-of-truth rule", if you introduce a name not in this table, either add the alias row here or rename the call site to match the canonical name in the same change.

> **Deployed addresses** for every contract named here (per chain — Heima mainnet v2 set, the ERC-4337 master infra #164, historical v1) plus the prod/test EVM deployer wallets live in [`spec/deployed-contracts.md`](spec/deployed-contracts.md), the canonical address registry. Mirrored to `scripts/operator-workstation.env` for tooling. The operator-facing **wallet/contract/funding map** (key custody tiers, prod-vs-test sets side by side, the funding-flow diagram, "which wallet do I fund") is [`chain-setup.md` §Wallets](chain-setup.md#wallets-contracts--funding-map-prod--test).
> **Deployed addresses** for every contract named here live in the chain profile [`crates/agentkeys-core/chain-profiles/heima.json`](../crates/agentkeys-core/chain-profiles/heima.json) (`.contracts[]` + `contract_set_version`) — the machine source of truth (#251), mirrored to `scripts/operator-workstation.env` for shell tooling. Test-set addresses live in `scripts/operator-workstation.test.env` (authoritative; synced one-way to the `TEST_*` GitHub secrets by `scripts/ci-set-github-secrets.sh`); wallet EOAs live in the env files. The human registry — design/version/ABI/cutover prose + the full **source-of-truth hierarchy** — is [`spec/deployed-contracts.md`](spec/deployed-contracts.md). Docs **anchor** to those sources, never copy: a literal contract address in any tracked `.md` is CI-rejected by the doc-literal gate in `scripts/check-deployed-contracts-sync.sh`. The operator-facing **wallet/contract/funding map** (key custody tiers, prod-vs-test sets side by side, the funding-flow diagram, "which wallet do I fund") is [`chain-setup.md` §Wallets](chain-setup.md#wallets-contracts--funding-map-prod--test).

| Canonical name | Identity | Aliases seen in the codebase / docs |
|---|---|---|
Expand Down
2 changes: 1 addition & 1 deletion docs/chain-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ name); resolve the live value from the source of truth:
| What | Single source of truth | Resolve a value |
|---|---|---|
| Prod **contract** addresses (versioned by `contract_set_version`) | the compiled chain profile [`heima.json`](../crates/agentkeys-core/chain-profiles/heima.json) — `.contracts[]`; human registry [`spec/deployed-contracts.md`](spec/deployed-contracts.md) | `jq -r '.contracts[] \| "\(.name) \(.address)"' crates/agentkeys-core/chain-profiles/heima.json` |
| Test **contract** addresses (parallel set) | [`scripts/operator-workstation.test.env`](../scripts/operator-workstation.test.env) (`*_ADDRESS_HEIMA`) + the `TEST_*` GitHub secrets | `grep _ADDRESS_HEIMA scripts/operator-workstation.test.env` |
| Test **contract** addresses (parallel set) | [`scripts/operator-workstation.test.env`](../scripts/operator-workstation.test.env) (`*_ADDRESS_HEIMA`) — **authoritative**; the `TEST_*` GitHub secrets are the CI copy, synced one-way by [`ci-set-github-secrets.sh`](../scripts/ci-set-github-secrets.sh) (re-run after any change) | `grep _ADDRESS_HEIMA scripts/operator-workstation.test.env` |
| **Wallet** (EOA) addresses | the env files: `scripts/operator-workstation{,.test}.env` (`*_DEPLOYER_ADDR_HEIMA`, `BROKER_SPONSOR_SIGNER_ADDRESS_HEIMA`) | `grep -E '_DEPLOYER_ADDR_HEIMA\|SPONSOR_SIGNER_ADDRESS' scripts/operator-workstation*.env` |

A redeploy/rotation updates those sources (`heima-bring-up.sh` / `heima-deploy-paymaster.sh`
Expand Down
2 changes: 1 addition & 1 deletion docs/operator-runbook-account-auth-cutover.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ hand-edit the unit.
```bash
# a. write the broker's accept env to a 0600 EnvironmentFile (the secret key NEVER goes inline —
# systemctl show / /proc/<pid>/environ would leak it; resolve the VALUE, never `cat ~/.zshenv`):
PM=0xca36550d30e2E4dF927c53C3a5272A319D427602 # your paymaster — `grep PAYMASTER_ADDRESS_HEIMA scripts/operator-workstation.env`
PM=$(grep '^PAYMASTER_ADDRESS_HEIMA=' scripts/operator-workstation.env | cut -d= -f2) # resolve from the env mirror — never paste a literal (#251)
{ printf 'BROKER_SPONSOR_SIGNER_KEY=%s\n' "$(zsh -c 'print -r -- "$BROKER_SPONSOR_SIGNER_KEY"')"
printf 'PAYMASTER_ADDRESS_HEIMA=%s\n' "$PM"
} | sudo tee /etc/agentkeys/broker-sponsor.env >/dev/null
Expand Down
8 changes: 4 additions & 4 deletions docs/plan/chain/erc4337-master-account.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ Deployed + exercised the real flow end-to-end on Heima mainnet (chainId **212013

| Result | Evidence |
|---|---|
| Canonical **eth-infinitism EntryPoint v0.7** compiles for Cancun (solc 0.8.23, 11.8KB runtime) and deploys + runs | live `0x6672E1b315332167aBA12E0B1d3532a7e9B1ADE9` |
| Canonical **eth-infinitism EntryPoint v0.7** compiles for Cancun (solc 0.8.23, 11.8KB runtime) and deploys + runs | the live `EntryPoint` — address in the chain profile [`heima.json`](../../../crates/agentkeys-core/chain-profiles/heima.json) `.contracts[]` (#251: anchor, don't copy) |
| A minimal **P256Account** `validateUserOp` staticcalls the **live** P256Verifier and the UserOp executes | UserOp tx `0x17939004d3ba7a8a5451fa69b815d581486e95ee3601c4e6ee557eb5b2a7d88a`, status 1, `Counter.number()==1` |
| Works via **direct `handleOps`** (no bundler) — bundler is just automation over this | the spike used a raw `handleOps` call |
| Live P256Verifier verifies a valid vector / rejects a tampered one | `0xda5b772f9d6c09abe80414eea908612df9b54749` → `0x..01` / `0x..00` |
| Live P256Verifier verifies a valid vector / rejects a tampered one | the live `P256Verifier` (chain profile) → `0x..01` / `0x..00` |
| Full passkey UserOp gas | **730,242** (~0.018 HEI @ 25 gwei) |

Spike artifacts (mainnet): P256Account `0x8897ee99434F6c9D8711565EE59c61a03DA0Cc98` (minimal, raw-P256 — **not** the production account), Counter `0x2861D0194d9B263D42eBd956f3aD336185b27C4E` (throwaway). The EntryPoint is the exact audited v0.7 bytecode; E1 decides adopt-this vs redeploy-at-a-deterministic-address.
Expand Down Expand Up @@ -80,8 +80,8 @@ Each phase is independently shippable, idempotent where it mutates chain state (
- **E0** ✅ threat-model drafted → [`erc4337-threat-model.md`](erc4337-threat-model.md).
- **E2** ✅ `IERC4337.sol`, `P256Account.sol`, `P256AccountFactory.sol` — **codex-reviewed** (1 P2 fixed: verifier/`abi.decode` reverts now map to `SIG_VALIDATION_FAILED` via a try/catch self-call); **17 account tests green**.
- **E1** ✅ **deployed live on Heima mainnet 2026-06-02** (recorded in [`deployed-contracts.md`](../../spec/deployed-contracts.md)):
- `EntryPoint` v0.7 = `0x6672E1b315332167aBA12E0B1d3532a7e9B1ADE9` (canonical bytecode; landed a UserOp in the spike).
- `P256AccountFactory` = `0x1ccCe65b22De81aDA4F378FeAf7503d93f5d27a3` (CREATE2 determinism smoke-verified on mainnet: `getAddress` == `createAccount`).
- `EntryPoint` v0.7 (canonical bytecode; landed a UserOp in the spike) — address in the chain profile [`heima.json`](../../../crates/agentkeys-core/chain-profiles/heima.json) `.contracts[]` (#251: anchor, don't copy).
- `P256AccountFactory` (CREATE2 determinism smoke-verified on mainnet: `getAddress` == `createAccount`) — address likewise in the chain profile.
- **E3** ✅ `AgentKeysScope` thinned to account-auth (in-contract K11 + `scopeNonce` retired; `setScopeWithWebauthn`→`setScope`); registry agent-bind closed structurally (master = account).
- **E4** ✅ folded into E3 (agent bind/revoke passkey-gated structurally, no new code).
- **E5** ✅ guardian **M-of-N social recovery** in `P256Account` (generation-rotation; independent of the lost primary passkey, per threat-model §7); **+6 tests**.
Expand Down
Loading
Loading