Skip to content

MagicBlock ephemeral-rollup credit metering + x402-over-ER - #93

Open
mizuki0x wants to merge 32 commits into
mainfrom
feat/magicblock-er
Open

MagicBlock ephemeral-rollup credit metering + x402-over-ER#93
mizuki0x wants to merge 32 commits into
mainfrom
feat/magicblock-er

Conversation

@mizuki0x

@mizuki0x mizuki0x commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Adds a MagicBlock Ephemeral Rollup settlement path: agents meter and pay per call by running consume_credits in an ER (gasless, ~ms), with the credit balance reconciling exactly to L1. Token custody, staking, slashing, and governance stay on mainnet; only the program-owned u64 credit balance delegates to the rollup.

What's here

On-chain (settlement program)

  • delegate_credits / commit_credits / undelegate_credits plus the #[ephemeral] undelegation callback, behind a new ephemeral feature.
  • Shipped as an isolated crate programs/settlement-ephemeral that include!s the settlement source with the feature on. It is excluded from the workspace so its ephemeral-rollups-sdk graph (which pins the solana runtime to 2.2.20) resolves in its own lock and never perturbs the litesvm 0.6 test deps the rest of the workspace pins to 2.2.1. The L1-only program and its tests are unchanged.

x402-over-ER (the agent-payment path)

  • covenant-x402: EphemeralSigner implements the Signer trait by metering. build_payment submits a consume_credits to the pinned ER validator and returns the signature as the x402 proof; receipt_hash = sha256(nonce) binds each payment to its 402 challenge. Drops into the daemon's pay_and_record() unchanged.
  • covenant-x402-facilitator: the verifier side. Gates a paid endpoint and verifies the ER consume (program, amount >= price, receipt_hash == sha256(nonce), single-use nonce) by reading the on-chain tx, not the client envelope. No solana-sdk, no on-chain SDK.

Three gotchas worth knowing

  1. ephemeral-rollups-sdk with default-features = false breaks the SDK build (utils.rs references solana_system_interface, which sits behind the default feature). Keep defaults and add anchor-compat.
  2. anchor-compat lets anchor-lang split into 0.31.1 + 0.32.1 under a fresh lock; pin with cargo update -p anchor-lang --precise 0.31.1.
  3. The ER SDK forces the solana runtime crates to 2.2.20, incompatible with litesvm 0.6 (2.2.1, pre the solana-feature-set -> agave-feature-set rename). Solved by the isolated-crate split above.

Verification (devnet)

