Skip to content

feat(bridge): derive, keep and recover private USDCx mint nonces (veil #166 port) - #85

Open
iamalwaysuncomfortable wants to merge 8 commits into
masterfrom
feat/private-mint-identities
Open

iamalwaysuncomfortable wants to merge 8 commits into
masterfrom
feat/private-mint-identities

Conversation

@iamalwaysuncomfortable

Copy link
Copy Markdown
Member

Python port of Veil PR ProvableHQ/veil#166 (with the #208 follow-up fixes folded in): a private xReserve USDC → USDCx deposit from the bridge's own Aleo account derives, keeps and recovers its secret nonce the way the Shield wallet does, so quote → execute → wait → complete needs no secret_nonce anywhere. An explicit secret_nonce= still wins on every verb and touches no store.

What it adds

aleo_bridge.private_mint — Shield's shielded-usdcx-secret-nonce derivation (Poseidon8.hash_to_scalar([wrapper_x, network_domain, view_key, counter]), BHP256 recipient commitment), verified byte-exact against the six shield-core vectors vendored from Veil (tests/fixtures/shield-core-private-mint.json, with _source). PrivateMintIdentity (its repr never shows the scalar), a three-method PrivateMintIdentityStore protocol, FilePrivateMintIdentityStore (atomic 0600 writes, a .counter file consumed before derivation so a failed derivation or save never recycles a counter, and a .lock inode via the journal's existing cross-process flock) and MemoryPrivateMintIdentityStore (explicit opt-in only). reserve_identity, peek_secret_nonce, find_identity, and recover_identity, which re-derives counters from zero when the store lacks a commitment, so losing the identities file costs a scan (about one second at the default 10,000-counter limit), not the funds. One resolver, resolve_secret_nonce, holds the whole policy and every verb calls it.

Bridge(private_mint_identities=…) — optional. When a FileCheckpointStore is bound (from_profile, or BRIDGE_CHECKPOINT_DIR), the default is a hidden .private-mint-identities.json inside the journal directory, which the journal's *.json listing already skips. No store and no nonce is a ConfigurationError naming both fixes, never a quiet 0scalar on a funds-moving path.

Lifecycle. execute reserves an identity right before the deposit (not at quote time, so price refreshes cost nothing and Plan and the checkpoint format are unchanged); resume reads the commitment back out of the checkpointed hook data and complete out of the attested payload, look it up or re-derive it, and refuse before any RPC on a malformed hook or a stored identity naming another recipient. A private quote stays read-only: it previews the next counter's nonce when it can and prices with a placeholder otherwise; execute is the gate. Tier-2 xreserve.private_mint(att, recipient) resolves the same way; eth.deposit_usdc keeps its explicit scalar parameter.

Agent tools / MCP. secret_nonce is optional on quote/execute/resume/complete; the pre-checks accept a derivable bridge; how_to_fix names both fixes.

Docs. README private-recipient section rewritten caller-first (derived default, then "bring your own nonce"), recovery notes, the examples (the private-recipient and journal-recovery scripts treat BRIDGE_MINT_SECRET_NONCE as an override), and the generated agent guide.

Design calls

  • Derive only when the recipient is the bridge's own account (the identity is one the recipient's wallet can reproduce); any other recipient needs a supplied nonce.
  • Not ported: the browser-wallet walletPreparedDeposit / privateMintNonceInput path (no Aleo wallet adapter in Python), Veil's circle option object, the external-commitment path, and lock-directory / dead-process reclamation (one flock instead). The #208 corrections are satisfied by design: no in-memory default store, no stale-lock races, reservation immediately before the deposit.
  • Backward compatibility: every public surface is additive. Wire formats (Plan, Receipt, Checkpoint, Progress) are unchanged; new Bridge arguments are trailing keywords; checkpoint._counter_lock / FileCheckpointStore._write keep their names over the shared _fs helpers. quote/private_mint defaults changed from "0scalar" to None, and a deposit made under eth.deposit_usdc's unchanged 0scalar default still completes without a nonce (the lookup recognises that commitment first), so pre-existing Tier-2 pairings keep working.

Validation

  • bridge-sdk: 1265 hermetic tests pass (-m "not slow"), pyright strict 0 errors, codegen/gen_context.py --check clean. New: test_private_mint_derivation.py (KATs, domains, malformed hooks, redacted repr), test_private_mint_store.py (monotonic counters, 0600 + atomic files, counter survives losing the identities file, consumed before a failed derivation, two instances on one path, 8-thread concurrency, peek, scan recovery and its limit, recipient mismatch, malformed files raise instead of resetting, scan speed), test_private_mint_resolver.py, test_private_mint_lifecycle.py (execute/quote/resume/complete/Tier-2 incl. the 0scalar compatibility case); agent tests updated.
  • Live on testnet: new test_testnet_evm_xreserve_deposit_derived beside the explicit-nonce case — 3 Sepolia USDC deposited with no secret_nonce, the executing client dropped, a new client recovered the checkpoint from disk, Circle attested, private_mint completed with the derived nonce and delivered (passed in 21m29s on 2026-10-29; .private-mint-identities.json + .counter created at 0600 in the live journal directory).

This branch has not been deployed

No deployments
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