MagicBlock ephemeral-rollup credit metering + x402-over-ER - #93
Open
mizuki0x wants to merge 32 commits into
Open
MagicBlock ephemeral-rollup credit metering + x402-over-ER#93mizuki0x wants to merge 32 commits into
mizuki0x wants to merge 32 commits into
Conversation
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
force-pushed
the
feat/magicblock-er
branch
from
June 23, 2026 20:11
0a519ba to
5937d29
Compare
…r reproducible verification
… MagicBlock rollup) + one-shot run
…(idle/lifetime/stall undelegate, TEE-capable)
…t (folded per consume) + migrate_credit_account for the upgrade path
…redit-account migration
…ote and relay it into a signed attestation
…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
…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.)
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.
Adds a MagicBlock Ephemeral Rollup settlement path: agents meter and pay per call by running
consume_creditsin an ER (gasless, ~ms), with the credit balance reconciling exactly to L1. Token custody, staking, slashing, and governance stay on mainnet; only the program-ownedu64credit balance delegates to the rollup.What's here
On-chain (settlement program)
delegate_credits/commit_credits/undelegate_creditsplus the#[ephemeral]undelegation callback, behind a newephemeralfeature.programs/settlement-ephemeralthatinclude!s the settlement source with the feature on. It is excluded from the workspace so itsephemeral-rollups-sdkgraph (which pins the solana runtime to 2.2.20) resolves in its own lock and never perturbs thelitesvm 0.6test 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:EphemeralSignerimplements theSignertrait by metering.build_paymentsubmits aconsume_creditsto 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'spay_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
ephemeral-rollups-sdkwithdefault-features = falsebreaks the SDK build (utils.rsreferencessolana_system_interface, which sits behind the default feature). Keep defaults and addanchor-compat.anchor-compatletsanchor-langsplit into 0.31.1 + 0.32.1 under a fresh lock; pin withcargo update -p anchor-lang --precise 0.31.1.litesvm 0.6(2.2.1, pre thesolana-feature-set->agave-feature-setrename). Solved by the isolated-crate split above.Verification (devnet)
Live on devnet (program
cov9UDyp..., EU ER validator), not just compiled or mocked:Client+EphemeralSigner-> facilitator -> ER:402 -> ER consume -> 200, balance -2 exactly, reconciled to L1.PayX402op drivesop_respond-> ER signer sidecar ->consume_creditsin the ER -> the facilitator ->200, with a settlement receipt recorded. Covered bypay_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-facilitator5; settlement litesvm suite unchanged.Daemon integration (landed + live-verified)
The full loop is wired into
covenantd:covenant-x402-er-signersidecar (incovenant-x402,solanafeature): stdinPaymentRequirements->consume_creditsin the ER viaEphemeralSigner-> stdoutx-paymentheader.X402Config::signer_for(network)routessolana-er:*providers to the ER sidecar (envCOVENANT_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.COVENANT_X402_ER_PROVIDERS(a JSON array); each is advertised to agents as aner.<slug>tool and routes through the same x402 pay path (gated bytool.call.er.<slug>, recording budget/receipt/audit).#[ignore]tests):pay_x402_settles_through_the_er_signer_live(raw op) ander_provider_tool_settles_in_the_er_live(an agent calling a registereder.*tool).