Live on devnet (program cov9UDyp..., EU ER validator), not just compiled or mocked:

  • Credit metering: delegate -> N x consume in ER -> undelegate -> exact L1 reconciliation. N=100: 75.5 ms/op, ~0.0003 SOL for the whole session (roughly constant in N, since the consumes are gasless).
  • x402-over-ER, full Rust loop: real Client + EphemeralSigner -> facilitator -> ER: 402 -> ER consume -> 200, balance -2 exactly, reconciled to L1.
  • Mixed-account txs (writable set spanning a delegated and a non-delegated account) are rejected on router/ER/L1, never split.
  • Full daemon path: a PayX402 op drives op_respond -> ER signer sidecar -> consume_credits in the ER -> the facilitator -> 200, with a settlement receipt recorded. Covered by pay_x402_settles_through_the_er_signer_live (#[ignore], green on devnet).

Tests: covenant-x402 +4 (incl. a wiremock integration test), 59 existing still green; covenant-x402-facilitator 5; settlement litesvm suite unchanged.

Daemon integration (landed + live-verified)

The full loop is wired into covenantd:

  • covenant-x402-er-signer sidecar (in covenant-x402, solana feature): stdin PaymentRequirements -> consume_credits in the ER via EphemeralSigner -> stdout x-payment header.
  • X402Config::signer_for(network) routes solana-er:* providers to the ER sidecar (env COVENANT_X402_ER_SIGNER_BINARY + COVENANT_X402_ER_{KEYPAIR,PROGRAM,RPC}); everything else uses the default SPL signer. The funding key stays in the sidecar's address space, never the daemon.
  • covenantd x402 tests 12/12 green including the selection test.
  • Provider registration: operators register ER-settled providers via COVENANT_X402_ER_PROVIDERS (a JSON array); each is advertised to agents as an er.<slug> tool and routes through the same x402 pay path (gated by tool.call.er.<slug>, recording budget/receipt/audit).
  • Both daemon paths are live-verified on devnet (#[ignore] tests): pay_x402_settles_through_the_er_signer_live (raw op) and er_provider_tool_settles_in_the_er_live (an agent calling a registered er.* tool).

mizuki0x added 16 commits June 16, 2026 20:34
new isolated crate programs/settlement-ephemeral (excluded from the workspace so
its solana 2.2.20 / ephemeral-rollups-sdk lock stays off the litesvm 2.2.1 test
deps) include!s the settlement source with the ephemeral feature on, adding
delegate_credits / commit_credits / undelegate_credits and the #[ephemeral]
undelegation callback. credit-account u64 balance delegates to the ER; token
custody, staking, slashing and governance stay on l1.

l1-only build and full litesvm suite unchanged. anchor-compat pinned to a single
anchor-lang 0.31.1. devnet spike harness + runbook under spike/.
manual-encoded client (anchor discriminators + the exact #[delegate]/#[commit]
account order, magicblock js sdk only for the delegation pdas) drives delegate
(l1) -> N x consume (eu er validator) -> undelegate -> reconcile (l1) against the
existing cov9UDyp devnet deployment. ran clean: exact reconciliation at N=5 and
N=100, 75.5 ms/op, ~0.0003 sol fixed session cost (consumes gasless). check.mjs
dumps config/credit state.
x402-demo.mjs: a paid http endpoint settled by gasless consume_credits in the er
instead of an spl transfer. real 402 boundary + a verifying facilitator that checks
the er signature (program, credit account, amount, receipt_hash == sha256(nonce),
anti-replay). delegate once -> K gasless paid calls -> undelegate -> reconcile. ran
live on devnet: 402 -> er pay -> 200 at ~71ms/call steady-state, exact l1 reconcile.

production path noted in README: same settlement as an EphemeralSigner Signer impl
inside covenant-x402 (build_payment runs the er consume) so pay_and_record is unchanged.
experiment-mixed.mjs: a tx spanning a delegated account + a real L1 system
transfer is rejected on router/ER/L1, never split or partially applied; on L1 the
delegated account fails with AccountOwnedByWrongProgram. confirms delegated and
non-delegated writes must be separate txs (our design already does this).
implements the Signer trait by metering: builds + submits consume_credits to the
pinned ER validator and returns the tx signature as the x402 proof, instead of an
spl transfer. receipt_hash = sha256(nonce) binds the payment to the 402 challenge
(nonce added to PaymentExtra). gated by the solana feature, no on-chain ER sdk dep
(just builds a tx + json-rpc to the ER), so no lock churn. the daemon's
pay_and_record path works unchanged. 4 tests incl a wiremock integration test;
existing 59 still green. the consume it emits is byte-identical to what ran live
in the node x402 demo. facilitator that accepts ER proofs is the remaining piece.
examples/ephemeral_live.rs drives the signer against a real ER validator;
spike/er-session.mjs does delegate/undelegate/balance. ran it: signer submitted a
consume_credits to devnet-eu, confirmed, returned the x402 envelope, ER balance
dropped by exactly the amount, reconciled to L1 on undelegate. closes the
verification gap (was unit + wiremock only).
new crate gating a paid endpoint on ER-settled payments. GET /paid -> 402 with a
single-use nonce (shaped as covenant-x402 PaymentRequirements so its Client parses
it); with x-payment, fetches the consume on the ER and checks program, amount >=
price, receipt_hash == sha256(nonce), and nonce single-use (anti-replay). reads the
credit account from the on-chain tx, not the envelope. no solana-sdk / no on-chain
sdk (json-rpc + base58), zero lock impact. 5 verifier tests.

live-verified the FULL rust loop on devnet: real covenant-x402 Client +
EphemeralSigner -> this facilitator -> EU ER validator: 402 -> ER consume -> 200,
balance -2 exactly, reconciled to l1 (examples/live_loop.rs).
- covenant-x402-er-signer sidecar: stdin PaymentRequirements -> consume_credits in
  the ER via EphemeralSigner -> stdout x-payment header (matches SubprocessSigner).
- X402Config gains an optional er_signer_binary/er_signer_env + signer_for(network):
  solana-er:* networks route to the ER sidecar, everything else to the default SPL
  signer. funding key stays in the sidecar, never the daemon.
- dispatch call site uses config.signer_for(&network); main reads
  COVENANT_X402_ER_SIGNER_BINARY + COVENANT_X402_ER_{KEYPAIR,PROGRAM,RPC}.
- PaymentExtra gained nonce (for exact-er); fixed literals in hyre + zauth.

covenantd x402 tests 12/12 green incl. the selection test; hyre/zauth build.
pay_x402_settles_through_the_er_signer_live (#[ignore], runs on devnet): drives a
real PayX402 op through op_respond -> pay_x402 -> X402Config::signer_for -> the ER
signer sidecar subprocess -> consume_credits in the ER -> the facilitator -> 200,
and asserts a settlement receipt is recorded. closes the last unverified link
(daemon SubprocessSigner <-> er-signer <-> live ER). passed green on devnet.

verified the whole chain end to end as an outsider would: documented build cmds,
spike + x402 node demo (exact reconcile), the rust live_loop, cargo build
--workspace, and this daemon test. all green.
operators register ER providers via COVENANT_X402_ER_PROVIDERS (json array of
{slug, endpoint, per_call_cap, ...}); each is advertised to agents as an er.<slug>
tool. CallTool er.<slug> routes through call_tool -> er_tool_call -> the x402 pay
path, where the solana-er:* network selects the ER signer sidecar; budget debit,
settlement receipt, audit, and the tool.call.<name> capability are all reused.

new er_provider module (registry + tool specs + resolve), Server.er_providers +
with_er_providers builder, list_tools/call_tool wiring, env loader in main. 2 unit
tests + a live #[ignore] test (er_provider_tool_settles_in_the_er_live) green on
devnet: agent calls registered er.quote -> ER consume -> facilitator -> 200 +
receipt. workspace build green.
Dockerfile.facilitator (multi-stage rust build of covenant-x402-facilitator) +
render.yaml web service covenant-x402-er-facilitator (docker, frankfurt, devnet
PROGRAM/ER/PRICE, healthCheckPath /health). add /health route to the facilitator
and a repo .dockerignore so build contexts stay lean. validated: docker image
builds, /health -> 200, /paid -> 402 challenge.
remote-check.mjs pays a deployed x402 ER facilitator end to end (GET /paid -> 402
-> consume_credits in the ER -> retry with x-payment -> 200); uses sha256(nonce
string) to match the rust facilitator. ran green against the live service
https://covenant-x402-er-facilitator.onrender.com. README documents the deployed
URL + how to verify.
issued nonces now carry an issue time, are TTL-evicted (300s) on every request,
and capped at MAX_OUTSTANDING_NONCES (503 over the cap) so unpaid challenges can't
grow memory without bound. expired nonces fail closed on redemption. +unit test.
OPERATING.md: facilitator -> daemon env (er signer + COVENANT_X402_ER_PROVIDERS) ->
grant tool.call.er.<slug> -> delegate the credit account -> agent calls er.<slug> ->
settles in the ER -> top-up. documents the live deployed facilitator + how to verify,
and surfaces the delegation-lifecycle automation decision (manual/keeper/daemon).
hitting the bare domain (incl. the custom domain) was a 404 because only /health
and /paid had routes. add a root handler returning a small json descriptor (service,
endpoints, program, network, price) so the root shows what it is.
@mizuki0x
mizuki0x force-pushed the feat/magicblock-er branch from 0a519ba to 5937d29 Compare June 23, 2026 20:11
mizuki0x added 13 commits June 23, 2026 23:52
…(idle/lifetime/stall undelegate, TEE-capable)
…t (folded per consume) + migrate_credit_account for the upgrade path
…nance-chained actions -> slash citing the provenance root)
…s on-chain provenance_root, read from the seed-bound credit account (no caller-supplied reason)
…into the TEE attestation (proves the agent's record came from a genuine attested enclave) with attest/verify API
…fig migrations + amd64 verified-build workflow
…z + lto + strip) to shrink the deployable .so
mizuki0x added 3 commits July 1, 2026 13:44
…lve/discover); drop wrong non-ephemeral build-settlement workflow
…erified ER, provenance-proven, bonded and slashed live on mainnet
only the owner could commit the credit account to L1, which meant the
agent being held accountable was the only party who could surface its
provenance root on the base layer. split commit from undelegate: any
payer now funds a commit (pda validated against its stored owner, no
owner signature), so a verifier or keeper can force the record on-chain.
undelegate stays owner-gated. (magicblock dev review.)
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