diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..91e527e9b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +# Keep Docker build contexts lean (Render builds from a clean git checkout; this +# matters for local `docker build` against a working tree with build artifacts). +**/target/ +**/node_modules/ +.git/ +landing/.next/ +**/*.log diff --git a/.github/workflows/verify-er.yml b/.github/workflows/verify-er.yml new file mode 100644 index 000000000..b28e8acca --- /dev/null +++ b/.github/workflows/verify-er.yml @@ -0,0 +1,47 @@ +name: verify-er + +# Reproducible build of the MagicBlock ER settlement program on a native amd64 +# runner (no qemu/rosetta), producing the verifiable .so to deploy + the +# executable hash. Runs when the program source or _shared.rs copy changes (so +# the drift-guard catches a stale copy), when this file changes, or manually. +on: + workflow_dispatch: + push: + branches: [feat/magicblock-er] + paths: + - '.github/workflows/verify-er.yml' + - 'agent-os/programs/settlement/src/lib.rs' + - 'agent-os/programs/settlement-ephemeral/src/_shared.rs' + - 'agent-os/programs/settlement-ephemeral/Cargo.toml' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install solana-verify + run: cargo install solana-verify --version 0.5.0 + + - name: Check _shared.rs matches settlement/src/lib.rs + run: diff agent-os/programs/settlement/src/lib.rs agent-os/programs/settlement-ephemeral/src/_shared.rs + + - name: Reproducible build (settlement-ephemeral, self-contained) + working-directory: agent-os/programs/settlement-ephemeral + run: | + solana-verify build "$PWD" \ + --library-name covenant_settlement_program_er \ + --base-image solanafoundation/solana-verifiable-build:3.1.14 + + - name: Executable hash + run: | + SO=agent-os/programs/settlement-ephemeral/target/deploy/covenant_settlement_program_er.so + ls -la "$SO" + solana-verify get-executable-hash "$SO" | tee erhash.txt + + - uses: actions/upload-artifact@v4 + with: + name: covenant-settlement-program-er + path: | + agent-os/programs/settlement-ephemeral/target/deploy/covenant_settlement_program_er.so + erhash.txt diff --git a/agent-os/Cargo.lock b/agent-os/Cargo.lock index 392cfa4a5..2506c73e8 100644 --- a/agent-os/Cargo.lock +++ b/agent-os/Cargo.lock @@ -1531,6 +1531,7 @@ dependencies = [ "reqwest 0.12.28", "serde", "serde_json", + "sha2 0.11.0", "solana-compute-budget-interface 3.0.0", "solana-sdk 4.0.1", "spl-associated-token-account 8.0.0", @@ -1543,6 +1544,27 @@ dependencies = [ "wiremock", ] +[[package]] +name = "covenant-x402-facilitator" +version = "0.0.0" +dependencies = [ + "axum", + "base64 0.22.1", + "bs58", + "covenant-x402", + "rand 0.9.4", + "reqwest 0.12.28", + "serde", + "serde_json", + "sha2 0.11.0", + "solana-sdk 4.0.1", + "thiserror 1.0.69", + "tokio", + "tracing", + "tracing-subscriber", + "wiremock", +] + [[package]] name = "covenant-zauth" version = "0.0.0" diff --git a/agent-os/Cargo.toml b/agent-os/Cargo.toml index 945caa806..7cc8b61b0 100644 --- a/agent-os/Cargo.toml +++ b/agent-os/Cargo.toml @@ -19,6 +19,7 @@ members = [ "crates/covenant-peer-auth", "crates/covenant-budget", "crates/covenant-x402", + "crates/covenant-x402-facilitator", "crates/covenant-hyre", "crates/covenant-zauth", "crates/covenant-metaplex", @@ -32,6 +33,11 @@ members = [ "programs/settlement", "programs/stake", ] +# settlement-ephemeral is the MagicBlock ER build of the settlement program. It is +# excluded from the workspace so its ephemeral-rollups-sdk graph (which forces the +# solana runtime crates to 2.2.20) resolves in its own lock and never perturbs the +# litesvm 0.6 test dependencies that the rest of the workspace pins to 2.2.1. +exclude = ["programs/settlement-ephemeral"] [workspace.package] edition = "2021" diff --git a/agent-os/crates/covenant-hyre/examples/live_payai_verify_smoke.rs b/agent-os/crates/covenant-hyre/examples/live_payai_verify_smoke.rs index 53fde1d89..1f755697a 100644 --- a/agent-os/crates/covenant-hyre/examples/live_payai_verify_smoke.rs +++ b/agent-os/crates/covenant-hyre/examples/live_payai_verify_smoke.rs @@ -47,6 +47,7 @@ async fn main() { scheme: "exact".into(), extra: Some(PaymentExtra { fee_payer: Some(PAYAI_FEE_PAYER.into()), + nonce: None, }), }; println!("=== input PaymentRequirements ==="); diff --git a/agent-os/crates/covenant-hyre/src/x402.rs b/agent-os/crates/covenant-hyre/src/x402.rs index 11aaa5a18..998dc11f0 100644 --- a/agent-os/crates/covenant-hyre/src/x402.rs +++ b/agent-os/crates/covenant-hyre/src/x402.rs @@ -185,6 +185,7 @@ fn to_requirements(accept: &Accept, caip2_network: &str) -> Result anyhow::Result<()> { + let tee = std::env::args().nth(1).unwrap_or_else(|| "https://mainnet-tee.magicblock.app".into()); + let key = std::env::var("COVENANT_ATTESTER_KEY") + .unwrap_or_else(|_| format!("{}/.config/solana/covenant-agent.json", std::env::var("HOME").unwrap())); + let (sk, pubkey) = signer_from_keypair_file(&key)?; + println!("attester : {pubkey}"); + println!("tee : {tee}"); + + let provenance = hex::encode(Sha256::digest(b"sample agent action chain")); + let att = attest(&tee, &sk, &pubkey, Some(&pubkey), Some(&provenance), None).await?; + let v = &att.body.er.validator.clone().unwrap_or_default(); + println!("validator: {v}"); + println!("enclave : TCB {} | mr_td {}...", att.body.enclave.status, &att.body.enclave.mr_td[..18]); + + verify_attestation(&att, &[pubkey.clone()])?; + println!("verify (trusted) : ok"); + match verify_attestation(&att, &["11111111111111111111111111111111".into()]) { + Err(e) => println!("verify (untrusted): rejected ({e})"), + Ok(()) => anyhow::bail!("untrusted key was wrongly accepted"), + } + println!("\nCOVENANT TEE ATTESTATION (Rust, agent-bound) VERIFIED"); + Ok(()) +} diff --git a/agent-os/crates/covenant-tee/src/lib.rs b/agent-os/crates/covenant-tee/src/lib.rs new file mode 100644 index 000000000..92b1c7bfa --- /dev/null +++ b/agent-os/crates/covenant-tee/src/lib.rs @@ -0,0 +1,244 @@ +//! Verify a MagicBlock Private ER's Intel TDX enclave off chain (DCAP via the +//! Phala quote-verification library) and relay the result into a signed Covenant +//! attestation. Optionally binds an agent and its on-chain provenance root into +//! the quote challenge, so the attestation proves a specific agent's verifiable +//! record came from a genuine, attested enclave. +//! +//! Trust model: the attestation carries the DCAP result, not the raw quote. The +//! attester's ed25519 signature is the anchor, so a verifier MUST pin which keys +//! it trusts; [`verify_attestation`] fails closed without a trusted set. + +use anyhow::{anyhow, bail, Result}; +use base64::{engine::general_purpose::STANDARD as B64, Engine}; +use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +const BIND_DOMAIN: &[u8] = b"covenant.tee.bind.v1"; +const PCCS: &str = "https://pccs.phala.network"; +const ACCEPTABLE: &[&str] = &["UpToDate"]; + +#[derive(Serialize, Deserialize, Clone)] +pub struct Er { + pub rpc_url: String, + pub validator: Option, + pub tee: String, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct Enclave { + pub status: String, + pub advisory_ids: Vec, + pub mr_td: String, + pub rt_mr0: String, + pub rt_mr1: String, + pub rt_mr2: String, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct Subject { + pub agent: String, + pub provenance_root: String, +} + +/// The signed half of an attestation. Serialized in declaration order, which is +/// the canonical form the signature covers. +#[derive(Serialize, Deserialize, Clone)] +pub struct AttestationBody { + pub kind: String, + pub er: Er, + pub enclave: Enclave, + pub subject: Option, + pub challenge: String, + pub nonce: String, + pub verified_at: u64, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct Attestation { + pub body: AttestationBody, + pub attester: String, + pub sig_alg: String, + pub signature: String, +} + +/// Load a Solana keypair file (the 64-byte JSON array) as an ed25519 signer, +/// returning the signing key and its base58 public key. +pub fn signer_from_keypair_file(path: &str) -> Result<(SigningKey, String)> { + let bytes: Vec = serde_json::from_str(&std::fs::read_to_string(path)?)?; + if bytes.len() < 32 { + bail!("keypair file holds {} bytes, need at least 32", bytes.len()); + } + let seed: [u8; 32] = bytes[..32].try_into().unwrap(); + let sk = SigningKey::from_bytes(&seed); + let pubkey = bs58::encode(sk.verifying_key().to_bytes()).into_string(); + Ok((sk, pubkey)) +} + +fn now_secs() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0) +} + +/// 64-byte quote challenge. With an agent and provenance root, the first 32 bytes +/// commit to sha256(domain || agent || provenance_root) and the last 32 are a +/// fresh nonce. Unbound: 64 random bytes. +fn build_challenge(agent: Option<&str>, prov: Option<&str>) -> Result<([u8; 64], [u8; 32])> { + let mut nonce = [0u8; 32]; + getrandom::getrandom(&mut nonce)?; + let mut challenge = [0u8; 64]; + challenge[32..].copy_from_slice(&nonce); + match (agent, prov) { + (Some(a), Some(p)) => { + let mut h = Sha256::new(); + h.update(BIND_DOMAIN); + h.update(bs58::decode(a).into_vec()?); + h.update(hex::decode(p)?); + challenge[..32].copy_from_slice(&h.finalize()); + } + _ => getrandom::getrandom(&mut challenge[..32])?, + } + Ok((challenge, nonce)) +} + +async fn fetch_quote(client: &reqwest::Client, tee_url: &str, challenge: &[u8; 64]) -> Result> { + let url = format!( + "{}/quote?challenge={}", + tee_url.trim_end_matches('/'), + urlencoding::encode(&B64.encode(challenge)) + ); + let v: serde_json::Value = client.get(&url).send().await?.error_for_status()?.json().await?; + let q = v["quote"].as_str().ok_or_else(|| anyhow!("TEE /quote returned no quote field"))?; + Ok(B64.decode(q)?) +} + +async fn fetch_identity(client: &reqwest::Client, tee_url: &str) -> Option { + let body = serde_json::json!({"jsonrpc": "2.0", "id": 1, "method": "getIdentity"}); + let v: serde_json::Value = client + .post(tee_url.trim_end_matches('/')) + .json(&body) + .send() + .await + .ok()? + .json() + .await + .ok()?; + v["result"]["identity"].as_str().map(String::from) +} + +/// Fetch a TDX quote from `tee_url`, DCAP-verify it against the Phala PCCS, and +/// return a signed Covenant attestation. `agent`/`provenance_root` (base58/hex) +/// bind that record into the quote challenge. +pub async fn attest( + tee_url: &str, + sk: &SigningKey, + pubkey: &str, + agent: Option<&str>, + provenance_root: Option<&str>, + now: Option, +) -> Result { + let client = reqwest::Client::new(); + let (challenge, nonce) = build_challenge(agent, provenance_root)?; + let raw_quote = fetch_quote(&client, tee_url, &challenge).await?; + let identity = fetch_identity(&client, tee_url).await; + + let collateral = dcap_qvl::collateral::CollateralClient::with_default_http(PCCS)? + .fetch(&raw_quote) + .await?; + let verified_at = now.unwrap_or_else(now_secs); + let verified = dcap_qvl::verify::verify(&raw_quote, &collateral, verified_at) + .map_err(|e| anyhow!("dcap verify failed: {e:?}"))?; + + let (report_data, mr_td, rt_mr0, rt_mr1, rt_mr2) = match &verified.report { + dcap_qvl::quote::Report::TD10(t) => (t.report_data, t.mr_td, t.rt_mr0, t.rt_mr1, t.rt_mr2), + dcap_qvl::quote::Report::TD15(t) => ( + t.base.report_data, + t.base.mr_td, + t.base.rt_mr0, + t.base.rt_mr1, + t.base.rt_mr2, + ), + _ => bail!("expected a TDX quote, got an SGX enclave quote"), + }; + if report_data != challenge { + bail!("report_data != challenge (stale or replayed quote)"); + } + if !ACCEPTABLE.contains(&verified.status.as_str()) { + bail!("unacceptable TCB status: {}", verified.status); + } + + let body = AttestationBody { + kind: "covenant.tee.attestation.v1".into(), + er: Er { rpc_url: tee_url.to_string(), validator: identity, tee: "intel-tdx".into() }, + enclave: Enclave { + status: verified.status.clone(), + advisory_ids: verified.advisory_ids.clone(), + mr_td: hex::encode(mr_td), + rt_mr0: hex::encode(rt_mr0), + rt_mr1: hex::encode(rt_mr1), + rt_mr2: hex::encode(rt_mr2), + }, + subject: match (agent, provenance_root) { + (Some(a), Some(p)) => Some(Subject { agent: a.into(), provenance_root: p.into() }), + _ => None, + }, + challenge: B64.encode(challenge), + nonce: B64.encode(nonce), + verified_at, + }; + let signature = sk.sign(&serde_json::to_vec(&body)?); + Ok(Attestation { + body, + attester: pubkey.to_string(), + sig_alg: "ed25519".into(), + signature: B64.encode(signature.to_bytes()), + }) +} + +/// Verify a Covenant TEE attestation against a pinned set of trusted attesters. +/// Checks the attester is trusted, its ed25519 signature over the canonical body, +/// the TCB status, the agent/provenance binding committed in the quote challenge, +/// and the nonce. Fails closed if `trusted` is empty. +pub fn verify_attestation(att: &Attestation, trusted: &[String]) -> Result<()> { + if trusted.is_empty() { + bail!("no trusted attesters configured"); + } + if !trusted.iter().any(|t| t == &att.attester) { + bail!("attester {} is not trusted", att.attester); + } + if att.sig_alg != "ed25519" { + bail!("unsupported sig_alg {}", att.sig_alg); + } + let pk: [u8; 32] = bs58::decode(&att.attester) + .into_vec()? + .as_slice() + .try_into() + .map_err(|_| anyhow!("attester is not a 32-byte key"))?; + let vk = VerifyingKey::from_bytes(&pk)?; + let sig = Signature::from_slice(&B64.decode(&att.signature)?)?; + vk.verify_strict(&serde_json::to_vec(&att.body)?, &sig) + .map_err(|_| anyhow!("bad attester signature"))?; + + if !ACCEPTABLE.contains(&att.body.enclave.status.as_str()) { + bail!("TCB status {}", att.body.enclave.status); + } + let challenge = B64.decode(&att.body.challenge)?; + if challenge.len() != 64 { + bail!("challenge is not 64 bytes"); + } + if let Some(sub) = &att.body.subject { + let mut h = Sha256::new(); + h.update(BIND_DOMAIN); + h.update(bs58::decode(&sub.agent).into_vec()?); + h.update(hex::decode(&sub.provenance_root)?); + if h.finalize()[..] != challenge[..32] { + bail!("agent/provenance not committed in the quote"); + } + } + if B64.decode(&att.body.nonce)? != challenge[32..64] { + bail!("nonce mismatch"); + } + Ok(()) +} diff --git a/agent-os/crates/covenant-x402-facilitator/Cargo.toml b/agent-os/crates/covenant-x402-facilitator/Cargo.toml new file mode 100644 index 000000000..282f4cb75 --- /dev/null +++ b/agent-os/crates/covenant-x402-facilitator/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "covenant-x402-facilitator" +version = "0.0.0" +edition.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +description = "x402 facilitator that verifies ER-settled (consume_credits) payments and gates a paid HTTP endpoint. Counterpart to covenant-x402's EphemeralSigner." + +[dependencies] +axum = { workspace = true } +tokio = { workspace = true } +reqwest = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +rand = { workspace = true } +sha2 = { workspace = true } +bs58 = { workspace = true } +base64 = "0.22" + +[dev-dependencies] +wiremock = "0.6" +# For the live_loop example: drive the real x402 Client + EphemeralSigner against +# this facilitator end to end. +covenant-x402 = { path = "../covenant-x402", features = ["solana"] } +solana-sdk = "4" + +[[example]] +name = "live_loop" +path = "examples/live_loop.rs" diff --git a/agent-os/crates/covenant-x402-facilitator/README.md b/agent-os/crates/covenant-x402-facilitator/README.md new file mode 100644 index 000000000..e6782f958 --- /dev/null +++ b/agent-os/crates/covenant-x402-facilitator/README.md @@ -0,0 +1,81 @@ +# covenant-x402-facilitator + +The verifier side of x402-over-ER. Counterpart to `covenant-x402`'s +`EphemeralSigner`: where the signer pays by running `consume_credits` in a +MagicBlock ephemeral rollup, this verifies that proof and gates a paid endpoint. + +- `GET /paid`, no `x-payment` → **402** + an x402 challenge array carrying a fresh + single-use `nonce` and the `amountCredits` price (shaped as `covenant-x402`'s + `PaymentRequirements`, so its `Client` parses it directly). +- `GET /paid` with `x-payment` → decode the envelope, fetch the consume on the ER + (`getTransaction`), and check: it's a `consume_credits` to the settlement program, + `amount >= price`, `receipt_hash == sha256(nonce)`, and the nonce was issued and + not yet consumed (anti-replay). Pass → **200** + content. + +The credit account is read from the on-chain transaction, not the client envelope, +so a spoofed field can't lie about what was metered. It speaks JSON-RPC and base58, +so it needs neither solana-sdk nor the on-chain ER SDK (zero lock impact). + +## Run + +```bash +cargo run -p covenant-x402-facilitator # PROGRAM, ER, PRICE, PORT via env +``` + +## Tests + live verification + +Unit + wiremock tests cover the verifier (good payment, nonce mismatch, underpay, +receipt-not-bound-to-nonce, wrong-program). The full loop is **live-verified on +devnet**: the real `covenant-x402` `Client` + `EphemeralSigner` against a running +facilitator and the EU ER validator — + +```bash +# 1) start the facilitator (PRICE=2) +PRICE=2 ER=https://devnet-eu.magicblock.app cargo run -p covenant-x402-facilitator +# 2) delegate the credit account +node ../../programs/settlement-ephemeral/spike/er-session.mjs delegate +# 3) drive the whole loop in Rust +cargo run -p covenant-x402-facilitator --example live_loop +``` + +Verified run: `GET /paid → 402 → ER consume → 200`, credit balance dropped by exactly +the price, reconciled to L1 on undelegate. + +## Full daemon path (live, reproducible) + +The end-to-end daemon integration is covered by an `#[ignore]`d test in `covenantd`, +`pay_x402_settles_through_the_er_signer_live`: it drives a real `PayX402` op through +`op_respond` → `X402Config::signer_for` → the ER signer sidecar → a `consume_credits` +in the ER → this facilitator → `200`, and asserts a settlement receipt was recorded. +Run it (delegate the credit account first via `er-session.mjs delegate`): + +```bash +ER_SIGNER_BIN=target/debug/covenant-x402-er-signer \ +FACILITATOR_BIN=target/debug/covenant-x402-facilitator \ +ER_KEYPAIR="$HOME/.config/solana/id.json" \ +cargo test -p covenantd --lib pay_x402_settles_through_the_er_signer_live -- --ignored --nocapture +``` + +Verified green on devnet: the daemon spawned the sidecar, the ER consume settled, the +facilitator returned 200, and a receipt was recorded. + +## Deployed (Render) + +Live devnet instance: **https://covenant-x402-er-facilitator.onrender.com** (Render +web service `covenant-x402-er-facilitator`, Frankfurt, built from +`deploy/Dockerfile.facilitator` and declared in the root `render.yaml` blueprint). +`/health` returns 200; `/paid` returns the x402 challenge. Verify a real payment +against the deployed URL with `spike/remote-check.mjs` (delegate the credit account +first): + +```bash +node er-session.mjs delegate +URL=https://covenant-x402-er-facilitator.onrender.com/paid node remote-check.mjs +node er-session.mjs undelegate +``` + +Verified live against the deployed service: `GET /paid → 402 → consume_credits in +the ER → 200` with the settling signature returned. The service tracks +`feat/magicblock-er` until #93 merges; repoint it to `main` after (the blueprint +matches it by name). Devnet config; mainnet is gated on MagicBlock's validator +answer. diff --git a/agent-os/crates/covenant-x402-facilitator/examples/live_loop.rs b/agent-os/crates/covenant-x402-facilitator/examples/live_loop.rs new file mode 100644 index 000000000..6f1b085d0 --- /dev/null +++ b/agent-os/crates/covenant-x402-facilitator/examples/live_loop.rs @@ -0,0 +1,48 @@ +//! Full x402-over-ER loop, live, in Rust: the real `covenant_x402::Client` drives +//! `EphemeralSigner` against a running facilitator and the live ER. +//! +//! Start the facilitator first (`cargo run -p covenant-x402-facilitator`) and make +//! sure the credit account is delegated (spike `er-session.mjs delegate`), then: +//! +//! cargo run -p covenant-x402-facilitator --example live_loop +//! +//! Env: PAYER, PROGRAM, ER (passed to the signer), URL (facilitator endpoint). + +use std::env; +use std::str::FromStr; + +use covenant_x402::{ephemeral::EphemeralSigner, http_client, Capability, Client}; +use reqwest::Method; +use solana_sdk::pubkey::Pubkey; + +#[tokio::main] +async fn main() { + let home = env::var("HOME").unwrap(); + let payer = env::var("PAYER").unwrap_or(format!("{home}/.config/solana/id.json")); + let program = Pubkey::from_str( + &env::var("PROGRAM").unwrap_or("cov9UDypG7nsryxdgMcKhKU2spRVWLVjxT2iTv6do5Y".into()), + ) + .expect("program id"); + let er = env::var("ER").unwrap_or("https://devnet-eu.magicblock.app".into()); + let url = env::var("URL").unwrap_or("http://127.0.0.1:8402/paid".into()); + + let signer = EphemeralSigner::from_keypair_file(&payer, program, er).expect("build signer"); + let client = Client::new(http_client()); + let cap = Capability { + provider: "covenant-er".into(), + network: "solana-er:devnet".into(), + asset: "credits".into(), + per_call_cap: 1_000_000, + }; + + println!("agent calling {url} (pays per call by metering in the ER)..."); + let resp = client + .request_paid(Method::GET, &url, None, &cap, &signer) + .await + .expect("paid call"); + let status = resp.status(); + let body = resp.text().await.unwrap_or_default(); + println!("-> {status}\n{body}"); + assert!(status.is_success(), "expected 200 after ER-settled payment"); + println!("\nOK — full x402-over-ER loop in Rust: 402 -> ER consume -> 200."); +} diff --git a/agent-os/crates/covenant-x402-facilitator/src/lib.rs b/agent-os/crates/covenant-x402-facilitator/src/lib.rs new file mode 100644 index 000000000..6bfbf3212 --- /dev/null +++ b/agent-os/crates/covenant-x402-facilitator/src/lib.rs @@ -0,0 +1,476 @@ +//! x402 facilitator for ER-settled payments. +//! +//! Counterpart to [`covenant_x402::EphemeralSigner`]. Where the signer pays by +//! running `consume_credits` in a MagicBlock ephemeral rollup and hands back the +//! signature, this verifies that proof and gates a paid endpoint: +//! +//! - `GET /paid` with no `x-payment` header → `402` + a challenge carrying a fresh +//! single-use `nonce` and the `amountCredits` price. +//! - `GET /paid` with an `x-payment` header → decode the envelope, fetch the consume +//! on the ER, and check: it's a `consume_credits` to the settlement program, the +//! `amount >= price`, `receipt_hash == sha256(nonce)`, and the nonce was one we +//! issued and have not yet consumed (anti-replay). Pass → `200` + content. +//! +//! It talks to the ER over JSON-RPC (`getTransaction`) and works in base58 strings, +//! so it needs neither solana-sdk nor the on-chain ER SDK. + +#![deny(unsafe_code)] + +use std::collections::HashMap; +use std::time::{Duration, Instant}; +use std::sync::{Arc, Mutex}; + +use axum::{ + extract::State, + http::{HeaderMap, StatusCode}, + response::{IntoResponse, Response}, + routing::get, + Json, Router, +}; +use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; +use serde_json::json; +use sha2::{Digest, Sha256}; + +/// `sha256("global:consume_credits")[..8]` — the settlement program's instruction +/// discriminator for `consume_credits`. +pub fn consume_discriminator() -> [u8; 8] { + let mut out = [0u8; 8]; + out.copy_from_slice(&Sha256::digest(b"global:consume_credits")[..8]); + out +} + +#[derive(Debug, thiserror::Error, PartialEq)] +pub enum VerifyError { + #[error("bad x-payment envelope: {0}")] + Envelope(String), + #[error("nonce mismatch")] + NonceMismatch, + #[error("ER rpc: {0}")] + Rpc(String), + #[error("consume tx not found on ER")] + NotFound, + #[error("consume tx failed on ER")] + TxFailed, + #[error("no consume_credits to the settlement program in tx")] + NoConsume, + #[error("underpaid: {paid} < {required}")] + Underpaid { paid: u64, required: u64 }, + #[error("receipt_hash does not match nonce")] + ReceiptMismatch, +} + +/// A verified ER-settled payment. +#[derive(Debug, Clone, PartialEq)] +pub struct Verified { + pub amount: u64, + pub signature: String, + /// The credit account that was metered (read from the on-chain tx, not the + /// client-supplied envelope). + pub credit_account: String, +} + +/// Verifies ER consume_credits payments against a settlement program on an ER. +pub struct ErPaymentVerifier { + program_id: String, + er_rpc_url: String, + http: reqwest::Client, +} + +impl ErPaymentVerifier { + pub fn new(program_id: impl Into, er_rpc_url: impl Into) -> Self { + Self { + program_id: program_id.into(), + er_rpc_url: er_rpc_url.into(), + http: reqwest::Client::new(), + } + } + + pub fn program_id(&self) -> &str { + &self.program_id + } + + /// Verifies that `x_payment_b64` proves a `consume_credits` of at least + /// `min_amount`, bound to `expected_nonce`. Reads the credit account from the + /// on-chain transaction so a spoofed envelope field cannot lie about it. + pub async fn verify( + &self, + x_payment_b64: &str, + expected_nonce: &str, + min_amount: u64, + ) -> Result { + let raw = BASE64 + .decode(x_payment_b64.trim()) + .map_err(|e| VerifyError::Envelope(e.to_string()))?; + let envelope: serde_json::Value = + serde_json::from_slice(&raw).map_err(|e| VerifyError::Envelope(e.to_string()))?; + let payload = envelope + .get("payload") + .ok_or_else(|| VerifyError::Envelope("no payload".into()))?; + let signature = payload + .get("signature") + .and_then(|v| v.as_str()) + .ok_or_else(|| VerifyError::Envelope("no signature".into()))?; + let nonce = payload + .get("nonce") + .and_then(|v| v.as_str()) + .ok_or_else(|| VerifyError::Envelope("no nonce".into()))?; + if nonce != expected_nonce { + return Err(VerifyError::NonceMismatch); + } + + let result = self.fetch_transaction(signature).await?; + if result + .pointer("/meta/err") + .map(|v| !v.is_null()) + .unwrap_or(false) + { + return Err(VerifyError::TxFailed); + } + let keys = result + .pointer("/transaction/message/accountKeys") + .and_then(|v| v.as_array()) + .ok_or(VerifyError::NoConsume)?; + let ixs = result + .pointer("/transaction/message/instructions") + .and_then(|v| v.as_array()) + .ok_or(VerifyError::NoConsume)?; + + let disc = consume_discriminator(); + let expected_receipt = Sha256::digest(expected_nonce.as_bytes()); + for ix in ixs { + let pid = ix + .get("programIdIndex") + .and_then(|v| v.as_u64()) + .and_then(|i| keys.get(i as usize)) + .and_then(|v| v.as_str()) + .unwrap_or_default(); + if pid != self.program_id { + continue; + } + let data = ix + .get("data") + .and_then(|v| v.as_str()) + .and_then(|d| bs58::decode(d).into_vec().ok()) + .ok_or_else(|| VerifyError::Envelope("undecodable ix data".into()))?; + if data.len() < 48 || data[..8] != disc { + continue; + } + let amount = u64::from_le_bytes(data[8..16].try_into().unwrap()); + if amount < min_amount { + return Err(VerifyError::Underpaid { + paid: amount, + required: min_amount, + }); + } + if data[16..48] != expected_receipt[..] { + return Err(VerifyError::ReceiptMismatch); + } + // ConsumeCredits accounts: [config, credits, owner]. Credit account is #1. + let credit_account = ix + .get("accounts") + .and_then(|v| v.as_array()) + .and_then(|a| a.get(1)) + .and_then(|v| v.as_u64()) + .and_then(|i| keys.get(i as usize)) + .and_then(|v| v.as_str()) + .unwrap_or_default() + .to_string(); + return Ok(Verified { + amount, + signature: signature.to_string(), + credit_account, + }); + } + Err(VerifyError::NoConsume) + } + + async fn fetch_transaction(&self, signature: &str) -> Result { + let body = json!({ + "jsonrpc": "2.0", "id": 1, "method": "getTransaction", + "params": [signature, {"encoding": "json", "commitment": "confirmed", "maxSupportedTransactionVersion": 0}] + }); + let resp = self + .http + .post(&self.er_rpc_url) + .json(&body) + .send() + .await + .map_err(|e| VerifyError::Rpc(e.to_string()))?; + let parsed: serde_json::Value = resp + .json() + .await + .map_err(|e| VerifyError::Rpc(e.to_string()))?; + if let Some(err) = parsed.get("error").filter(|e| !e.is_null()) { + return Err(VerifyError::Rpc(err.to_string())); + } + parsed + .get("result") + .filter(|v| !v.is_null()) + .cloned() + .ok_or(VerifyError::NotFound) + } +} + +/// A paid endpoint gated by ER-settled payments. +/// How long an issued challenge nonce stays valid. A payment must present its +/// `x-payment` within this window or the nonce is evicted (fail-closed). +const NONCE_TTL: Duration = Duration::from_secs(300); +/// Backstop bound on outstanding nonces so a flood of unpaid challenges cannot +/// grow memory without limit. With TTL eviction the steady state is far below this. +const MAX_OUTSTANDING_NONCES: usize = 50_000; + +/// Drop nonces older than [`NONCE_TTL`]. Called under the lock on every request so +/// the issued set stays bounded by (issuance rate x TTL), and expired challenges +/// can never be redeemed. +fn evict_expired(issued: &mut HashMap) { + let now = Instant::now(); + issued.retain(|_, &mut t| now.duration_since(t) < NONCE_TTL); +} + +pub struct PaidEndpoint { + pub verifier: ErPaymentVerifier, + pub price: u64, + pub content: serde_json::Value, + /// Single-use nonces we have issued and not yet consumed, with their issue + /// time. TTL-evicted (see [`evict_expired`]) and bounded by + /// [`MAX_OUTSTANDING_NONCES`], so unpaid challenges don't accumulate. + issued: Mutex>, +} + +impl PaidEndpoint { + pub fn new(verifier: ErPaymentVerifier, price: u64, content: serde_json::Value) -> Arc { + Arc::new(Self { + verifier, + price, + content, + issued: Mutex::new(HashMap::new()), + }) + } +} + +pub fn router(state: Arc) -> Router { + Router::new() + .route("/", get(index)) + .route("/health", get(health)) + .route("/paid", get(handler)) + .with_state(state) +} + +/// Root descriptor so hitting the bare domain shows what this is, not a 404. +async fn index(State(state): State>) -> Response { + ( + StatusCode::OK, + Json(json!({ + "service": "covenant-x402-er-facilitator", + "description": "Verifies ER-settled (consume_credits) x402 payments and gates /paid.", + "endpoints": { + "GET /paid": "402 + an x402 challenge; pay with an x-payment header", + "GET /health": "liveness probe" + }, + "program": state.verifier.program_id(), + "network": "solana-er:devnet", + "scheme": "exact-er", + "priceCredits": state.price.to_string() + })), + ) + .into_response() +} + +/// Liveness probe for the deploy platform (Render health check). Always 200. +async fn health() -> Response { + (StatusCode::OK, "ok").into_response() +} + +fn nonce_from_envelope(x_payment_b64: &str) -> Option { + let raw = BASE64.decode(x_payment_b64.trim()).ok()?; + let env: serde_json::Value = serde_json::from_slice(&raw).ok()?; + env.pointer("/payload/nonce") + .and_then(|v| v.as_str()) + .map(str::to_string) +} + +fn random_nonce() -> String { + format!("{:032x}", rand::random::()) +} + +fn pay402(why: &str) -> Response { + (StatusCode::PAYMENT_REQUIRED, Json(json!({"error": why}))).into_response() +} + +async fn handler(State(state): State>, headers: HeaderMap) -> Response { + let Some(hdr) = headers.get("x-payment") else { + let nonce = random_nonce(); + { + let mut issued = state.issued.lock().unwrap(); + evict_expired(&mut issued); + if issued.len() >= MAX_OUTSTANDING_NONCES { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(json!({ "error": "too many outstanding challenges; retry shortly" })), + ) + .into_response(); + } + issued.insert(nonce.clone(), Instant::now()); + } + // An x402 challenge body: an array of acceptable payment options. The + // shape matches covenant-x402's `PaymentRequirements` so its `Client` + // parses it directly; the per-request nonce rides in `extra.nonce`, which + // the `EphemeralSigner` reads to bind the on-chain receipt_hash. + return ( + StatusCode::PAYMENT_REQUIRED, + Json(json!([{ + "network": "solana-er:devnet", + "asset": "credits", + "amount": state.price.to_string(), + "amountUsdc": 0.0, + "payTo": state.verifier.program_id(), + "scheme": "exact-er", + "extra": { "nonce": nonce }, + }])), + ) + .into_response(); + }; + + let Ok(hdr) = hdr.to_str() else { + return pay402("invalid x-payment header"); + }; + let Some(nonce) = nonce_from_envelope(hdr) else { + return pay402("bad x-payment envelope"); + }; + // Consume the nonce atomically: unknown, already-used, or expired nonces are + // rejected. Single-use consumption is the primary replay guard; the TTL eviction + // means an expired challenge is already gone here and fails closed. + { + let mut issued = state.issued.lock().unwrap(); + evict_expired(&mut issued); + if issued.remove(&nonce).is_none() { + return pay402("unknown, already-used, or expired nonce"); + } + } + match state.verifier.verify(hdr, &nonce, state.price).await { + Ok(v) => ( + StatusCode::OK, + Json(json!({ + "content": state.content, + "paidCredits": v.amount, + "creditAccount": v.credit_account, + "settledBy": v.signature, + })), + ) + .into_response(), + Err(e) => pay402(&e.to_string()), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use wiremock::matchers::{body_string_contains, method}; + use wiremock::{Mock, MockServer, ResponseTemplate}; + + const PROGRAM: &str = "cov9UDypG7nsryxdgMcKhKU2spRVWLVjxT2iTv6do5Y"; + + #[test] + fn evict_expired_drops_old_nonces_keeps_fresh() { + let mut issued: HashMap = HashMap::new(); + let stale = Instant::now() + .checked_sub(NONCE_TTL + Duration::from_secs(60)) + .expect("instant sub"); + issued.insert("stale".into(), stale); + issued.insert("fresh".into(), Instant::now()); + evict_expired(&mut issued); + assert!(!issued.contains_key("stale"), "expired nonce must be evicted"); + assert!(issued.contains_key("fresh"), "fresh nonce must survive"); + } + + fn envelope(signature: &str, nonce: &str) -> String { + BASE64.encode( + json!({"x402Version":1,"scheme":"exact-er","payload":{"signature":signature,"nonce":nonce}}) + .to_string(), + ) + } + + /// Builds a getTransaction result whose instruction is a consume_credits of + /// `amount` with `receipt_hash = sha256(receipt_nonce)`. + fn consume_tx_result(amount: u64, receipt_nonce: &str, program: &str) -> serde_json::Value { + let mut data = Vec::new(); + data.extend_from_slice(&consume_discriminator()); + data.extend_from_slice(&amount.to_le_bytes()); + data.extend_from_slice(&Sha256::digest(receipt_nonce.as_bytes())); + let data_b58 = bs58::encode(data).into_string(); + json!({ + "meta": {"err": null}, + "transaction": {"message": { + "accountKeys": ["ConfigPDA11111111111111111111111111111111", "CreditPDA1111111111111111111111111111111", "Owner11111111111111111111111111111111111", program], + "instructions": [{"programIdIndex": 3, "accounts": [0, 1, 2], "data": data_b58}] + }} + }) + } + + async fn server_with(result: serde_json::Value) -> (MockServer, ErPaymentVerifier) { + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(body_string_contains("getTransaction")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "jsonrpc": "2.0", "id": 1, "result": result + }))) + .mount(&server) + .await; + let verifier = ErPaymentVerifier::new(PROGRAM, server.uri()); + (server, verifier) + } + + #[tokio::test] + async fn verifies_a_good_payment() { + let (_s, v) = server_with(consume_tx_result(5, "abc123", PROGRAM)).await; + let out = v + .verify(&envelope("SiGmAtUrE", "abc123"), "abc123", 5) + .await + .expect("verify"); + assert_eq!(out.amount, 5); + assert_eq!(out.signature, "SiGmAtUrE"); + assert_eq!(out.credit_account, "CreditPDA1111111111111111111111111111111"); + } + + #[tokio::test] + async fn rejects_nonce_mismatch_before_rpc() { + let (_s, v) = server_with(consume_tx_result(5, "abc123", PROGRAM)).await; + let err = v + .verify(&envelope("sig", "different"), "abc123", 5) + .await + .unwrap_err(); + assert_eq!(err, VerifyError::NonceMismatch); + } + + #[tokio::test] + async fn rejects_underpayment() { + let (_s, v) = server_with(consume_tx_result(2, "abc123", PROGRAM)).await; + let err = v + .verify(&envelope("sig", "abc123"), "abc123", 5) + .await + .unwrap_err(); + assert_eq!(err, VerifyError::Underpaid { paid: 2, required: 5 }); + } + + #[tokio::test] + async fn rejects_receipt_not_bound_to_nonce() { + // on-chain receipt_hash is sha256("wrong"), but the challenge nonce is "abc123" + let (_s, v) = server_with(consume_tx_result(5, "wrong", PROGRAM)).await; + let err = v + .verify(&envelope("sig", "abc123"), "abc123", 5) + .await + .unwrap_err(); + assert_eq!(err, VerifyError::ReceiptMismatch); + } + + #[tokio::test] + async fn rejects_consume_to_a_different_program() { + let other = "11111111111111111111111111111111"; + let (_s, v) = server_with(consume_tx_result(5, "abc123", other)).await; + let err = v + .verify(&envelope("sig", "abc123"), "abc123", 5) + .await + .unwrap_err(); + assert_eq!(err, VerifyError::NoConsume); + } +} diff --git a/agent-os/crates/covenant-x402-facilitator/src/main.rs b/agent-os/crates/covenant-x402-facilitator/src/main.rs new file mode 100644 index 000000000..4f04fd339 --- /dev/null +++ b/agent-os/crates/covenant-x402-facilitator/src/main.rs @@ -0,0 +1,29 @@ +//! Runs the x402 ER facilitator over a single paid endpoint. +//! +//! cargo run -p covenant-x402-facilitator +//! +//! Env: PROGRAM (settlement program, default cov9UDyp…), ER (validator RPC, default +//! devnet-eu), PRICE (credits/call, default 1), PORT (default 8402). + +use std::env; + +use covenant_x402_facilitator::{router, ErPaymentVerifier, PaidEndpoint}; +use serde_json::json; + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt::init(); + + let program = env::var("PROGRAM").unwrap_or("cov9UDypG7nsryxdgMcKhKU2spRVWLVjxT2iTv6do5Y".into()); + let er = env::var("ER").unwrap_or("https://devnet-eu.magicblock.app".into()); + let price: u64 = env::var("PRICE").ok().and_then(|s| s.parse().ok()).unwrap_or(1); + let port: u16 = env::var("PORT").ok().and_then(|s| s.parse().ok()).unwrap_or(8402); + + let content = json!({ "quote": "BTC looks coiled. Position for a breakout." }); + let state = PaidEndpoint::new(ErPaymentVerifier::new(program.clone(), er.clone()), price, content); + + let addr = format!("0.0.0.0:{port}"); + tracing::info!(%program, %er, price, %addr, "x402 ER facilitator listening on /paid"); + let listener = tokio::net::TcpListener::bind(&addr).await.expect("bind"); + axum::serve(listener, router(state)).await.expect("serve"); +} diff --git a/agent-os/crates/covenant-x402/Cargo.toml b/agent-os/crates/covenant-x402/Cargo.toml index d2750d81f..c8167d464 100644 --- a/agent-os/crates/covenant-x402/Cargo.toml +++ b/agent-os/crates/covenant-x402/Cargo.toml @@ -21,6 +21,7 @@ solana = [ "dep:base64", "dep:bincode", "dep:tokio", + "dep:sha2", ] [dependencies] @@ -39,7 +40,8 @@ spl-token = { version = "9", optional = true } spl-associated-token-account = { version = "8", optional = true } base64 = { version = "0.22", optional = true } bincode = { version = "1.3", optional = true } -tokio = { version = "1", features = ["rt-multi-thread", "macros"], optional = true } +tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"], optional = true } +sha2 = { workspace = true, optional = true } [dev-dependencies] tokio = { version = "1", features = ["rt-multi-thread", "macros"] } @@ -50,3 +52,13 @@ tempfile = { workspace = true } name = "xona-demo" path = "src/bin/xona-demo.rs" required-features = ["solana"] + +[[bin]] +name = "covenant-x402-er-signer" +path = "src/bin/er-signer.rs" +required-features = ["solana"] + +[[example]] +name = "ephemeral_live" +path = "examples/ephemeral_live.rs" +required-features = ["solana"] diff --git a/agent-os/crates/covenant-x402/examples/ephemeral_live.rs b/agent-os/crates/covenant-x402/examples/ephemeral_live.rs new file mode 100644 index 000000000..41905f347 --- /dev/null +++ b/agent-os/crates/covenant-x402/examples/ephemeral_live.rs @@ -0,0 +1,57 @@ +//! Live check: drive `EphemeralSigner` against a real MagicBlock ER validator. +//! +//! The credit account must already be delegated (see the settlement-ephemeral +//! spike's `er-session.mjs delegate`). This calls `build_payment`, which submits a +//! `consume_credits` to the ER and returns the x402 envelope, then prints it. +//! +//! cargo run -p covenant-x402 --features solana --example ephemeral_live +//! +//! Env: PAYER (keypair, default ~/.config/solana/id.json), PROGRAM (default +//! cov9UDyp…), ER (default devnet-eu), AMOUNT (default 3), NONCE (default a fixed hex). + +use std::env; +use std::str::FromStr; + +use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; +use covenant_x402::{ephemeral::EphemeralSigner, PaymentExtra, PaymentRequirements, Signer}; +use solana_sdk::pubkey::Pubkey; + +#[tokio::main] +async fn main() { + let home = env::var("HOME").unwrap(); + let payer = env::var("PAYER").unwrap_or(format!("{home}/.config/solana/id.json")); + let program = Pubkey::from_str( + &env::var("PROGRAM").unwrap_or("cov9UDypG7nsryxdgMcKhKU2spRVWLVjxT2iTv6do5Y".into()), + ) + .expect("program id"); + let er = env::var("ER").unwrap_or("https://devnet-eu.magicblock.app".into()); + let amount = env::var("AMOUNT").unwrap_or("3".into()); + let nonce = env::var("NONCE").unwrap_or("deadbeefcafe".into()); + + let signer = EphemeralSigner::from_keypair_file(&payer, program, er).expect("build signer"); + println!( + "payer {} | credit account {}", + signer.pubkey(), + signer.credit_account() + ); + + let req = PaymentRequirements { + network: "solana-er:devnet".into(), + asset: "credits".into(), + amount: amount.clone(), + amount_usdc: 0.0, + pay_to: "credits".into(), + scheme: "exact-er".into(), + extra: Some(PaymentExtra { + fee_payer: None, + nonce: Some(nonce.clone()), + }), + }; + + println!("metering {amount} credits in the ER (nonce {nonce})..."); + let header = signer.build_payment(&req).await.expect("build_payment"); + let json: serde_json::Value = + serde_json::from_slice(&BASE64.decode(&header).expect("b64")).expect("json"); + println!("x-payment envelope:\n{}", serde_json::to_string_pretty(&json).unwrap()); + println!("\nER consume signature: {}", json["payload"]["signature"]); +} diff --git a/agent-os/crates/covenant-x402/src/bin/er-signer.rs b/agent-os/crates/covenant-x402/src/bin/er-signer.rs new file mode 100644 index 000000000..eb134c3c8 --- /dev/null +++ b/agent-os/crates/covenant-x402/src/bin/er-signer.rs @@ -0,0 +1,52 @@ +//! ER x402 signer sidecar. +//! +//! Matches the daemon's `SubprocessSigner` contract: reads a `PaymentRequirements` +//! JSON on stdin, runs `consume_credits` in the ER via [`EphemeralSigner`], and +//! writes the resulting `x-payment` header to stdout. The funding key, settlement +//! program, and ER RPC come from this process's own env, so they never enter the +//! daemon's address space. +//! +//! Env: COVENANT_X402_ER_KEYPAIR (Solana CLI keypair json), COVENANT_X402_ER_PROGRAM +//! (settlement program id), COVENANT_X402_ER_RPC (pinned ER validator RPC). + +use std::io::Read; +use std::process::exit; +use std::str::FromStr; + +use covenant_x402::{ephemeral::EphemeralSigner, PaymentRequirements, Signer}; +use solana_sdk::pubkey::Pubkey; + +#[tokio::main] +async fn main() { + if let Err(e) = run().await { + eprintln!("{e}"); + exit(1); + } +} + +async fn run() -> Result<(), String> { + let keypair = env_var("COVENANT_X402_ER_KEYPAIR")?; + let program = env_var("COVENANT_X402_ER_PROGRAM")?; + let rpc = env_var("COVENANT_X402_ER_RPC")?; + let program = Pubkey::from_str(&program).map_err(|e| format!("bad program id: {e}"))?; + + let mut input = String::new(); + std::io::stdin() + .read_to_string(&mut input) + .map_err(|e| format!("read stdin: {e}"))?; + let req: PaymentRequirements = + serde_json::from_str(&input).map_err(|e| format!("decode requirement: {e}"))?; + + let signer = EphemeralSigner::from_keypair_file(&keypair, program, rpc) + .map_err(|e| format!("build signer: {e}"))?; + let header = signer + .build_payment(&req) + .await + .map_err(|e| format!("sign: {e}"))?; + println!("{header}"); + Ok(()) +} + +fn env_var(key: &str) -> Result { + std::env::var(key).map_err(|_| format!("{key} not set")) +} diff --git a/agent-os/crates/covenant-x402/src/ephemeral.rs b/agent-os/crates/covenant-x402/src/ephemeral.rs new file mode 100644 index 000000000..6eba927a8 --- /dev/null +++ b/agent-os/crates/covenant-x402/src/ephemeral.rs @@ -0,0 +1,378 @@ +//! Ephemeral-rollup payment signer for the x402 loop. +//! +//! Gated by the `solana` feature. Where [`crate::SolanaSigner`] pays by signing an +//! SPL `TransferChecked`, [`EphemeralSigner`] pays by metering: it runs the Covenant +//! settlement program's `consume_credits` against a credit account that has been +//! delegated to a MagicBlock ephemeral rollup, then returns the ER transaction +//! signature as proof. No SPL transfer, no per-call L1 fee — the consume is gasless +//! in the rollup. +//! +//! Session model: the caller delegates the credit account once (out of band), serves +//! many gasless paid calls through this signer, then undelegates. This signer only +//! does the per-call consume. +//! +//! ## Envelope +//! +//! Same outer shape as the Coinbase x402 reference, ER-specific payload: +//! +//! ```json +//! { +//! "x402Version": 1, +//! "scheme": "", +//! "network": "", +//! "payload": { "signature": "", "nonce": "", +//! "creditAccount": "" } +//! } +//! ``` +//! +//! The facilitator verifies the signature on the ER: the tx is a `consume_credits` +//! to the settlement program against `creditAccount`, `amount >= price`, and +//! `receipt_hash == sha256(nonce)` (binds the payment to this request). The nonce is +//! carried in the 402 challenge's `extra.nonce`. +//! +//! ## RPC +//! +//! Talks directly to the pinned ER validator RPC (where the delegated account lives) +//! with `getLatestBlockhash` / `sendTransaction` / `getSignatureStatuses`, via +//! `reqwest` — no `solana-client` dep, matching [`crate::SolanaSigner`]. + +use std::path::Path; +use std::str::FromStr; +use std::time::Duration; + +use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; +use sha2::{Digest, Sha256}; +use solana_sdk::{ + hash::Hash, + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + signer::{ + keypair::{read_keypair_file, Keypair}, + Signer as SolanaKeypairSigner, + }, + transaction::Transaction, +}; +use tracing::debug; + +use crate::http::{read_capped, MAX_RESPONSE_BYTES}; +use crate::{PaymentRequirements, Result, Signer, X402Error}; + +/// Settlement-program instruction discriminator: `sha256("global:consume_credits")[..8]`. +fn consume_discriminator() -> [u8; 8] { + let digest = Sha256::digest(b"global:consume_credits"); + let mut out = [0u8; 8]; + out.copy_from_slice(&digest[..8]); + out +} + +/// `receipt_hash = sha256(nonce)` — binds an on-chain consume to a specific 402 +/// challenge so a facilitator can match payment to request and reject replays. +fn receipt_hash(nonce: &str) -> [u8; 32] { + let mut out = [0u8; 32]; + out.copy_from_slice(&Sha256::digest(nonce.as_bytes())); + out +} + +/// Builds the `consume_credits(amount, receipt_hash)` instruction. Account order +/// matches the program's `ConsumeCredits`: `config` (ro), `credits` (w), `owner` +/// (signer, ro). PDAs are derived from `program_id`. +pub fn consume_instruction( + program_id: &Pubkey, + owner: &Pubkey, + amount: u64, + receipt_hash: [u8; 32], +) -> Instruction { + let (config, _) = Pubkey::find_program_address(&[b"config"], program_id); + let credits = credit_account(program_id, owner); + let mut data = Vec::with_capacity(48); + data.extend_from_slice(&consume_discriminator()); + data.extend_from_slice(&amount.to_le_bytes()); + data.extend_from_slice(&receipt_hash); + Instruction { + program_id: *program_id, + accounts: vec![ + AccountMeta::new_readonly(config, false), + AccountMeta::new(credits, false), + AccountMeta::new_readonly(*owner, true), + ], + data, + } +} + +/// The credit-account PDA `[b"credits", owner]` for a settlement program. +pub fn credit_account(program_id: &Pubkey, owner: &Pubkey) -> Pubkey { + Pubkey::find_program_address(&[b"credits", owner.as_ref()], program_id).0 +} + +/// Settles x402 calls by metering a delegated credit account in an ephemeral rollup. +pub struct EphemeralSigner { + keypair: Keypair, + program_id: Pubkey, + /// Pinned ER validator RPC where the delegated account lives. + er_rpc_url: String, + http: reqwest::Client, + max_bytes: usize, +} + +impl EphemeralSigner { + pub fn new(keypair: Keypair, program_id: Pubkey, er_rpc_url: impl Into) -> Self { + Self { + keypair, + program_id, + er_rpc_url: er_rpc_url.into(), + http: reqwest::Client::new(), + max_bytes: MAX_RESPONSE_BYTES, + } + } + + pub fn from_keypair_file( + path: impl AsRef, + program_id: Pubkey, + er_rpc_url: impl Into, + ) -> Result { + let path = path.as_ref(); + let keypair = read_keypair_file(path) + .map_err(|e| X402Error::Sign(format!("read funding keypair {}: {e}", path.display())))?; + Ok(Self::new(keypair, program_id, er_rpc_url)) + } + + pub fn pubkey(&self) -> Pubkey { + self.keypair.pubkey() + } + + /// The credit-account PDA this signer meters. + pub fn credit_account(&self) -> Pubkey { + credit_account(&self.program_id, &self.keypair.pubkey()) + } + + async fn rpc(&self, body: serde_json::Value) -> Result { + let resp = self.http.post(&self.er_rpc_url).json(&body).send().await?; + let status = resp.status(); + if !status.is_success() { + let text = read_capped(resp, self.max_bytes, X402Error::Sign) + .await + .unwrap_or_default(); + return Err(X402Error::Sign(format!("ER rpc status {status}: {text}"))); + } + let text = read_capped(resp, self.max_bytes, X402Error::Sign).await?; + let parsed: serde_json::Value = serde_json::from_str(&text) + .map_err(|e| X402Error::Sign(format!("decode ER rpc response: {e}")))?; + if let Some(err) = parsed.get("error") { + return Err(X402Error::Sign(format!("ER rpc error: {err}"))); + } + Ok(parsed) + } + + async fn latest_blockhash(&self) -> Result { + let parsed = self + .rpc(serde_json::json!({ + "jsonrpc": "2.0", "id": 1, "method": "getLatestBlockhash", + "params": [{"commitment": "confirmed"}] + })) + .await?; + let s = parsed + .pointer("/result/value/blockhash") + .and_then(|v| v.as_str()) + .ok_or_else(|| X402Error::Sign(format!("ER rpc: no blockhash: {parsed}")))?; + Hash::from_str(s).map_err(|e| X402Error::Sign(format!("parse blockhash: {e}"))) + } + + /// Submits the signed consume to the ER and waits for confirmation, so a + /// facilitator querying the signature right away will find it. + async fn submit_and_confirm(&self, tx: &Transaction) -> Result { + let serialized = + bincode::serialize(tx).map_err(|e| X402Error::Sign(format!("serialize tx: {e}")))?; + let tx_b64 = BASE64.encode(serialized); + let parsed = self + .rpc(serde_json::json!({ + "jsonrpc": "2.0", "id": 1, "method": "sendTransaction", + "params": [tx_b64, {"encoding": "base64", "preflightCommitment": "confirmed"}] + })) + .await?; + let sig = parsed + .pointer("/result") + .and_then(|v| v.as_str()) + .ok_or_else(|| X402Error::Sign(format!("ER rpc: no signature: {parsed}")))? + .to_string(); + + for _ in 0..25 { + let parsed = self + .rpc(serde_json::json!({ + "jsonrpc": "2.0", "id": 1, "method": "getSignatureStatuses", + "params": [[sig], {"searchTransactionHistory": false}] + })) + .await?; + let status = parsed.pointer("/result/value/0"); + if let Some(status) = status.filter(|s| !s.is_null()) { + if let Some(err) = status.get("err").filter(|e| !e.is_null()) { + return Err(X402Error::Sign(format!("consume failed on ER: {err}"))); + } + let level = status + .get("confirmationStatus") + .and_then(|v| v.as_str()) + .unwrap_or(""); + if level == "confirmed" || level == "finalized" { + return Ok(sig); + } + } + tokio::time::sleep(Duration::from_millis(200)).await; + } + Err(X402Error::Sign(format!( + "consume {sig} not confirmed on ER within timeout" + ))) + } +} + +#[async_trait::async_trait] +impl Signer for EphemeralSigner { + async fn build_payment(&self, requirements: &PaymentRequirements) -> Result { + let amount: u64 = requirements + .amount + .parse() + .map_err(|e| X402Error::Sign(format!("parse amount {:?}: {e}", requirements.amount)))?; + let nonce = requirements + .extra + .as_ref() + .and_then(|x| x.nonce.as_deref()) + .ok_or_else(|| { + X402Error::Sign("ER payment requires a nonce in the 402 challenge's extra".into()) + })?; + + let owner = self.keypair.pubkey(); + let ix = consume_instruction(&self.program_id, &owner, amount, receipt_hash(nonce)); + let blockhash = self.latest_blockhash().await?; + let mut tx = Transaction::new_with_payer(&[ix], Some(&owner)); + tx.try_sign(&[&self.keypair], blockhash) + .map_err(|e| X402Error::Sign(format!("sign consume: {e}")))?; + + debug!(payer = %owner, amount, "EphemeralSigner metering consume in ER"); + let signature = self.submit_and_confirm(&tx).await?; + + let envelope = serde_json::json!({ + "x402Version": 1, + "scheme": requirements.scheme, + "network": requirements.network, + "payload": { + "signature": signature, + "nonce": nonce, + "creditAccount": self.credit_account().to_string(), + }, + }); + Ok(BASE64.encode(envelope.to_string().as_bytes())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::PaymentExtra; + use wiremock::matchers::{body_string_contains, method}; + use wiremock::{Mock, MockServer, ResponseTemplate}; + + fn keypair() -> Keypair { + Keypair::new() + } + + #[test] + fn consume_instruction_layout() { + let program = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let receipt = [7u8; 32]; + let ix = consume_instruction(&program, &owner, 42, receipt); + + assert_eq!(ix.program_id, program); + assert_eq!(ix.accounts.len(), 3); + // [config (ro), credits (w), owner (signer, ro)] + assert!(!ix.accounts[0].is_writable && !ix.accounts[0].is_signer); + assert!(ix.accounts[1].is_writable && !ix.accounts[1].is_signer); + assert_eq!(ix.accounts[1].pubkey, credit_account(&program, &owner)); + assert!(ix.accounts[2].is_signer && !ix.accounts[2].is_writable); + assert_eq!(ix.accounts[2].pubkey, owner); + + assert_eq!(&ix.data[..8], &consume_discriminator()); + assert_eq!(&ix.data[8..16], &42u64.to_le_bytes()); + assert_eq!(&ix.data[16..48], &receipt); + } + + #[test] + fn receipt_hash_is_sha256_of_nonce() { + let mut expected = [0u8; 32]; + expected.copy_from_slice(&Sha256::digest(b"abc")); + assert_eq!(receipt_hash("abc"), expected); + } + + fn er_requirements(nonce: Option<&str>) -> PaymentRequirements { + PaymentRequirements { + network: "solana-er:devnet".into(), + asset: "credits".into(), + amount: "1".into(), + amount_usdc: 0.0, + pay_to: "credits".into(), + scheme: "exact-er".into(), + extra: nonce.map(|n| PaymentExtra { + fee_payer: None, + nonce: Some(n.into()), + }), + } + } + + #[tokio::test] + async fn missing_nonce_is_a_sign_error() { + let signer = EphemeralSigner::new(keypair(), Pubkey::new_unique(), "http://unused"); + let err = signer + .build_payment(&er_requirements(None)) + .await + .expect_err("must require a nonce"); + assert!(matches!(err, X402Error::Sign(_))); + } + + #[tokio::test] + async fn build_payment_meters_and_wraps_signature() { + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(body_string_contains("getLatestBlockhash")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "jsonrpc": "2.0", "id": 1, + "result": {"context": {"slot": 1}, + "value": {"blockhash": "11111111111111111111111111111111", "lastValidBlockHeight": 1}} + }))) + .mount(&server) + .await; + Mock::given(method("POST")) + .and(body_string_contains("sendTransaction")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "jsonrpc": "2.0", "id": 1, "result": "5SIGtestsignaturevalue" + }))) + .mount(&server) + .await; + Mock::given(method("POST")) + .and(body_string_contains("getSignatureStatuses")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "jsonrpc": "2.0", "id": 1, + "result": {"context": {"slot": 1}, "value": [{"confirmationStatus": "confirmed", "err": null}]} + }))) + .mount(&server) + .await; + + let kp = keypair(); + let owner = kp.pubkey(); + let program = Pubkey::new_unique(); + let signer = EphemeralSigner::new(kp, program, server.uri()); + + let header = signer + .build_payment(&er_requirements(Some("deadbeef"))) + .await + .expect("build_payment"); + + let json: serde_json::Value = + serde_json::from_slice(&BASE64.decode(header).expect("b64")).expect("json"); + assert_eq!(json["x402Version"], 1); + assert_eq!(json["scheme"], "exact-er"); + assert_eq!(json["payload"]["signature"], "5SIGtestsignaturevalue"); + assert_eq!(json["payload"]["nonce"], "deadbeef"); + assert_eq!( + json["payload"]["creditAccount"], + credit_account(&program, &owner).to_string() + ); + } +} diff --git a/agent-os/crates/covenant-x402/src/lib.rs b/agent-os/crates/covenant-x402/src/lib.rs index 6e10478e1..3380540b9 100644 --- a/agent-os/crates/covenant-x402/src/lib.rs +++ b/agent-os/crates/covenant-x402/src/lib.rs @@ -42,6 +42,11 @@ pub use types::{Capability, PaymentExtra, PaymentRequirements}; #[cfg(feature = "solana")] pub mod payai; +#[cfg(feature = "solana")] +pub mod ephemeral; + +#[cfg(feature = "solana")] +pub use ephemeral::EphemeralSigner; #[cfg(feature = "solana")] pub use payai::PayaiSolanaSigner; #[cfg(feature = "solana")] diff --git a/agent-os/crates/covenant-x402/src/payai.rs b/agent-os/crates/covenant-x402/src/payai.rs index 1692331a7..7a2bb5807 100644 --- a/agent-os/crates/covenant-x402/src/payai.rs +++ b/agent-os/crates/covenant-x402/src/payai.rs @@ -446,6 +446,7 @@ mod tests { scheme: "exact".into(), extra: Some(PaymentExtra { fee_payer: Some(PAYAI_FEE_PAYER.into()), + nonce: None, }), }; let err = signer.build_payment(&req).await.expect_err("unknown mint"); @@ -471,12 +472,14 @@ mod tests { scheme: "exact".into(), extra: Some(PaymentExtra { fee_payer: Some(PAYAI_FEE_PAYER.into()), + nonce: None, }), }; let mut req = valid(); req.extra = Some(PaymentExtra { fee_payer: Some("not-a-pubkey".into()), + nonce: None, }); assert!(matches!( signer.build_payment(&req).await.expect_err("bad feePayer"), @@ -596,6 +599,7 @@ mod tests { scheme: "exact".into(), extra: Some(PaymentExtra { fee_payer: Some(PAYAI_FEE_PAYER.into()), + nonce: None, }), }; // account_exists is the first RPC; its Ok(false) returns before diff --git a/agent-os/crates/covenant-x402/src/types.rs b/agent-os/crates/covenant-x402/src/types.rs index d91274542..5a2bf86c7 100644 --- a/agent-os/crates/covenant-x402/src/types.rs +++ b/agent-os/crates/covenant-x402/src/types.rs @@ -57,6 +57,13 @@ pub struct PaymentExtra { /// partial-signs as the funder only. #[serde(rename = "feePayer", default, skip_serializing_if = "Option::is_none")] pub fee_payer: Option, + /// Per-request nonce for ER-settled (`exact-er`) payments. The + /// `EphemeralSigner` binds the on-chain consume to this request via + /// `receipt_hash = sha256(nonce)`; the facilitator checks the match and + /// rejects replays. Absent for SPL flows, so the on-wire shape there is + /// unchanged. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub nonce: Option, } /// A daemon-issued authorization to call paid endpoints. @@ -151,6 +158,7 @@ mod tests { // the Rust `fee_payer`, and survive a round-trip unchanged. let original = requirement(Some(PaymentExtra { fee_payer: Some("2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHDBg4".into()), + nonce: None, })); let json = serde_json::to_value(&original).expect("encode"); assert_eq!( diff --git a/agent-os/crates/covenant-zauth/src/challenge.rs b/agent-os/crates/covenant-zauth/src/challenge.rs index 9e80d859f..6e0f3daca 100644 --- a/agent-os/crates/covenant-zauth/src/challenge.rs +++ b/agent-os/crates/covenant-zauth/src/challenge.rs @@ -154,6 +154,7 @@ pub fn to_payment_requirements(accept: &Accept) -> PaymentRequirements { .and_then(|e| e.fee_payer.clone()) .map(|fee_payer| PaymentExtra { fee_payer: Some(fee_payer), + nonce: None, }), } } diff --git a/agent-os/crates/covenantd/src/er_provider.rs b/agent-os/crates/covenantd/src/er_provider.rs new file mode 100644 index 000000000..95fc49325 --- /dev/null +++ b/agent-os/crates/covenantd/src/er_provider.rs @@ -0,0 +1,121 @@ +//! Local registry of ER-settled x402 providers, exposed to agents as tools. +//! +//! An operator registers a provider (an x402 endpoint that settles in a MagicBlock +//! ephemeral rollup) once, via config. Each registered provider appears in the agent +//! tool list as `er.` and, when called, routes through the daemon's x402 pay +//! path. Because the provider's `network` is `solana-er:*`, +//! [`crate::x402::X402Config::signer_for`] selects the ER signer sidecar, so the call +//! settles by metering credits in the rollup. The capability (`tool.call.er.`), +//! budget debit, settlement receipt, and audit event are the same ones every other +//! tool and paid call use. + +use covenant_mcp::ToolSpec; +use serde::Deserialize; +use serde_json::json; + +/// A registered ER-settled provider. `network` should be a `solana-er:*` CAIP-2 id +/// so the daemon routes the call to the ER signer sidecar. Deserializes from the +/// operator's registry config; only `slug`, `endpoint`, and `per_call_cap` are +/// required (the rest default to a GET call on `solana-er:devnet` in `credits`). +#[derive(Debug, Clone, Deserialize)] +pub struct ErProvider { + pub slug: String, + pub endpoint: String, + /// HTTP method for the call (usually GET). + #[serde(default = "default_method")] + pub method: String, + #[serde(default = "default_network")] + pub network: String, + #[serde(default = "default_asset")] + pub asset: String, + /// Atomic per-call price cap (credits), the amount advertised to the signer. + pub per_call_cap: u128, + /// USD-pegged budget cost debited from the caller per call. + #[serde(default = "default_credits")] + pub credits: u64, + #[serde(default)] + pub description: String, +} + +fn default_method() -> String { + "GET".into() +} +fn default_network() -> String { + "solana-er:devnet".into() +} +fn default_asset() -> String { + "credits".into() +} +fn default_credits() -> u64 { + 1 +} + +impl ErProvider { + pub fn tool_name(&self) -> String { + format!("er.{}", self.slug) + } +} + +/// Tool specs for the registered ER providers: one `er.` per provider. +pub fn er_specs(providers: &[ErProvider]) -> Vec { + providers + .iter() + .map(|p| ToolSpec { + name: p.tool_name(), + description: format!( + "{} Settled by gasless credit metering in a MagicBlock ephemeral rollup \ + ({} {} per call).", + p.description, p.per_call_cap, p.asset + ), + input_schema: json!({ + "type": "object", + "properties": { + "body": { + "type": "object", + "description": "Optional JSON body forwarded to the endpoint." + } + } + }), + }) + .collect() +} + +/// Resolve an `er.` tool name to its registered provider. +pub fn find<'a>(providers: &'a [ErProvider], tool_name: &str) -> Option<&'a ErProvider> { + let slug = tool_name.strip_prefix("er.")?; + providers.iter().find(|p| p.slug == slug) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sample() -> Vec { + vec![ErProvider { + slug: "quote".into(), + endpoint: "http://127.0.0.1:8402/paid".into(), + method: "GET".into(), + network: "solana-er:devnet".into(), + asset: "credits".into(), + per_call_cap: 100, + credits: 1, + description: "A market quote.".into(), + }] + } + + #[test] + fn specs_name_tools_by_slug() { + let specs = er_specs(&sample()); + assert_eq!(specs.len(), 1); + assert_eq!(specs[0].name, "er.quote"); + assert!(specs[0].description.contains("ephemeral rollup")); + } + + #[test] + fn find_resolves_prefixed_name_and_rejects_unknown() { + let p = sample(); + assert_eq!(find(&p, "er.quote").unwrap().slug, "quote"); + assert!(find(&p, "er.missing").is_none()); + assert!(find(&p, "quote").is_none()); // must carry the er. prefix + } +} diff --git a/agent-os/crates/covenantd/src/hyre.rs b/agent-os/crates/covenantd/src/hyre.rs index 8fd0787db..c26f6b0a3 100644 --- a/agent-os/crates/covenantd/src/hyre.rs +++ b/agent-os/crates/covenantd/src/hyre.rs @@ -185,6 +185,7 @@ mod tests { enabled: true, signer_binary: "/nonexistent-signer".into(), signer_env: vec![], + ..Default::default() } } diff --git a/agent-os/crates/covenantd/src/lib.rs b/agent-os/crates/covenantd/src/lib.rs index 3610a4ad7..5fa2d694a 100644 --- a/agent-os/crates/covenantd/src/lib.rs +++ b/agent-os/crates/covenantd/src/lib.rs @@ -9,6 +9,7 @@ #![deny(unsafe_code)] pub mod http; +pub mod er_provider; pub mod hyre; pub mod metaplex; pub mod secret; @@ -1212,6 +1213,10 @@ pub struct Server { /// None when the operator has not enabled Hyre; in that state no /// `hyre.*` tool is advertised or callable. hyre: Option>, + /// Locally-registered ER-settled x402 providers, advertised as `er.` + /// tools. `None`/empty means none registered. Calls route through the x402 pay + /// path with the ER signer sidecar selected by the `solana-er:*` network. + er_providers: Option>>, /// Opt-in Metaplex profile: config + a DAS read client. None when /// the operator has not enabled Metaplex; in that state no /// `metaplex.*` tool is advertised or callable. @@ -1268,6 +1273,7 @@ impl Server { x402_dispatch: None, secret_source: None, hyre: None, + er_providers: None, metaplex: None, sap_bridge: None, intent_outcomes: Arc::new(std::sync::Mutex::new(OutcomeStore::default())), @@ -1361,6 +1367,15 @@ impl Server { self } + /// Register ER-settled x402 providers. Each is advertised as an `er.` + /// tool and routes through the x402 pay path with the ER signer sidecar. + /// Requires the x402 dispatch + ER signer to be wired via + /// [`Self::with_x402_dispatch`]. + pub fn with_er_providers(mut self, providers: Vec) -> Self { + self.er_providers = Some(Arc::new(providers)); + self + } + /// Enable the Metaplex profile. Advertises the `metaplex.das.*` read /// tools whenever a DAS endpoint is configured, and the /// `metaplex.attest.*` / `metaplex.identity.*` write tools whenever @@ -4041,6 +4056,9 @@ impl Server { if let Some(state) = &self.metaplex { tools.extend(covenant_metaplex::metaplex_specs(&state.config)); } + if let Some(providers) = &self.er_providers { + tools.extend(er_provider::er_specs(providers)); + } Response::ToolList { tools } } @@ -4106,6 +4124,9 @@ impl Server { if name.starts_with("hyre.") { return self.hyre_tool_call(name, arguments, peer).await; } + if name.starts_with("er.") { + return self.er_tool_call(name, arguments, peer).await; + } if name.starts_with("metaplex.") { return self.metaplex_tool_call(name, arguments).await; } @@ -4225,6 +4246,106 @@ impl Server { } } + /// Execute a registered ER-settled provider tool (`er.`). The + /// `tool.call.` capability and scope are already enforced by + /// [`Self::call_tool`]. Routes through the same `pay_and_record` path as every + /// outbound x402 payment; the provider's `solana-er:*` network selects the ER + /// signer sidecar, so the call settles by metering credits in the rollup. + async fn er_tool_call( + &self, + name: String, + arguments: serde_json::Value, + peer: &AgentId, + ) -> Response { + let Some(providers) = self.er_providers.clone() else { + return Response::Error { + message: "no ER providers are registered on this daemon.".into(), + }; + }; + let Some(provider) = er_provider::find(&providers, &name) else { + return Response::Error { + message: format!("unknown ER provider tool: {name}"), + }; + }; + let Some(config) = self.x402_dispatch.clone() else { + return Response::Error { + message: "ER providers require the x402 funding-key sidecar. \ + Wire it via Server::with_x402_dispatch and restart." + .into(), + }; + }; + if !config.enabled { + return Response::Error { + message: "x402 dispatch is disabled in this daemon's config.".into(), + }; + } + let http_method = match provider.method.parse::() { + Ok(m) => m, + Err(_) => { + return Response::Error { + message: format!( + "ER provider {} has an invalid HTTP method: {:?}", + provider.slug, provider.method + ), + } + } + }; + + let body = arguments.get("body").cloned(); + let signer = config.signer_for(&provider.network); + let capability = covenant_x402::Capability { + provider: provider.slug.clone(), + network: provider.network.clone(), + asset: provider.asset.clone(), + per_call_cap: provider.per_call_cap, + }; + let call = x402::PaidCall { + provider: &provider.slug, + endpoint: &provider.endpoint, + method: http_method, + capability, + body: body.as_ref(), + amount: provider.per_call_cap.to_string(), + network: provider.network.clone(), + asset: provider.asset.clone(), + credits: provider.credits, + }; + let issuer = self.identity.agent_id(); + let ctx = x402::SettlementContext { + settlement: self.settlement.as_ref(), + audit: self.audit.as_ref(), + budget: self.budget.as_ref(), + issuer: &issuer, + }; + let client = covenant_x402::Client::new(covenant_x402::http_client()); + let outcome = + match x402::pay_and_record(&ctx, &config, &client, &signer, peer, &call).await { + Ok(o) => o, + Err(e) => { + return Response::Error { + message: format!("ER provider call failed: {e}"), + } + } + }; + let status = outcome.response.status().as_u16(); + let body_text = match x402::read_response_body(outcome.response).await { + Ok(t) => t, + Err(e) => { + return Response::Error { + message: format!("read upstream body: {e}"), + } + } + }; + let content = match serde_json::from_str::(&body_text) { + Ok(v) => vec![covenant_mcp::Content::json(v)], + Err(_) => vec![covenant_mcp::Content::text(body_text)], + }; + Response::ToolResult { + content, + is_error: !(200..300).contains(&status), + } + } + /// Execute a Metaplex tool on the caller's behalf. The /// `tool.call.` capability and scope are already enforced by /// [`Self::call_tool`]. Reads run a DAS query; writes are delegated @@ -6053,10 +6174,9 @@ impl Server { } }; - let mut signer = x402::SubprocessSigner::new(&config.signer_binary); - for (k, v) in &config.signer_env { - signer = signer.env(k.clone(), v.clone()); - } + // ER-settled providers (network solana-er:*) route to the ER signer + // sidecar when configured; everything else uses the default SPL signer. + let signer = config.signer_for(&network); let capability = covenant_x402::Capability { provider: provider.clone(), @@ -47522,6 +47642,7 @@ required = {caps:?} enabled: true, signer_binary: signer, signer_env: vec![], + ..Default::default() }) .with_hyre(hyre::HyreState::new(catalog, cfg)); @@ -58068,6 +58189,7 @@ budget_credits_per_hour = {credits} enabled: true, signer_binary: std::path::PathBuf::from("/bin/true"), signer_env: vec![], + ..Default::default() }); let resp = s.op_respond(pay_x402_req()).await; match resp { @@ -58114,6 +58236,203 @@ budget_credits_per_hour = {credits} } } + /// Live end-to-end: a PayX402 op on a `solana-er:*` network drives the full + /// daemon path -- op_respond -> pay_x402 -> X402Config::signer_for routes to the + /// ER signer sidecar -> the sidecar runs consume_credits in the ER -> the + /// facilitator verifies the on-chain proof -> 200, and a settlement receipt is + /// recorded. Needs the ER signer + facilitator binaries, a delegated credit + /// account for ER_KEYPAIR's owner, and the devnet ER. Run with: + /// ER_SIGNER_BIN=... FACILITATOR_BIN=... ER_KEYPAIR=... \ + /// cargo test -p covenantd --lib pay_x402_settles_through_the_er_signer_live -- --ignored --nocapture + #[tokio::test] + #[ignore = "live: needs ER signer + facilitator binaries, a delegated credit account, and the devnet ER"] + async fn pay_x402_settles_through_the_er_signer_live() { + use std::process::{Command, Stdio}; + let er_signer = std::env::var("ER_SIGNER_BIN").expect("ER_SIGNER_BIN"); + let facilitator = std::env::var("FACILITATOR_BIN").expect("FACILITATOR_BIN"); + let keypair = std::env::var("ER_KEYPAIR").expect("ER_KEYPAIR"); + let program = std::env::var("ER_PROGRAM") + .unwrap_or_else(|_| "cov9UDypG7nsryxdgMcKhKU2spRVWLVjxT2iTv6do5Y".into()); + let rpc = + std::env::var("ER_RPC").unwrap_or_else(|_| "https://devnet-eu.magicblock.app".into()); + let port = std::env::var("FACILITATOR_PORT").unwrap_or_else(|_| "8499".into()); + let endpoint = format!("http://127.0.0.1:{port}/paid"); + + let mut fac = Command::new(&facilitator) + .env("PRICE", "1") + .env("PORT", &port) + .env("PROGRAM", &program) + .env("ER", &rpc) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .expect("spawn facilitator"); + let mut ready = false; + for _ in 0..50 { + if std::net::TcpStream::connect(format!("127.0.0.1:{port}")).is_ok() { + ready = true; + break; + } + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + } + assert!(ready, "facilitator did not start listening on {port}"); + + let audit = Arc::new(covenant_audit::InMemoryAuditLog::new()); + let budget = Arc::new(covenant_budget::InMemoryLedger::new()); + let s = server_with_audit_and_budget(audit.clone(), budget.clone()).with_x402_dispatch( + x402::X402Config { + enabled: true, + signer_binary: std::path::PathBuf::from("/bin/true"), + signer_env: vec![], + er_signer_binary: Some(er_signer.into()), + er_signer_env: vec![ + ("COVENANT_X402_ER_KEYPAIR".into(), keypair), + ("COVENANT_X402_ER_PROGRAM".into(), program), + ("COVENANT_X402_ER_RPC".into(), rpc), + ], + }, + ); + let payer = s.identity.agent_id(); + budget.set_capacity(&payer, 1_000_000).await.unwrap(); + grant_action(&s, "x402.outbound.pay").await; + + let resp = s + .op_respond(Request::PayX402 { + provider: "covenant-er".into(), + endpoint: endpoint.clone(), + method: "GET".into(), + body: None, + network: "solana-er:devnet".into(), + asset: "credits".into(), + per_call_cap: "100".into(), + credits: 1, + }) + .await; + + let _ = fac.kill(); + + match resp { + Response::X402Paid { + status, + body, + receipt_id, + } => { + assert_eq!(status, 200, "expected 200 from facilitator; body: {body}"); + assert!(body.contains("quote"), "facilitator content missing: {body}"); + assert_ne!(receipt_id, Uuid::nil(), "a receipt id must be assigned"); + } + other => panic!("expected X402Paid, got: {other:?}"), + } + let receipts = s.settlement.recent(10).await.expect("settlement recent"); + assert!( + !receipts.is_empty(), + "the paid call must record a settlement receipt" + ); + } + + /// Live: an agent calls a *registered* ER provider as a tool. The provider is + /// registered once via `with_er_providers`, shows up in the tool list as + /// `er.quote`, and a `CallTool` drives call_tool -> er_tool_call -> the ER + /// signer sidecar -> consume_credits in the ER -> the facilitator -> 200, with a + /// settlement receipt recorded. Same external prerequisites as + /// pay_x402_settles_through_the_er_signer_live. + #[tokio::test] + #[ignore = "live: needs ER signer + facilitator binaries, a delegated credit account, and the devnet ER"] + async fn er_provider_tool_settles_in_the_er_live() { + use std::process::{Command, Stdio}; + let er_signer = std::env::var("ER_SIGNER_BIN").expect("ER_SIGNER_BIN"); + let facilitator = std::env::var("FACILITATOR_BIN").expect("FACILITATOR_BIN"); + let keypair = std::env::var("ER_KEYPAIR").expect("ER_KEYPAIR"); + let program = std::env::var("ER_PROGRAM") + .unwrap_or_else(|_| "cov9UDypG7nsryxdgMcKhKU2spRVWLVjxT2iTv6do5Y".into()); + let rpc = + std::env::var("ER_RPC").unwrap_or_else(|_| "https://devnet-eu.magicblock.app".into()); + let port = std::env::var("FACILITATOR_PORT").unwrap_or_else(|_| "8497".into()); + let endpoint = format!("http://127.0.0.1:{port}/paid"); + + let mut fac = Command::new(&facilitator) + .env("PRICE", "1") + .env("PORT", &port) + .env("PROGRAM", &program) + .env("ER", &rpc) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .expect("spawn facilitator"); + let mut ready = false; + for _ in 0..50 { + if std::net::TcpStream::connect(format!("127.0.0.1:{port}")).is_ok() { + ready = true; + break; + } + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + } + assert!(ready, "facilitator did not start listening on {port}"); + + let audit = Arc::new(covenant_audit::InMemoryAuditLog::new()); + let budget = Arc::new(covenant_budget::InMemoryLedger::new()); + let s = server_with_audit_and_budget(audit.clone(), budget.clone()) + .with_x402_dispatch(x402::X402Config { + enabled: true, + signer_binary: std::path::PathBuf::from("/bin/true"), + signer_env: vec![], + er_signer_binary: Some(er_signer.into()), + er_signer_env: vec![ + ("COVENANT_X402_ER_KEYPAIR".into(), keypair), + ("COVENANT_X402_ER_PROGRAM".into(), program), + ("COVENANT_X402_ER_RPC".into(), rpc), + ], + }) + .with_er_providers(vec![er_provider::ErProvider { + slug: "quote".into(), + endpoint: endpoint.clone(), + method: "GET".into(), + network: "solana-er:devnet".into(), + asset: "credits".into(), + per_call_cap: 100, + credits: 1, + description: "A market quote.".into(), + }]); + + let payer = s.identity.agent_id(); + budget.set_capacity(&payer, 1_000_000).await.unwrap(); + grant_action(&s, "tool.call.er.quote").await; + + // Registration is discoverable: the provider shows up in the tool list. + match s.op_respond(Request::ListTools).await { + Response::ToolList { tools } => assert!( + tools.iter().any(|t| t.name == "er.quote"), + "registered ER provider must be advertised as er.quote" + ), + other => panic!("expected ToolList, got: {other:?}"), + } + + let resp = s + .op_respond(Request::CallTool { + name: "er.quote".into(), + arguments: serde_json::json!({}), + }) + .await; + + let _ = fac.kill(); + + match resp { + Response::ToolResult { content, is_error } => { + assert!(!is_error, "tool call should succeed; content: {content:?}"); + assert!( + format!("{content:?}").contains("quote"), + "facilitator content missing: {content:?}" + ); + } + other => panic!("expected ToolResult, got: {other:?}"), + } + let receipts = s.settlement.recent(10).await.expect("settlement recent"); + assert!( + !receipts.is_empty(), + "the paid tool call must record a settlement receipt" + ); + } + #[tokio::test] async fn pay_x402_enforces_destination_scope_and_audits_denied_provider() { // A provider-scoped x402.outbound.pay grant is least-privilege egress: @@ -58125,6 +58444,7 @@ budget_credits_per_hour = {credits} enabled: true, signer_binary: std::path::PathBuf::from("/bin/true"), signer_env: vec![], + ..Default::default() }); grant_scoped_action( &s, diff --git a/agent-os/crates/covenantd/src/main.rs b/agent-os/crates/covenantd/src/main.rs index f7dd37ca6..a91dc52c9 100644 --- a/agent-os/crates/covenantd/src/main.rs +++ b/agent-os/crates/covenantd/src/main.rs @@ -320,6 +320,16 @@ async fn main() -> Result<()> { None => server, }; + let server = { + let providers = er_providers_from_env(); + if providers.is_empty() { + server + } else { + info!(count = providers.len(), "ER x402 providers registered"); + server.with_er_providers(providers) + } + }; + let server = match hyre_config_from_env() { Some(cfg) => { // Prefer the live manifest so a restart picks up Hyre's @@ -725,13 +735,52 @@ fn x402_dispatch_config_from_env() -> Option { signer_env.push((key.to_string(), v)); } } + // Optional ER signer sidecar for ephemeral-rollup-settled providers. When the + // binary is set, calls on a solana-er:* network route to it with the ER funding + // key, settlement program, and ER RPC from these vars. + let er_signer_binary = std::env::var("COVENANT_X402_ER_SIGNER_BINARY") + .ok() + .map(std::path::PathBuf::from); + let mut er_signer_env = Vec::new(); + if er_signer_binary.is_some() { + for key in [ + "COVENANT_X402_ER_KEYPAIR", + "COVENANT_X402_ER_PROGRAM", + "COVENANT_X402_ER_RPC", + ] { + if let Ok(v) = std::env::var(key) { + er_signer_env.push((key.to_string(), v)); + } + } + } Some(covenantd::x402::X402Config { enabled: true, signer_binary, signer_env, + er_signer_binary, + er_signer_env, }) } +/// Registered ER-settled x402 providers from `COVENANT_X402_ER_PROVIDERS`, a JSON +/// array of `{slug, endpoint, per_call_cap, [method, network, asset, credits, +/// description]}`. Each becomes an `er.` tool. Unset or empty means none. +fn er_providers_from_env() -> Vec { + let Ok(raw) = std::env::var("COVENANT_X402_ER_PROVIDERS") else { + return Vec::new(); + }; + match serde_json::from_str::>(&raw) { + Ok(providers) => providers, + Err(e) => { + tracing::warn!( + error = %e, + "COVENANT_X402_ER_PROVIDERS did not parse as a provider array; no ER providers registered" + ); + Vec::new() + } + } +} + /// Build the Hyre provider config from env, or None when the operator /// hasn't opted in. The catalog itself loads from the vendored manifest; /// these vars only tune the rail and the spend policy. diff --git a/agent-os/crates/covenantd/src/x402.rs b/agent-os/crates/covenantd/src/x402.rs index 03bf5b712..1ddc9c54b 100644 --- a/agent-os/crates/covenantd/src/x402.rs +++ b/agent-os/crates/covenantd/src/x402.rs @@ -49,6 +49,39 @@ pub struct X402Config { pub enabled: bool, pub signer_binary: PathBuf, pub signer_env: Vec<(String, String)>, + /// Optional ER signer sidecar for ephemeral-rollup-settled providers (CAIP-2 + /// network `solana-er:*`). When set, calls on an ER network route here instead + /// of `signer_binary` — the sidecar runs `consume_credits` in the ER and returns + /// the signature envelope. Its funding key + program + ER RPC live in + /// `er_signer_env`, never in the daemon. + pub er_signer_binary: Option, + pub er_signer_env: Vec<(String, String)>, +} + +impl X402Config { + /// The signer binary + env to use for a payment `network`: the ER sidecar for + /// `solana-er:*` when configured, otherwise the default SPL signer. + fn signer_parts_for(&self, network: &str) -> (&PathBuf, &[(String, String)]) { + if network.starts_with("solana-er:") { + if let Some(bin) = self.er_signer_binary.as_ref() { + return (bin, &self.er_signer_env); + } + } + (&self.signer_binary, &self.signer_env) + } + + /// Builds the subprocess signer for a payment `network`. ER networks route to + /// the ER sidecar when one is configured; everything else uses the default + /// signer. Only the binary path and env cross into the spawned process — the + /// funding key stays in the sidecar's address space. + pub fn signer_for(&self, network: &str) -> SubprocessSigner { + let (bin, env) = self.signer_parts_for(network); + let mut signer = SubprocessSigner::new(bin); + for (k, v) in env { + signer = signer.env(k.clone(), v.clone()); + } + signer + } } /// A [`Signer`] that delegates to the standalone `covenant-x402-signer` @@ -516,6 +549,30 @@ mod tests { use covenant_budget::InMemoryLedger; use covenant_settlement::InMemorySettlement; + #[test] + fn signer_selection_routes_er_networks_to_the_er_sidecar() { + let cfg = X402Config { + enabled: true, + signer_binary: "/spl-signer".into(), + signer_env: vec![], + er_signer_binary: Some("/er-signer".into()), + er_signer_env: vec![("COVENANT_X402_ER_PROGRAM".into(), "cov9".into())], + }; + // ER network -> ER sidecar + its env. + let (bin, env) = cfg.signer_parts_for("solana-er:devnet"); + assert_eq!(bin, &PathBuf::from("/er-signer")); + assert_eq!(env.len(), 1); + // Non-ER network -> default SPL signer. + assert_eq!(cfg.signer_parts_for("solana:mainnet").0, &PathBuf::from("/spl-signer")); + + // With no ER signer configured, an ER network falls back to the default. + let cfg = X402Config { + er_signer_binary: None, + ..cfg + }; + assert_eq!(cfg.signer_parts_for("solana-er:devnet").0, &PathBuf::from("/spl-signer")); + } + fn agent(tag: u8) -> AgentId { AgentId::new("payer@local", [tag; 32]) } diff --git a/agent-os/programs/settlement-ephemeral/.gitignore b/agent-os/programs/settlement-ephemeral/.gitignore new file mode 100644 index 000000000..272925530 --- /dev/null +++ b/agent-os/programs/settlement-ephemeral/.gitignore @@ -0,0 +1,2 @@ +/target/ +spike/node_modules/ diff --git a/agent-os/programs/settlement-ephemeral/Cargo.lock b/agent-os/programs/settlement-ephemeral/Cargo.lock new file mode 100644 index 000000000..6760d26d0 --- /dev/null +++ b/agent-os/programs/settlement-ephemeral/Cargo.lock @@ -0,0 +1,4411 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes-gcm-siv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae0784134ba9375416d469ec31e7c5f9fa94405049cf08c5ce5b4698be673e0d" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "polyval", + "subtle", + "zeroize", +] + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anchor-attribute-access-control" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f70fd141a4d18adf11253026b32504f885447048c7494faf5fa83b01af9c0cf" +dependencies = [ + "anchor-syn", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-account" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "715a261c57c7679581e06f07a74fa2af874ac30f86bd8ea07cca4a7e5388a064" +dependencies = [ + "anchor-syn", + "bs58", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-constant" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "730d6df8ae120321c5c25e0779e61789e4b70dc8297102248902022f286102e4" +dependencies = [ + "anchor-syn", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-error" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27e6e449cc3a37b2880b74dcafb8e5a17b954c0e58e376432d7adc646fb333ef" +dependencies = [ + "anchor-syn", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-event" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7710e4c54adf485affcd9be9adec5ef8846d9c71d7f31e16ba86ff9fc1dd49f" +dependencies = [ + "anchor-syn", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-program" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ecfd49b2aeadeb32f35262230db402abed76ce87e27562b34f61318b2ec83c" +dependencies = [ + "anchor-lang-idl", + "anchor-syn", + "anyhow", + "bs58", + "heck 0.3.3", + "proc-macro2", + "quote", + "serde_json", + "syn 1.0.109", +] + +[[package]] +name = "anchor-derive-accounts" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be89d160793a88495af462a7010b3978e48e30a630c91de47ce2c1d3cb7a6149" +dependencies = [ + "anchor-syn", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-derive-serde" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abc6ee78acb7bfe0c2dd2abc677aaa4789c0281a0c0ef01dbf6fe85e0fd9e6e4" +dependencies = [ + "anchor-syn", + "borsh-derive-internal", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-derive-space" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134a01c0703f6fd355a0e472c033f6f3e41fac1ef6e370b20c50f4c8d022cea7" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-lang" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6bab117055905e930f762c196e08f861f8dfe7241b92cee46677a3b15561a0a" +dependencies = [ + "anchor-attribute-access-control", + "anchor-attribute-account", + "anchor-attribute-constant", + "anchor-attribute-error", + "anchor-attribute-event", + "anchor-attribute-program", + "anchor-derive-accounts", + "anchor-derive-serde", + "anchor-derive-space", + "anchor-lang-idl", + "base64 0.21.7", + "bincode", + "borsh 0.10.4", + "bytemuck", + "solana-program 2.3.0", + "thiserror 1.0.69", +] + +[[package]] +name = "anchor-lang-idl" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308910c7570f3598e631648dd33ce1a6016cfcb019542667f696b0ffb37e7001" +dependencies = [ + "anchor-lang-idl-spec", + "anyhow", + "heck 0.3.3", + "regex", + "serde", + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "anchor-lang-idl-spec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bdf143115440fe621bdac3a29a1f7472e09f6cd82b2aa569429a0c13f103838" +dependencies = [ + "anyhow", + "serde", +] + +[[package]] +name = "anchor-spl" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c08cb5d762c0694f74bd02c9a5b04ea53cefc496e2c27b3234acffca5cd076b" +dependencies = [ + "anchor-lang", + "spl-associated-token-account", + "spl-pod", + "spl-token", + "spl-token-2022", + "spl-token-group-interface", + "spl-token-metadata-interface", +] + +[[package]] +name = "anchor-syn" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dc7a6d90cc643df0ed2744862cdf180587d1e5d28936538c18fc8908489ed67" +dependencies = [ + "anyhow", + "bs58", + "cargo_toml", + "heck 0.3.3", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.9", + "syn 1.0.109", + "thiserror 1.0.69", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "borsh" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115e54d64eb62cdebad391c19efc9dce4981c690c85a33a12199d99bb9546fee" +dependencies = [ + "borsh-derive 0.10.4", + "hashbrown 0.12.3", +] + +[[package]] +name = "borsh" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" +dependencies = [ + "borsh-derive 1.6.1", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831213f80d9423998dd696e2c5345aba6be7a0bd8cd19e31c5243e13df1cef89" +dependencies = [ + "borsh-derive-internal", + "borsh-schema-derive-internal", + "proc-macro-crate 0.1.5", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "borsh-derive" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" +dependencies = [ + "once_cell", + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "borsh-derive-internal" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65d6ba50644c98714aa2a70d13d7df3cd75cd2b523a2b452bf010443800976b3" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "borsh-schema-derive-internal" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276691d96f063427be83e6692b86148e488ebba9f48f77788724ca027ba3b6d4" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8834bb1d8ee5dc048ee3124f2c7c1afcc6bc9aed03f11e9dfd8c69470a5db340" +dependencies = [ + "feature-probe", + "serde", +] + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cargo_toml" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a98356df42a2eb1bd8f1793ae4ee4de48e384dd974ce5eac8eee802edb7492be" +dependencies = [ + "serde", + "toml 0.8.23", +] + +[[package]] +name = "cc" +version = "1.2.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "console_log" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89f72f65e8501878b8a004d5a1afb780987e2ce2b4532c562e367a72c57499f" +dependencies = [ + "log", + "web-sys", +] + +[[package]] +name = "const-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c06f1eb05f06cf2e380fdded278fbf056a38974299d77960555a311dcf91a52" +dependencies = [ + "keccak-const", + "sha2-const-stable", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "covenant-settlement-program-er" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "anchor-spl", + "ephemeral-rollups-sdk", + "solana-security-txt", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rand_core 0.6.4", + "rustc_version", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.118", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "derivation-path" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e5c37193a1db1d8ed868c03ec7b152175f26160a5b740e5e484143877e0adf0" + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "ephemeral-rollups-sdk" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4eab07971a4a45ffee2946bddcced0e11d8e0d1f47c5a4846370cb0e52a1503" +dependencies = [ + "anchor-lang", + "base64ct", + "bincode", + "bytemuck", + "ephemeral-rollups-sdk-attribute-action", + "ephemeral-rollups-sdk-attribute-commit", + "ephemeral-rollups-sdk-attribute-delegate", + "ephemeral-rollups-sdk-attribute-ephemeral", + "ephemeral-rollups-sdk-attribute-ephemeral-accounts", + "ephemeral-vrf-sdk", + "ephemeral-vrf-sdk-vrf-macro", + "five8 0.2.1", + "getrandom 0.2.17", + "magicblock-delegation-program-api", + "magicblock-magic-program-api", + "solana-account-info 2.3.0", + "solana-address 2.6.1", + "solana-program 2.3.0", + "solana-program 3.0.0", + "solana-program-error 2.2.2", + "solana-system-interface 2.0.0", +] + +[[package]] +name = "ephemeral-rollups-sdk-attribute-action" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9bd58b232c61df82655aa3c22fa078ed9f3ea5c788b101a293c31027db4d58" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ephemeral-rollups-sdk-attribute-commit" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531470c07a97f468392e979a2e0dd7ccd8ce991785966db9db63519b32185017" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ephemeral-rollups-sdk-attribute-delegate" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc8fdccd70d8ca73304384e5b1aa971155c25fe942a5dcd05922aba987cf695e" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ephemeral-rollups-sdk-attribute-ephemeral" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a656a4b979b090a6b01abfc253501518efc1c6abf9ddd67b2517bf0c32a2f7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ephemeral-rollups-sdk-attribute-ephemeral-accounts" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de2b6566f45f6358d658c38e5d01609bca32c80e5f689b6e632eb7754b2183c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ephemeral-vrf-sdk" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77f3741c733907c10e0d0b32f9aa485c20a73e0b1e7e673a0ac38185fb2a6c11" +dependencies = [ + "anchor-lang", + "borsh 0.10.4", + "borsh 1.6.1", + "ephemeral-vrf-sdk-vrf-macro", + "solana-program 2.3.0", + "solana-program 3.0.0", + "solana-pubkey 2.4.0", + "solana-system-interface 3.2.0", +] + +[[package]] +name = "ephemeral-vrf-sdk-vrf-macro" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7b8b3fe3863a27d4c0f32da7b3cfeff179772fd4124d6c9fdcb9d4ab23cd602" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "feature-probe" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "five8" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75b8549488b4715defcb0d8a8a1c1c76a80661b5fa106b4ca0e7fce59d7d875" +dependencies = [ + "five8_core 0.1.2", +] + +[[package]] +name = "five8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f76610e969fa1784327ded240f1e28a3fd9520c9cec93b636fcf62dd37f772" +dependencies = [ + "five8_core 1.0.0", +] + +[[package]] +name = "five8_const" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26dec3da8bc3ef08f2c04f61eab298c3ab334523e55f076354d6d6f613799a7b" +dependencies = [ + "five8_core 0.1.2", +] + +[[package]] +name = "five8_const" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a0f1728185f277989ca573a402716ae0beaaea3f76a8ff87ef9dd8fb19436c5" +dependencies = [ + "five8_core 1.0.0", +] + +[[package]] +name = "five8_core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2551bf44bc5f776c15044b9b94153a00198be06743e262afaaa61f11ac7523a5" + +[[package]] +name = "five8_core" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "059c31d7d36c43fe39d89e55711858b4da8be7eb6dabac23c7289b1a19489406" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2 0.10.9", + "signature", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures 0.2.17", +] + +[[package]] +name = "keccak-const" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d8d8ce877200136358e0bbff3a77965875db3af755a11e1fa6b1b3e2df13ea" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libsecp256k1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9d220bc1feda2ac231cb78c3d26f27676b8cf82c96971f7aeef3d0cf2797c73" +dependencies = [ + "arrayref", + "base64 0.12.3", + "digest 0.9.0", + "libsecp256k1-core", + "libsecp256k1-gen-ecmult", + "libsecp256k1-gen-genmult", + "rand 0.7.3", + "serde", + "sha2 0.9.9", +] + +[[package]] +name = "libsecp256k1-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f6ab710cec28cef759c5f18671a27dae2a5f952cdaaee1d8e2908cb2478a80" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccab96b584d38fac86a83f07e659f0deafd0253dc096dab5a36d53efe653c5c3" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67abfe149395e3aa1c48a2beb32b068e2334402df8181f818d3aee2b304c4f5d" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[package]] +name = "magicblock-delegation-program-api" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "288904a9950bd20f27f0ef934f320ab1410bd35a6d5c9cf138eca276442b6b2e" +dependencies = [ + "bincode", + "borsh 0.10.4", + "borsh 1.6.1", + "bytemuck", + "const-crypto", + "num_enum", + "pinocchio 0.10.2", + "pinocchio-log", + "pinocchio-pubkey", + "pinocchio-system", + "rkyv", + "serde", + "solana-address 2.6.1", + "solana-instruction 3.4.0", + "solana-loader-v3-interface 6.1.1", + "solana-program 3.0.0", + "solana-pubkey 2.4.0", + "solana-sdk-ids 3.1.0", + "solana-sha256-hasher 3.1.0", + "solana-system-interface 2.0.0", + "static_assertions", + "strum", + "thiserror 2.0.18", +] + +[[package]] +name = "magicblock-magic-program-api" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dc8fba0307c90b91b70c9ed06d4242d6c4159f331b2f05bf8f875c2a94e0e98" +dependencies = [ + "bincode", + "const-crypto", + "serde", + "solana-program 2.3.0", + "solana-program 3.0.0", + "solana-signature 2.3.0", + "solana-signature 3.4.1", +] + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core 0.6.4", + "zeroize", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pinocchio" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8afe4f39c0e25cc471b35b89963312791a5162d45a86578cbeaad9e5e7d1b3b" + +[[package]] +name = "pinocchio" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06810dac15a4ef83d3dabdb4f2f22fb39c9adff669cd2781da4f716510a647c" +dependencies = [ + "solana-account-view", + "solana-address 2.6.1", + "solana-define-syscall 4.0.1", + "solana-instruction-view", + "solana-program-error 3.0.1", +] + +[[package]] +name = "pinocchio-log" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd11022408f312e6179ece321c1f7dc0d1b2aa7765fddd39b2a7378d65a899e8" +dependencies = [ + "pinocchio-log-macro", +] + +[[package]] +name = "pinocchio-log-macro" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69fb52edb3c5736b044cc462b0957b9767d0f574d138f4e2761438c498a4b467" +dependencies = [ + "quote", + "regex", + "syn 1.0.109", +] + +[[package]] +name = "pinocchio-pubkey" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0225638cadcbebae8932cb7f49cb5da7c15c21beb19f048f05a5ca7d93f065" +dependencies = [ + "five8_const 0.1.4", + "pinocchio 0.9.3", + "sha2-const-stable", +] + +[[package]] +name = "pinocchio-system" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24044a0815753862b558e179e78f03f7344cb755de48617a09d7d23b50883b6c" +dependencies = [ + "pinocchio 0.10.2", + "solana-address 2.6.1", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml 0.5.11", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.12+spec-1.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "qstring" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rkyv" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-big-array" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2-const-stable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9" + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest 0.10.7", + "keccak", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "solana-account" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f949fe4edaeaea78c844023bfc1c898e0b1f5a100f8a8d2d0f85d0a7b090258" +dependencies = [ + "solana-account-info 2.3.0", + "solana-clock 2.2.3", + "solana-instruction 2.3.3", + "solana-pubkey 2.4.0", + "solana-sdk-ids 2.2.1", +] + +[[package]] +name = "solana-account-info" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8f5152a288ef1912300fc6efa6c2d1f9bb55d9398eb6c72326360b8063987da" +dependencies = [ + "bincode", + "serde", + "solana-program-error 2.2.2", + "solana-program-memory 2.3.1", + "solana-pubkey 2.4.0", +] + +[[package]] +name = "solana-account-info" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9cf16495d9eb53e3d04e72366a33bb1c20c24e78c171d8b8f5978357b63ae95" +dependencies = [ + "bincode", + "serde_core", + "solana-address 2.6.1", + "solana-program-error 3.0.1", + "solana-program-memory 3.1.0", +] + +[[package]] +name = "solana-account-view" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37ca34c37f92ee341b73d5ce7c8ef5bb38e9a87955b4bd343c63fa18b149215" +dependencies = [ + "solana-address 2.6.1", + "solana-program-error 3.0.1", +] + +[[package]] +name = "solana-address" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2ecac8e1b7f74c2baa9e774c42817e3e75b20787134b76cc4d45e8a604488f5" +dependencies = [ + "solana-address 2.6.1", +] + +[[package]] +name = "solana-address" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39c93e262f671bf402e1040e4a7e40b05d81da5956c7681948c975a0997517bb" +dependencies = [ + "borsh 1.6.1", + "bytemuck", + "bytemuck_derive", + "curve25519-dalek", + "five8 1.0.0", + "five8_const 1.0.0", + "serde", + "serde_derive", + "sha2-const-stable", + "solana-atomic-u64 3.0.1", + "solana-define-syscall 5.1.0", + "solana-program-error 3.0.1", + "solana-sanitize 3.0.1", + "solana-sha256-hasher 3.1.0", + "wincode", +] + +[[package]] +name = "solana-address-lookup-table-interface" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1673f67efe870b64a65cb39e6194be5b26527691ce5922909939961a6e6b395" +dependencies = [ + "bincode", + "bytemuck", + "serde", + "serde_derive", + "solana-clock 2.2.3", + "solana-instruction 2.3.3", + "solana-pubkey 2.4.0", + "solana-sdk-ids 2.2.1", + "solana-slot-hashes 2.2.1", +] + +[[package]] +name = "solana-address-lookup-table-interface" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115b4f773acc4f3f3cb986b0d335e9845c0368c82b0940410935bc11ae065578" +dependencies = [ + "solana-clock 3.1.0", + "solana-pubkey 4.2.0", + "solana-sdk-ids 3.1.0", + "solana-slot-hashes 3.0.2", +] + +[[package]] +name = "solana-atomic-u64" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52e52720efe60465b052b9e7445a01c17550666beec855cce66f44766697bc2" +dependencies = [ + "parking_lot", +] + +[[package]] +name = "solana-atomic-u64" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "085db4906d89324cef2a30840d59eaecf3d4231c560ec7c9f6614a93c652f501" +dependencies = [ + "parking_lot", +] + +[[package]] +name = "solana-big-mod-exp" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75db7f2bbac3e62cfd139065d15bcda9e2428883ba61fc8d27ccb251081e7567" +dependencies = [ + "num-bigint", + "num-traits", + "solana-define-syscall 2.3.0", +] + +[[package]] +name = "solana-big-mod-exp" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30c80fb6d791b3925d5ec4bf23a7c169ef5090c013059ec3ed7d0b2c04efa085" +dependencies = [ + "num-bigint", + "num-traits", + "solana-define-syscall 3.0.0", +] + +[[package]] +name = "solana-bincode" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a3787b8cf9c9fe3dd360800e8b70982b9e5a8af9e11c354b6665dd4a003adc" +dependencies = [ + "bincode", + "serde", + "solana-instruction 2.3.3", +] + +[[package]] +name = "solana-blake3-hasher" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0801e25a1b31a14494fc80882a036be0ffd290efc4c2d640bfcca120a4672" +dependencies = [ + "blake3", + "solana-define-syscall 2.3.0", + "solana-hash 2.3.0", + "solana-sanitize 2.2.1", +] + +[[package]] +name = "solana-blake3-hasher" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7116e1d942a2432ca3f514625104757ab8a56233787e95144c93950029e31176" +dependencies = [ + "blake3", + "solana-define-syscall 4.0.1", + "solana-hash 4.4.0", +] + +[[package]] +name = "solana-borsh" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "718333bcd0a1a7aed6655aa66bef8d7fb047944922b2d3a18f49cbc13e73d004" +dependencies = [ + "borsh 0.10.4", + "borsh 1.6.1", +] + +[[package]] +name = "solana-borsh" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c04abbae16f57178a163125805637b8a076175bb5c0002fb04f4792bea901cf7" +dependencies = [ + "borsh 1.6.1", +] + +[[package]] +name = "solana-clock" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8584296123df8fe229b95e2ebfd37ae637fe9db9b7d4dd677ac5a78e80dbfce" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids 2.2.1", + "solana-sdk-macro 2.2.1", + "solana-sysvar-id 2.2.1", +] + +[[package]] +name = "solana-clock" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ea35d8f69b67daddb921a9da7f78ca591b533cf5e98833cd9ae62fdc2e4652c" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids 3.1.0", + "solana-sdk-macro 3.0.1", + "solana-sysvar-id 3.1.0", +] + +[[package]] +name = "solana-cpi" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc71126edddc2ba014622fc32d0f5e2e78ec6c5a1e0eb511b85618c09e9ea11" +dependencies = [ + "solana-account-info 2.3.0", + "solana-define-syscall 2.3.0", + "solana-instruction 2.3.3", + "solana-program-error 2.2.2", + "solana-pubkey 2.4.0", + "solana-stable-layout 2.2.1", +] + +[[package]] +name = "solana-cpi" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dea26709d867aada85d0d3617db0944215c8bb28d3745b912de7db13a23280c" +dependencies = [ + "solana-account-info 3.1.1", + "solana-define-syscall 4.0.1", + "solana-instruction 3.4.0", + "solana-program-error 3.0.1", + "solana-pubkey 4.2.0", + "solana-stable-layout 3.0.1", +] + +[[package]] +name = "solana-curve25519" +version = "2.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae4261b9a8613d10e77ac831a8fa60b6fa52b9b103df46d641deff9f9812a23" +dependencies = [ + "bytemuck", + "bytemuck_derive", + "curve25519-dalek", + "solana-define-syscall 2.3.0", + "subtle", + "thiserror 2.0.18", +] + +[[package]] +name = "solana-decode-error" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c781686a18db2f942e70913f7ca15dc120ec38dcab42ff7557db2c70c625a35" +dependencies = [ + "num-traits", +] + +[[package]] +name = "solana-define-syscall" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae3e2abcf541c8122eafe9a625d4d194b4023c20adde1e251f94e056bb1aee2" + +[[package]] +name = "solana-define-syscall" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9697086a4e102d28a156b8d6b521730335d6951bd39a5e766512bbe09007cee" + +[[package]] +name = "solana-define-syscall" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57e5b1c0bc1d4a4d10c88a4100499d954c09d3fecfae4912c1a074dff68b1738" + +[[package]] +name = "solana-define-syscall" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e14a4f604117f379840956a8fc8695e4c84f5b0ebed192f31f60d9b85d581d" + +[[package]] +name = "solana-derivation-path" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "939756d798b25c5ec3cca10e06212bdca3b1443cb9bb740a38124f58b258737b" +dependencies = [ + "derivation-path", + "qstring", + "uriparse", +] + +[[package]] +name = "solana-epoch-rewards" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b575d3dd323b9ea10bb6fe89bf6bf93e249b215ba8ed7f68f1a3633f384db7" +dependencies = [ + "serde", + "serde_derive", + "solana-hash 2.3.0", + "solana-sdk-ids 2.2.1", + "solana-sdk-macro 2.2.1", + "solana-sysvar-id 2.2.1", +] + +[[package]] +name = "solana-epoch-rewards" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cddf2388b28291210d9aa60690740733cab527531f06ed153c4d388951e407c" +dependencies = [ + "serde", + "serde_derive", + "solana-hash 4.4.0", + "solana-sdk-ids 3.1.0", + "solana-sdk-macro 3.0.1", + "solana-sysvar-id 3.1.0", +] + +[[package]] +name = "solana-epoch-schedule" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fce071fbddecc55d727b1d7ed16a629afe4f6e4c217bc8d00af3b785f6f67ed" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids 2.2.1", + "solana-sdk-macro 2.2.1", + "solana-sysvar-id 2.2.1", +] + +[[package]] +name = "solana-epoch-schedule" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce264b7b42322325947c4136a09460bf5c73d9aa8262c9b0a2064be63ba8639" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids 3.1.0", + "solana-sdk-macro 3.0.1", + "solana-sysvar-id 3.1.0", +] + +[[package]] +name = "solana-epoch-stake" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "027e6d0b9e7daac5b2ac7c3f9ca1b727861121d9ef05084cf435ff736051e7c2" +dependencies = [ + "solana-define-syscall 5.1.0", + "solana-pubkey 4.2.0", +] + +[[package]] +name = "solana-example-mocks" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84461d56cbb8bb8d539347151e0525b53910102e4bced875d49d5139708e39d3" +dependencies = [ + "serde", + "serde_derive", + "solana-address-lookup-table-interface 2.2.2", + "solana-clock 2.2.3", + "solana-hash 2.3.0", + "solana-instruction 2.3.3", + "solana-keccak-hasher 2.2.1", + "solana-message 2.4.0", + "solana-nonce 2.2.1", + "solana-pubkey 2.4.0", + "solana-sdk-ids 2.2.1", + "solana-system-interface 1.0.0", + "thiserror 2.0.18", +] + +[[package]] +name = "solana-example-mocks" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978855d164845c1b0235d4b4d101cadc55373fffaf0b5b6cfa2194d25b2ed658" +dependencies = [ + "serde", + "serde_derive", + "solana-address-lookup-table-interface 3.1.0", + "solana-clock 3.1.0", + "solana-hash 3.1.0", + "solana-instruction 3.4.0", + "solana-keccak-hasher 3.1.0", + "solana-message 3.1.0", + "solana-nonce 3.2.0", + "solana-pubkey 3.0.0", + "solana-sdk-ids 3.1.0", + "solana-system-interface 2.0.0", + "thiserror 2.0.18", +] + +[[package]] +name = "solana-feature-gate-interface" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f5c5382b449e8e4e3016fb05e418c53d57782d8b5c30aa372fc265654b956d" +dependencies = [ + "bincode", + "serde", + "serde_derive", + "solana-account", + "solana-account-info 2.3.0", + "solana-instruction 2.3.3", + "solana-program-error 2.2.2", + "solana-pubkey 2.4.0", + "solana-rent 2.2.1", + "solana-sdk-ids 2.2.1", + "solana-system-interface 1.0.0", +] + +[[package]] +name = "solana-fee-calculator" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89bc408da0fb3812bc3008189d148b4d3e08252c79ad810b245482a3f70cd8d" +dependencies = [ + "log", + "serde", + "serde_derive", +] + +[[package]] +name = "solana-fee-calculator" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ee18959f176ba6229105c6c2a2ddaaa04bd53615af9277d834b113571bd205" +dependencies = [ + "log", + "serde", + "serde_derive", +] + +[[package]] +name = "solana-hash" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b96e9f0300fa287b545613f007dfe20043d7812bee255f418c1eb649c93b63" +dependencies = [ + "borsh 1.6.1", + "bytemuck", + "bytemuck_derive", + "five8 0.2.1", + "js-sys", + "serde", + "serde_derive", + "solana-atomic-u64 2.2.1", + "solana-sanitize 2.2.1", + "wasm-bindgen", +] + +[[package]] +name = "solana-hash" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "337c246447142f660f778cf6cb582beba8e28deb05b3b24bfb9ffd7c562e5f41" +dependencies = [ + "solana-hash 4.4.0", +] + +[[package]] +name = "solana-hash" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe51db00ac3aa9f950d1e6201a126acfa26e6d81bc4a183ba64ec02effcad883" +dependencies = [ + "borsh 1.6.1", + "bytemuck", + "bytemuck_derive", + "five8 1.0.0", + "serde", + "serde_derive", + "solana-atomic-u64 3.0.1", + "solana-sanitize 3.0.1", +] + +[[package]] +name = "solana-instruction" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab5682934bd1f65f8d2c16f21cb532526fcc1a09f796e2cacdb091eee5774ad" +dependencies = [ + "bincode", + "borsh 1.6.1", + "getrandom 0.2.17", + "js-sys", + "num-traits", + "serde", + "serde_derive", + "serde_json", + "solana-define-syscall 2.3.0", + "solana-pubkey 2.4.0", + "wasm-bindgen", +] + +[[package]] +name = "solana-instruction" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ebb0ffd19263051bc3f683fcc086134b8ff23af894dcb63f7563c7137b42f1" +dependencies = [ + "bincode", + "borsh 1.6.1", + "serde", + "serde_derive", + "solana-define-syscall 5.1.0", + "solana-instruction-error", + "solana-pubkey 4.2.0", +] + +[[package]] +name = "solana-instruction-error" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0b188842592fdf6cb96f55263ae1bf11713ab5114401d1d5a881ed7cc41bef6" +dependencies = [ + "num-traits", + "solana-program-error 3.0.1", +] + +[[package]] +name = "solana-instruction-view" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60147e4d0a4620013df40bf30a86dd299203ff12fcb8b593cd51014fce0875d8" +dependencies = [ + "solana-account-view", + "solana-address 2.6.1", + "solana-define-syscall 4.0.1", + "solana-program-error 3.0.1", +] + +[[package]] +name = "solana-instructions-sysvar" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0e85a6fad5c2d0c4f5b91d34b8ca47118fc593af706e523cdbedf846a954f57" +dependencies = [ + "bitflags", + "solana-account-info 2.3.0", + "solana-instruction 2.3.3", + "solana-program-error 2.2.2", + "solana-pubkey 2.4.0", + "solana-sanitize 2.2.1", + "solana-sdk-ids 2.2.1", + "solana-serialize-utils 2.2.1", + "solana-sysvar-id 2.2.1", +] + +[[package]] +name = "solana-instructions-sysvar" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e0732294560e88ecdb2bbc656e67383e9f88c78ec09469cef172f0d28cd1bcd" +dependencies = [ + "bitflags", + "solana-account-info 3.1.1", + "solana-instruction 3.4.0", + "solana-instruction-error", + "solana-program-error 3.0.1", + "solana-sanitize 3.0.1", + "solana-sdk-ids 3.1.0", + "solana-serialize-utils 3.1.2", + "solana-sysvar-id 3.1.0", +] + +[[package]] +name = "solana-keccak-hasher" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7aeb957fbd42a451b99235df4942d96db7ef678e8d5061ef34c9b34cae12f79" +dependencies = [ + "sha3", + "solana-define-syscall 2.3.0", + "solana-hash 2.3.0", + "solana-sanitize 2.2.1", +] + +[[package]] +name = "solana-keccak-hasher" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed1c0d16d6fdeba12291a1f068cdf0d479d9bff1141bf44afd7aa9d485f65ef8" +dependencies = [ + "sha3", + "solana-define-syscall 4.0.1", + "solana-hash 4.4.0", +] + +[[package]] +name = "solana-last-restart-slot" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6360ac2fdc72e7463565cd256eedcf10d7ef0c28a1249d261ec168c1b55cdd" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids 2.2.1", + "solana-sdk-macro 2.2.1", + "solana-sysvar-id 2.2.1", +] + +[[package]] +name = "solana-last-restart-slot" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "426711c6564b790026e45cabec3c64b971864c48b6b2d83c0ebf52a118bb4cda" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids 3.1.0", + "solana-sdk-macro 3.0.1", + "solana-sysvar-id 3.1.0", +] + +[[package]] +name = "solana-loader-v2-interface" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8ab08006dad78ae7cd30df8eea0539e207d08d91eaefb3e1d49a446e1c49654" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "solana-instruction 2.3.3", + "solana-pubkey 2.4.0", + "solana-sdk-ids 2.2.1", +] + +[[package]] +name = "solana-loader-v3-interface" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f7162a05b8b0773156b443bccd674ea78bb9aa406325b467ea78c06c99a63a2" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "solana-instruction 2.3.3", + "solana-pubkey 2.4.0", + "solana-sdk-ids 2.2.1", + "solana-system-interface 1.0.0", +] + +[[package]] +name = "solana-loader-v3-interface" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e0538d4dbc9022e01616f1c58f2db98ece739c5d5ed4a2ef8737a953e76a2d4" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "solana-instruction 3.4.0", + "solana-pubkey 4.2.0", + "solana-sdk-ids 3.1.0", +] + +[[package]] +name = "solana-loader-v4-interface" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "706a777242f1f39a83e2a96a2a6cb034cb41169c6ecbee2cf09cb873d9659e7e" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "solana-instruction 2.3.3", + "solana-pubkey 2.4.0", + "solana-sdk-ids 2.2.1", + "solana-system-interface 1.0.0", +] + +[[package]] +name = "solana-message" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1796aabce376ff74bf89b78d268fa5e683d7d7a96a0a4e4813ec34de49d5314b" +dependencies = [ + "bincode", + "blake3", + "lazy_static", + "serde", + "serde_derive", + "solana-bincode", + "solana-hash 2.3.0", + "solana-instruction 2.3.3", + "solana-pubkey 2.4.0", + "solana-sanitize 2.2.1", + "solana-sdk-ids 2.2.1", + "solana-short-vec 2.2.1", + "solana-system-interface 1.0.0", + "solana-transaction-error 2.2.1", + "wasm-bindgen", +] + +[[package]] +name = "solana-message" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0448b1fd891c5f46491e5dc7d9986385ba3c852c340db2911dd29faa01d2b08d" +dependencies = [ + "lazy_static", + "serde", + "serde_derive", + "solana-address 2.6.1", + "solana-hash 4.4.0", + "solana-instruction 3.4.0", + "solana-sanitize 3.0.1", + "solana-sdk-ids 3.1.0", + "solana-short-vec 3.2.2", + "solana-transaction-error 3.2.1", +] + +[[package]] +name = "solana-msg" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36a1a14399afaabc2781a1db09cb14ee4cc4ee5c7a5a3cfcc601811379a8092" +dependencies = [ + "solana-define-syscall 2.3.0", +] + +[[package]] +name = "solana-msg" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726b7cbbc6be6f1c6f29146ac824343b9415133eee8cce156452ad1db93f8008" +dependencies = [ + "solana-define-syscall 5.1.0", +] + +[[package]] +name = "solana-native-token" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61515b880c36974053dd499c0510066783f0cc6ac17def0c7ef2a244874cf4a9" + +[[package]] +name = "solana-native-token" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8dd4c280dca9d046139eb5b7a5ac9ad10403fbd64964c7d7571214950d758f" + +[[package]] +name = "solana-nonce" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703e22eb185537e06204a5bd9d509b948f0066f2d1d814a6f475dafb3ddf1325" +dependencies = [ + "serde", + "serde_derive", + "solana-fee-calculator 2.2.1", + "solana-hash 2.3.0", + "solana-pubkey 2.4.0", + "solana-sha256-hasher 2.3.0", +] + +[[package]] +name = "solana-nonce" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95dbc9f2e33b6c10e231df15cb2a3bff9ea7eab6347f9e316fe75c97fd67bbb" +dependencies = [ + "solana-fee-calculator 3.2.1", + "solana-hash 4.4.0", + "solana-pubkey 4.2.0", + "solana-sha256-hasher 3.1.0", +] + +[[package]] +name = "solana-program" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98eca145bd3545e2fbb07166e895370576e47a00a7d824e325390d33bf467210" +dependencies = [ + "bincode", + "blake3", + "borsh 0.10.4", + "borsh 1.6.1", + "bs58", + "bytemuck", + "console_error_panic_hook", + "console_log", + "getrandom 0.2.17", + "lazy_static", + "log", + "memoffset", + "num-bigint", + "num-derive", + "num-traits", + "rand 0.8.6", + "serde", + "serde_bytes", + "serde_derive", + "solana-account-info 2.3.0", + "solana-address-lookup-table-interface 2.2.2", + "solana-atomic-u64 2.2.1", + "solana-big-mod-exp 2.2.1", + "solana-bincode", + "solana-blake3-hasher 2.2.1", + "solana-borsh 2.2.1", + "solana-clock 2.2.3", + "solana-cpi 2.2.1", + "solana-decode-error", + "solana-define-syscall 2.3.0", + "solana-epoch-rewards 2.2.1", + "solana-epoch-schedule 2.2.1", + "solana-example-mocks 2.2.1", + "solana-feature-gate-interface", + "solana-fee-calculator 2.2.1", + "solana-hash 2.3.0", + "solana-instruction 2.3.3", + "solana-instructions-sysvar 2.2.2", + "solana-keccak-hasher 2.2.1", + "solana-last-restart-slot 2.2.1", + "solana-loader-v2-interface", + "solana-loader-v3-interface 5.0.0", + "solana-loader-v4-interface", + "solana-message 2.4.0", + "solana-msg 2.2.1", + "solana-native-token 2.3.0", + "solana-nonce 2.2.1", + "solana-program-entrypoint 2.3.0", + "solana-program-error 2.2.2", + "solana-program-memory 2.3.1", + "solana-program-option 2.2.1", + "solana-program-pack 2.2.1", + "solana-pubkey 2.4.0", + "solana-rent 2.2.1", + "solana-sanitize 2.2.1", + "solana-sdk-ids 2.2.1", + "solana-sdk-macro 2.2.1", + "solana-secp256k1-recover 2.2.1", + "solana-serde-varint 2.2.2", + "solana-serialize-utils 2.2.1", + "solana-sha256-hasher 2.3.0", + "solana-short-vec 2.2.1", + "solana-slot-hashes 2.2.1", + "solana-slot-history 2.2.1", + "solana-stable-layout 2.2.1", + "solana-stake-interface", + "solana-system-interface 1.0.0", + "solana-sysvar 2.3.0", + "solana-sysvar-id 2.2.1", + "solana-vote-interface", + "thiserror 2.0.18", + "wasm-bindgen", +] + +[[package]] +name = "solana-program" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91b12305dd81045d705f427acd0435a2e46444b65367d7179d7bdcfc3bc5f5eb" +dependencies = [ + "memoffset", + "solana-account-info 3.1.1", + "solana-big-mod-exp 3.0.0", + "solana-blake3-hasher 3.1.0", + "solana-borsh 3.0.2", + "solana-clock 3.1.0", + "solana-cpi 3.1.0", + "solana-define-syscall 3.0.0", + "solana-epoch-rewards 3.0.2", + "solana-epoch-schedule 3.1.0", + "solana-epoch-stake", + "solana-example-mocks 3.0.0", + "solana-fee-calculator 3.2.1", + "solana-hash 3.1.0", + "solana-instruction 3.4.0", + "solana-instruction-error", + "solana-instructions-sysvar 3.0.1", + "solana-keccak-hasher 3.1.0", + "solana-last-restart-slot 3.0.1", + "solana-msg 3.1.0", + "solana-native-token 3.0.0", + "solana-program-entrypoint 3.1.1", + "solana-program-error 3.0.1", + "solana-program-memory 3.1.0", + "solana-program-option 3.1.0", + "solana-program-pack 3.1.0", + "solana-pubkey 3.0.0", + "solana-rent 3.1.0", + "solana-sdk-ids 3.1.0", + "solana-secp256k1-recover 3.1.1", + "solana-serde-varint 3.0.1", + "solana-serialize-utils 3.1.2", + "solana-sha256-hasher 3.1.0", + "solana-short-vec 3.2.2", + "solana-slot-hashes 3.0.2", + "solana-slot-history 3.0.1", + "solana-stable-layout 3.0.1", + "solana-sysvar 3.1.1", + "solana-sysvar-id 3.1.0", +] + +[[package]] +name = "solana-program-entrypoint" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32ce041b1a0ed275290a5008ee1a4a6c48f5054c8a3d78d313c08958a06aedbd" +dependencies = [ + "solana-account-info 2.3.0", + "solana-msg 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.4.0", +] + +[[package]] +name = "solana-program-entrypoint" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c9b0a1ff494e05f503a08b3d51150b73aa639544631e510279d6375f290997" +dependencies = [ + "solana-account-info 3.1.1", + "solana-define-syscall 4.0.1", + "solana-program-error 3.0.1", + "solana-pubkey 4.2.0", +] + +[[package]] +name = "solana-program-error" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ee2e0217d642e2ea4bee237f37bd61bb02aec60da3647c48ff88f6556ade775" +dependencies = [ + "borsh 1.6.1", + "num-traits", + "serde", + "serde_derive", + "solana-decode-error", + "solana-instruction 2.3.3", + "solana-msg 2.2.1", + "solana-pubkey 2.4.0", +] + +[[package]] +name = "solana-program-error" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f04fa578707b3612b095f0c8e19b66a1233f7c42ca8082fcb3b745afcc0add6" +dependencies = [ + "borsh 1.6.1", + "serde", + "serde_derive", +] + +[[package]] +name = "solana-program-memory" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a5426090c6f3fd6cfdc10685322fede9ca8e5af43cd6a59e98bfe4e91671712" +dependencies = [ + "solana-define-syscall 2.3.0", +] + +[[package]] +name = "solana-program-memory" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4068648649653c2c50546e9a7fb761791b5ab0cda054c771bb5808d3a4b9eb52" +dependencies = [ + "solana-define-syscall 4.0.1", +] + +[[package]] +name = "solana-program-option" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc677a2e9bc616eda6dbdab834d463372b92848b2bfe4a1ed4e4b4adba3397d0" + +[[package]] +name = "solana-program-option" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a88006a9b8594088cec9027ab77caaaa258a2aaa2083d3f086c44b42e50aeab" + +[[package]] +name = "solana-program-pack" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "319f0ef15e6e12dc37c597faccb7d62525a509fec5f6975ecb9419efddeb277b" +dependencies = [ + "solana-program-error 2.2.2", +] + +[[package]] +name = "solana-program-pack" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7701cb15b90667ae1c89ef4ac35a59c61e66ce58ddee13d729472af7f41d59" +dependencies = [ + "solana-program-error 3.0.1", +] + +[[package]] +name = "solana-pubkey" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b62adb9c3261a052ca1f999398c388f1daf558a1b492f60a6d9e64857db4ff1" +dependencies = [ + "borsh 0.10.4", + "borsh 1.6.1", + "bytemuck", + "bytemuck_derive", + "curve25519-dalek", + "five8 0.2.1", + "five8_const 0.1.4", + "getrandom 0.2.17", + "js-sys", + "num-traits", + "serde", + "serde_derive", + "solana-atomic-u64 2.2.1", + "solana-decode-error", + "solana-define-syscall 2.3.0", + "solana-sanitize 2.2.1", + "solana-sha256-hasher 2.3.0", + "wasm-bindgen", +] + +[[package]] +name = "solana-pubkey" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8909d399deb0851aa524420beeb5646b115fd253ef446e35fe4504c904da3941" +dependencies = [ + "solana-address 1.1.0", +] + +[[package]] +name = "solana-pubkey" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7db719574990de7e8b0f55a8593ac92a5ccb42c8ce67b3e4bf05b139d5d9ee71" +dependencies = [ + "solana-address 2.6.1", +] + +[[package]] +name = "solana-rent" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1aea8fdea9de98ca6e8c2da5827707fb3842833521b528a713810ca685d2480" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids 2.2.1", + "solana-sdk-macro 2.2.1", + "solana-sysvar-id 2.2.1", +] + +[[package]] +name = "solana-rent" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e860d5499a705369778647e97d760f7670adfb6fc8419dd3d568deccd46d5487" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids 3.1.0", + "solana-sdk-macro 3.0.1", + "solana-sysvar-id 3.1.0", +] + +[[package]] +name = "solana-sanitize" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61f1bc1357b8188d9c4a3af3fc55276e56987265eb7ad073ae6f8180ee54cecf" + +[[package]] +name = "solana-sanitize" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf09694a0fc14e5ffb18f9b7b7c0f15ecb6eac5b5610bf76a1853459d19daf9" + +[[package]] +name = "solana-sdk-ids" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5d8b9cc68d5c88b062a33e23a6466722467dde0035152d8fb1afbcdf350a5f" +dependencies = [ + "solana-pubkey 2.4.0", +] + +[[package]] +name = "solana-sdk-ids" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def234c1956ff616d46c9dd953f251fa7096ddbaa6d52b165218de97882b7280" +dependencies = [ + "solana-address 2.6.1", +] + +[[package]] +name = "solana-sdk-macro" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86280da8b99d03560f6ab5aca9de2e38805681df34e0bb8f238e69b29433b9df" +dependencies = [ + "bs58", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "solana-sdk-macro" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8765316242300c48242d84a41614cb3388229ec353ba464f6fe62a733e41806f" +dependencies = [ + "bs58", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "solana-secp256k1-recover" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa3120b6cdaa270f39444f5093a90a7b03d296d362878f7a6991d6de3bbe496" +dependencies = [ + "libsecp256k1", + "solana-define-syscall 2.3.0", + "thiserror 2.0.18", +] + +[[package]] +name = "solana-secp256k1-recover" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c5f18893d62e6c73117dcba48f8f5e3266d90e5ec3d0a0a90f9785adac36c1" +dependencies = [ + "k256", + "solana-define-syscall 5.1.0", + "thiserror 2.0.18", +] + +[[package]] +name = "solana-security-txt" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c94a02d486b28f219a4f8f5d7dd93cbfbb93c9f466cb7871c22e50cd5ae9a7a2" + +[[package]] +name = "solana-seed-derivable" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beb82b5adb266c6ea90e5cf3967235644848eac476c5a1f2f9283a143b7c97f" +dependencies = [ + "solana-derivation-path", +] + +[[package]] +name = "solana-seed-phrase" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36187af2324f079f65a675ec22b31c24919cb4ac22c79472e85d819db9bbbc15" +dependencies = [ + "hmac", + "pbkdf2", + "sha2 0.10.9", +] + +[[package]] +name = "solana-serde-varint" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a7e155eba458ecfb0107b98236088c3764a09ddf0201ec29e52a0be40857113" +dependencies = [ + "serde", +] + +[[package]] +name = "solana-serde-varint" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "950e5b83e839dc0f92c66afc124bb8f40e89bc90f0579e8ec5499296d27f54e3" +dependencies = [ + "serde", +] + +[[package]] +name = "solana-serialize-utils" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "817a284b63197d2b27afdba829c5ab34231da4a9b4e763466a003c40ca4f535e" +dependencies = [ + "solana-instruction 2.3.3", + "solana-pubkey 2.4.0", + "solana-sanitize 2.2.1", +] + +[[package]] +name = "solana-serialize-utils" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "761357b0853c9623bf12c1d2314b3d6160a85b087b84c45224fb85766d22616b" +dependencies = [ + "solana-instruction-error", + "solana-pubkey 4.2.0", + "solana-sanitize 3.0.1", +] + +[[package]] +name = "solana-sha256-hasher" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa3feb32c28765f6aa1ce8f3feac30936f16c5c3f7eb73d63a5b8f6f8ecdc44" +dependencies = [ + "sha2 0.10.9", + "solana-define-syscall 2.3.0", + "solana-hash 2.3.0", +] + +[[package]] +name = "solana-sha256-hasher" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db7dc3011ea4c0334aaaa7e7128cb390ecf546b28d412e9bf2064680f57f588f" +dependencies = [ + "sha2 0.10.9", + "solana-define-syscall 4.0.1", + "solana-hash 4.4.0", +] + +[[package]] +name = "solana-short-vec" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c54c66f19b9766a56fa0057d060de8378676cb64987533fa088861858fc5a69" +dependencies = [ + "serde", +] + +[[package]] +name = "solana-short-vec" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8250a4495aad49ad20556a607da53bdcb20de78da10b65afbf918b7f1de647" +dependencies = [ + "serde_core", +] + +[[package]] +name = "solana-signature" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c8ec8e657aecfc187522fc67495142c12f35e55ddeca8698edbb738b8dbd8c" +dependencies = [ + "five8 0.2.1", + "serde", + "serde-big-array", + "serde_derive", + "solana-sanitize 2.2.1", +] + +[[package]] +name = "solana-signature" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0364c7577c3c82a693ce28a1febc8d1b5d1b0a175fdc2114ae6186b69effe1e" +dependencies = [ + "five8 1.0.0", + "serde", + "serde-big-array", + "serde_derive", + "solana-sanitize 3.0.1", + "wincode", +] + +[[package]] +name = "solana-signer" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c41991508a4b02f021c1342ba00bcfa098630b213726ceadc7cb032e051975b" +dependencies = [ + "solana-pubkey 2.4.0", + "solana-signature 2.3.0", + "solana-transaction-error 2.2.1", +] + +[[package]] +name = "solana-slot-hashes" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8691982114513763e88d04094c9caa0376b867a29577939011331134c301ce" +dependencies = [ + "serde", + "serde_derive", + "solana-hash 2.3.0", + "solana-sdk-ids 2.2.1", + "solana-sysvar-id 2.2.1", +] + +[[package]] +name = "solana-slot-hashes" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a57c158c35629f9e302ab385f16b15813f4927a31c27dda72f3df828bb08d93" +dependencies = [ + "serde", + "serde_derive", + "solana-hash 4.4.0", + "solana-sdk-ids 3.1.0", + "solana-sysvar-id 3.1.0", +] + +[[package]] +name = "solana-slot-history" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ccc1b2067ca22754d5283afb2b0126d61eae734fc616d23871b0943b0d935e" +dependencies = [ + "bv", + "serde", + "serde_derive", + "solana-sdk-ids 2.2.1", + "solana-sysvar-id 2.2.1", +] + +[[package]] +name = "solana-slot-history" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0622d03a823770f7763afd866e012b296d5a3cbbbe51e110b5bd9ab3441efdca" +dependencies = [ + "bv", + "serde", + "serde_derive", + "solana-sdk-ids 3.1.0", + "solana-sysvar-id 3.1.0", +] + +[[package]] +name = "solana-stable-layout" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f14f7d02af8f2bc1b5efeeae71bc1c2b7f0f65cd75bcc7d8180f2c762a57f54" +dependencies = [ + "solana-instruction 2.3.3", + "solana-pubkey 2.4.0", +] + +[[package]] +name = "solana-stable-layout" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9f6a291ba063a37780af29e7db14bdd3dc447584d8ba5b3fc4b88e2bbc982fa" +dependencies = [ + "solana-instruction 3.4.0", + "solana-pubkey 4.2.0", +] + +[[package]] +name = "solana-stake-interface" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5269e89fde216b4d7e1d1739cf5303f8398a1ff372a81232abbee80e554a838c" +dependencies = [ + "borsh 0.10.4", + "borsh 1.6.1", + "num-traits", + "serde", + "serde_derive", + "solana-clock 2.2.3", + "solana-cpi 2.2.1", + "solana-decode-error", + "solana-instruction 2.3.3", + "solana-program-error 2.2.2", + "solana-pubkey 2.4.0", + "solana-system-interface 1.0.0", + "solana-sysvar-id 2.2.1", +] + +[[package]] +name = "solana-system-interface" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94d7c18cb1a91c6be5f5a8ac9276a1d7c737e39a21beba9ea710ab4b9c63bc90" +dependencies = [ + "js-sys", + "num-traits", + "serde", + "serde_derive", + "solana-decode-error", + "solana-instruction 2.3.3", + "solana-pubkey 2.4.0", + "wasm-bindgen", +] + +[[package]] +name = "solana-system-interface" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e1790547bfc3061f1ee68ea9d8dc6c973c02a163697b24263a8e9f2e6d4afa2" +dependencies = [ + "num-traits", + "serde", + "serde_derive", + "solana-instruction 3.4.0", + "solana-msg 3.1.0", + "solana-program-error 3.0.1", + "solana-pubkey 3.0.0", +] + +[[package]] +name = "solana-system-interface" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55b54965bf0b76fa8e2b35376583efddd4d916618cfe595bf48c7d7b55a9e628" +dependencies = [ + "num-traits", + "serde", + "serde_derive", + "solana-address 2.6.1", + "solana-instruction 3.4.0", + "solana-msg 3.1.0", + "solana-program-error 3.0.1", +] + +[[package]] +name = "solana-sysvar" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c3595f95069f3d90f275bb9bd235a1973c4d059028b0a7f81baca2703815db" +dependencies = [ + "base64 0.22.1", + "bincode", + "bytemuck", + "bytemuck_derive", + "lazy_static", + "serde", + "serde_derive", + "solana-account-info 2.3.0", + "solana-clock 2.2.3", + "solana-define-syscall 2.3.0", + "solana-epoch-rewards 2.2.1", + "solana-epoch-schedule 2.2.1", + "solana-fee-calculator 2.2.1", + "solana-hash 2.3.0", + "solana-instruction 2.3.3", + "solana-instructions-sysvar 2.2.2", + "solana-last-restart-slot 2.2.1", + "solana-program-entrypoint 2.3.0", + "solana-program-error 2.2.2", + "solana-program-memory 2.3.1", + "solana-pubkey 2.4.0", + "solana-rent 2.2.1", + "solana-sanitize 2.2.1", + "solana-sdk-ids 2.2.1", + "solana-sdk-macro 2.2.1", + "solana-slot-hashes 2.2.1", + "solana-slot-history 2.2.1", + "solana-stake-interface", + "solana-sysvar-id 2.2.1", +] + +[[package]] +name = "solana-sysvar" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6690d3dd88f15c21edff68eb391ef8800df7a1f5cec84ee3e8d1abf05affdf74" +dependencies = [ + "base64 0.22.1", + "bincode", + "bytemuck", + "bytemuck_derive", + "lazy_static", + "serde", + "serde_derive", + "solana-account-info 3.1.1", + "solana-clock 3.1.0", + "solana-define-syscall 4.0.1", + "solana-epoch-rewards 3.0.2", + "solana-epoch-schedule 3.1.0", + "solana-fee-calculator 3.2.1", + "solana-hash 4.4.0", + "solana-instruction 3.4.0", + "solana-last-restart-slot 3.0.1", + "solana-program-entrypoint 3.1.1", + "solana-program-error 3.0.1", + "solana-program-memory 3.1.0", + "solana-pubkey 4.2.0", + "solana-rent 3.1.0", + "solana-sdk-ids 3.1.0", + "solana-sdk-macro 3.0.1", + "solana-slot-hashes 3.0.2", + "solana-slot-history 3.0.1", + "solana-sysvar-id 3.1.0", +] + +[[package]] +name = "solana-sysvar-id" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5762b273d3325b047cfda250787f8d796d781746860d5d0a746ee29f3e8812c1" +dependencies = [ + "solana-pubkey 2.4.0", + "solana-sdk-ids 2.2.1", +] + +[[package]] +name = "solana-sysvar-id" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17358d1e9a13e5b9c2264d301102126cf11a47fd394cdf3dec174fe7bc96e1de" +dependencies = [ + "solana-address 2.6.1", + "solana-sdk-ids 3.1.0", +] + +[[package]] +name = "solana-transaction-error" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a9dc8fdb61c6088baab34fc3a8b8473a03a7a5fd404ed8dd502fa79b67cb1" +dependencies = [ + "solana-instruction 2.3.3", + "solana-sanitize 2.2.1", +] + +[[package]] +name = "solana-transaction-error" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2441d6dcd51100e7d97c3fb3b723e08aa701066ff7afc00026fd8d8e222cb95b" +dependencies = [ + "solana-instruction-error", + "solana-sanitize 3.0.1", +] + +[[package]] +name = "solana-vote-interface" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b80d57478d6599d30acc31cc5ae7f93ec2361a06aefe8ea79bc81739a08af4c3" +dependencies = [ + "bincode", + "num-derive", + "num-traits", + "serde", + "serde_derive", + "solana-clock 2.2.3", + "solana-decode-error", + "solana-hash 2.3.0", + "solana-instruction 2.3.3", + "solana-pubkey 2.4.0", + "solana-rent 2.2.1", + "solana-sdk-ids 2.2.1", + "solana-serde-varint 2.2.2", + "solana-serialize-utils 2.2.1", + "solana-short-vec 2.2.1", + "solana-system-interface 1.0.0", +] + +[[package]] +name = "solana-zk-sdk" +version = "2.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b9fc6ec37d16d0dccff708ed1dd6ea9ba61796700c3bb7c3b401973f10f63b" +dependencies = [ + "aes-gcm-siv", + "base64 0.22.1", + "bincode", + "bytemuck", + "bytemuck_derive", + "curve25519-dalek", + "itertools", + "js-sys", + "merlin", + "num-derive", + "num-traits", + "rand 0.8.6", + "serde", + "serde_derive", + "serde_json", + "sha3", + "solana-derivation-path", + "solana-instruction 2.3.3", + "solana-pubkey 2.4.0", + "solana-sdk-ids 2.2.1", + "solana-seed-derivable", + "solana-seed-phrase", + "solana-signature 2.3.0", + "solana-signer", + "subtle", + "thiserror 2.0.18", + "wasm-bindgen", + "zeroize", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "spl-associated-token-account" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76fee7d65013667032d499adc3c895e286197a35a0d3a4643c80e7fd3e9969e3" +dependencies = [ + "borsh 1.6.1", + "num-derive", + "num-traits", + "solana-program 2.3.0", + "spl-associated-token-account-client", + "spl-token", + "spl-token-2022", + "thiserror 1.0.69", +] + +[[package]] +name = "spl-associated-token-account-client" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f8349dbcbe575f354f9a533a21f272f3eb3808a49e2fdc1c34393b88ba76cb" +dependencies = [ + "solana-instruction 2.3.3", + "solana-pubkey 2.4.0", +] + +[[package]] +name = "spl-discriminator" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7398da23554a31660f17718164e31d31900956054f54f52d5ec1be51cb4f4b3" +dependencies = [ + "bytemuck", + "solana-program-error 2.2.2", + "solana-sha256-hasher 2.3.0", + "spl-discriminator-derive", +] + +[[package]] +name = "spl-discriminator-derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9e8418ea6269dcfb01c712f0444d2c75542c04448b480e87de59d2865edc750" +dependencies = [ + "quote", + "spl-discriminator-syn", + "syn 2.0.118", +] + +[[package]] +name = "spl-discriminator-syn" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d1dbc82ab91422345b6df40a79e2b78c7bce1ebb366da323572dd60b7076b67" +dependencies = [ + "proc-macro2", + "quote", + "sha2 0.10.9", + "syn 2.0.118", + "thiserror 1.0.69", +] + +[[package]] +name = "spl-elgamal-registry" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce0f668975d2b0536e8a8fd60e56a05c467f06021dae037f1d0cfed0de2e231d" +dependencies = [ + "bytemuck", + "solana-program 2.3.0", + "solana-zk-sdk", + "spl-pod", + "spl-token-confidential-transfer-proof-extraction", +] + +[[package]] +name = "spl-memo" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f09647c0974e33366efeb83b8e2daebb329f0420149e74d3a4bd2c08cf9f7cb" +dependencies = [ + "solana-account-info 2.3.0", + "solana-instruction 2.3.3", + "solana-msg 2.2.1", + "solana-program-entrypoint 2.3.0", + "solana-program-error 2.2.2", + "solana-pubkey 2.4.0", +] + +[[package]] +name = "spl-pod" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d994afaf86b779104b4a95ba9ca75b8ced3fdb17ee934e38cb69e72afbe17799" +dependencies = [ + "borsh 1.6.1", + "bytemuck", + "bytemuck_derive", + "num-derive", + "num-traits", + "solana-decode-error", + "solana-msg 2.2.1", + "solana-program-error 2.2.2", + "solana-program-option 2.2.1", + "solana-pubkey 2.4.0", + "solana-zk-sdk", + "thiserror 2.0.18", +] + +[[package]] +name = "spl-program-error" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d39b5186f42b2b50168029d81e58e800b690877ef0b30580d107659250da1d1" +dependencies = [ + "num-derive", + "num-traits", + "solana-program 2.3.0", + "spl-program-error-derive", + "thiserror 1.0.69", +] + +[[package]] +name = "spl-program-error-derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d375dd76c517836353e093c2dbb490938ff72821ab568b545fd30ab3256b3e" +dependencies = [ + "proc-macro2", + "quote", + "sha2 0.10.9", + "syn 2.0.118", +] + +[[package]] +name = "spl-tlv-account-resolution" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd99ff1e9ed2ab86e3fd582850d47a739fec1be9f4661cba1782d3a0f26805f3" +dependencies = [ + "bytemuck", + "num-derive", + "num-traits", + "solana-account-info 2.3.0", + "solana-decode-error", + "solana-instruction 2.3.3", + "solana-msg 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.4.0", + "spl-discriminator", + "spl-pod", + "spl-program-error", + "spl-type-length-value", + "thiserror 1.0.69", +] + +[[package]] +name = "spl-token" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed320a6c934128d4f7e54fe00e16b8aeaecf215799d060ae14f93378da6dc834" +dependencies = [ + "arrayref", + "bytemuck", + "num-derive", + "num-traits", + "num_enum", + "solana-program 2.3.0", + "thiserror 1.0.69", +] + +[[package]] +name = "spl-token-2022" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b27f7405010ef816587c944536b0eafbcc35206ab6ba0f2ca79f1d28e488f4f" +dependencies = [ + "arrayref", + "bytemuck", + "num-derive", + "num-traits", + "num_enum", + "solana-program 2.3.0", + "solana-security-txt", + "solana-zk-sdk", + "spl-elgamal-registry", + "spl-memo", + "spl-pod", + "spl-token", + "spl-token-confidential-transfer-ciphertext-arithmetic", + "spl-token-confidential-transfer-proof-extraction", + "spl-token-confidential-transfer-proof-generation", + "spl-token-group-interface", + "spl-token-metadata-interface", + "spl-transfer-hook-interface", + "spl-type-length-value", + "thiserror 1.0.69", +] + +[[package]] +name = "spl-token-confidential-transfer-ciphertext-arithmetic" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170378693c5516090f6d37ae9bad2b9b6125069be68d9acd4865bbe9fc8499fd" +dependencies = [ + "base64 0.22.1", + "bytemuck", + "solana-curve25519", + "solana-zk-sdk", +] + +[[package]] +name = "spl-token-confidential-transfer-proof-extraction" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff2d6a445a147c9d6dd77b8301b1e116c8299601794b558eafa409b342faf96" +dependencies = [ + "bytemuck", + "solana-curve25519", + "solana-program 2.3.0", + "solana-zk-sdk", + "spl-pod", + "thiserror 2.0.18", +] + +[[package]] +name = "spl-token-confidential-transfer-proof-generation" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8627184782eec1894de8ea26129c61303f1f0adeed65c20e0b10bc584f09356d" +dependencies = [ + "curve25519-dalek", + "solana-zk-sdk", + "thiserror 1.0.69", +] + +[[package]] +name = "spl-token-group-interface" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d595667ed72dbfed8c251708f406d7c2814a3fa6879893b323d56a10bedfc799" +dependencies = [ + "bytemuck", + "num-derive", + "num-traits", + "solana-decode-error", + "solana-instruction 2.3.3", + "solana-msg 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.4.0", + "spl-discriminator", + "spl-pod", + "thiserror 1.0.69", +] + +[[package]] +name = "spl-token-metadata-interface" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfb9c89dbc877abd735f05547dcf9e6e12c00c11d6d74d8817506cab4c99fdbb" +dependencies = [ + "borsh 1.6.1", + "num-derive", + "num-traits", + "solana-borsh 2.2.1", + "solana-decode-error", + "solana-instruction 2.3.3", + "solana-msg 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.4.0", + "spl-discriminator", + "spl-pod", + "spl-type-length-value", + "thiserror 1.0.69", +] + +[[package]] +name = "spl-transfer-hook-interface" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa7503d52107c33c88e845e1351565050362c2314036ddf19a36cd25137c043" +dependencies = [ + "arrayref", + "bytemuck", + "num-derive", + "num-traits", + "solana-account-info 2.3.0", + "solana-cpi 2.2.1", + "solana-decode-error", + "solana-instruction 2.3.3", + "solana-msg 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.4.0", + "spl-discriminator", + "spl-pod", + "spl-program-error", + "spl-tlv-account-resolution", + "spl-type-length-value", + "thiserror 1.0.69", +] + +[[package]] +name = "spl-type-length-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba70ef09b13af616a4c987797870122863cba03acc4284f226a4473b043923f9" +dependencies = [ + "bytemuck", + "num-derive", + "num-traits", + "solana-account-info 2.3.0", + "solana-decode-error", + "solana-msg 2.2.1", + "solana-program-error 2.2.2", + "spl-discriminator", + "spl-pod", + "thiserror 1.0.69", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "uriparse" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0200d0fc04d809396c2ad43f3c95da3582a2556eba8d453c1087f4120ee352ff" +dependencies = [ + "fnv", + "lazy_static", +] + +[[package]] +name = "uuid" +version = "1.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.118", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wincode" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d967db7705dc29120bb6e8ce5b5a2e27734ed5976d1c904e95bd238d1c3c5a" +dependencies = [ + "pastey", + "proc-macro2", + "quote", + "thiserror 2.0.18", + "wincode-derive", +] + +[[package]] +name = "wincode-derive" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15ab90b719560d0fda79c74550ad1c948d17b118765942838055ebaf34d67071" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/agent-os/programs/settlement-ephemeral/Cargo.toml b/agent-os/programs/settlement-ephemeral/Cargo.toml new file mode 100644 index 000000000..dbd93efbc --- /dev/null +++ b/agent-os/programs/settlement-ephemeral/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "covenant-settlement-program-er" +version = "0.1.0" +description = "MagicBlock Ephemeral Rollup build of the Covenant settlement program. Reuses ../settlement/src/lib.rs verbatim with the `ephemeral` feature on, adding the credit-account delegate/commit/undelegate lifecycle. Kept out of the agent-os workspace so its ephemeral-rollups-sdk lock (solana 2.2.20) stays isolated from the litesvm 2.2.1 test deps." +edition = "2021" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib", "lib"] +name = "covenant_settlement_program_er" + +[features] +default = ["ephemeral"] +ephemeral = [] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +task-escrow = [] + +[dependencies] +anchor-lang = "0.31.1" +anchor-spl = { version = "0.31.1", features = ["token_2022"] } +solana-security-txt = "1.1" +ephemeral-rollups-sdk = { version = "0.15.5", features = ["anchor-compat"] } + +[lints.rust] +deprecated = "allow" +unexpected_cfgs = "allow" + +[profile.release] +overflow-checks = true +opt-level = "z" +lto = true +codegen-units = 1 +strip = true diff --git a/agent-os/programs/settlement-ephemeral/OPERATING.md b/agent-os/programs/settlement-ephemeral/OPERATING.md new file mode 100644 index 000000000..a2bfdfa94 --- /dev/null +++ b/agent-os/programs/settlement-ephemeral/OPERATING.md @@ -0,0 +1,122 @@ +# Operating the x402-over-ER path (end to end) + +How an operator runs the MagicBlock ER settlement path so an agent can pay per call +by metering credits in an ephemeral rollup. Every piece below is built and +live-verified on devnet; this stitches them into one runbook. + +The model: an agent calls a registered `er.` tool → the daemon routes it +(`solana-er:*` network) to the ER signer sidecar → the sidecar runs +`consume_credits` in the ER → the facilitator verifies the on-chain proof → 200, +with a settlement receipt recorded. Token custody, staking, and governance stay on +L1; only the program-owned credit balance is delegated to the rollup. + +## 0. Prereqs (one-time) + +- Settlement program deployed (devnet: `cov9UDypG7nsryxdgMcKhKU2spRVWLVjxT2iTv6do5Y`), + a `Config` initialized, and the agent's `CreditAccount` opened + funded + (`buy_credits`). See the settlement program docs. +- The ER signer sidecar built: `cargo build -p covenant-x402 --features solana + --bin covenant-x402-er-signer` (`target/release/covenant-x402-er-signer`). + +## 1. Facilitator (the verifier) + +Already deployed on Render: **https://covenant-x402-er-facilitator.onrender.com** +(devnet, `PRICE=1`). To run your own: `cargo run -p covenant-x402-facilitator` +with `PROGRAM` / `ER` / `PRICE` (or deploy via `deploy/Dockerfile.facilitator` + +the `render.yaml` blueprint entry). `/health` → 200, `/paid` → the x402 challenge. + +## 2. Wire the daemon (covenantd) + +The funding key stays in the sidecar, never the daemon. Set on the daemon: + +```bash +COVENANT_X402_ENABLED=true +COVENANT_X402_SIGNER_BINARY=/path/to/covenant-x402-signer # default SPL signer (non-ER calls) +# ER signer sidecar + its env (the sidecar holds the key): +COVENANT_X402_ER_SIGNER_BINARY=/path/to/covenant-x402-er-signer +COVENANT_X402_ER_KEYPAIR=/path/to/owner-keypair.json # the credit-account owner +COVENANT_X402_ER_PROGRAM=cov9UDypG7nsryxdgMcKhKU2spRVWLVjxT2iTv6do5Y +COVENANT_X402_ER_RPC=https://devnet-eu.magicblock.app +# Register one or more ER providers (each becomes an er. tool): +COVENANT_X402_ER_PROVIDERS='[{"slug":"quote","endpoint":"https://covenant-x402-er-facilitator.onrender.com/paid","per_call_cap":1,"description":"A market quote."}]' +``` + +The daemon advertises `er.quote` in its tool list. `signer_for("solana-er:*")` routes +ER providers to the ER sidecar; everything else uses the default SPL signer. + +## 3. Grant the agent the capability + +```bash +covenant capabilities grant tool.call.er.quote +``` + +(Scope it per provider if you want least privilege.) The same budget, settlement +receipt, and audit machinery every tool/paid call uses then applies. + +## 4. Delegate the credit account (the prerequisite) + +ER consumes only run if the credit account is delegated to an ER validator. Do this +before agents call ER providers, and re-do it after any top-up: + +```bash +cd agent-os/programs/settlement-ephemeral/spike +node er-session.mjs delegate # delegates [b"credits", owner] to the EU validator +node er-session.mjs balance # L1 vs ER view +``` + +`er-session.mjs` uses the official `@magicblock-labs/ephemeral-rollups-sdk` for the +delegation PDAs. This is the operator's delegation utility today (see "Open decision" +below for automating it). + +## 5. Agent pays + +The agent calls the tool (over the daemon IPC / `POST /x402/...`): + +```jsonc +// CallTool +{ "name": "er.quote", "arguments": {} } +``` + +→ `402 → consume_credits in the ER → 200` + a `ToolResult` with the content and the +settling signature. Gasless per call; the only cost is the fixed delegate + commit + +session overhead (~0.0003 SOL), constant in the number of calls. + +## 6. Top-up + +Credits are consumed in the ER. To refill: `node er-session.mjs undelegate` +(commits the final balance to L1) → `buy_credits` on L1 → `er-session.mjs delegate` +again. The account is frozen on L1 while delegated, so top-ups happen between +sessions. + +## Verify the live path + +```bash +node er-session.mjs delegate +URL=https://covenant-x402-er-facilitator.onrender.com/paid node remote-check.mjs +node er-session.mjs undelegate +``` + +`remote-check.mjs` pays the deployed facilitator end to end. The daemon paths are +covered by the `#[ignore]` live tests in `covenantd` +(`pay_x402_settles_through_the_er_signer_live`, +`er_provider_tool_settles_in_the_er_live`). + +## Open decision: delegation-lifecycle automation + +Today delegation is a manual operator step (4/6). The production options, in order of +effort: +1. **Manual / scripted** (current): the operator delegates before a session and + undelegates to top up. Fine for a single long-lived credit account. +2. **Keeper**: a small loop that keeps a set of credit accounts delegated and + undelegates them on a top-up signal. The right fit if many accounts cycle. +3. **Daemon-managed**: the daemon delegates a credit account on first ER use and + undelegates on top-up/shutdown. Most seamless, but the daemon would sign the + delegate txs for the credit-account owner — a key-custody + policy decision. + +Pick (2) or (3) based on how many accounts cycle and where the owner key should live. + +## Not in scope here (gated) + +Mainnet: needs the ER program security review, the settlement-readiness gates +(`docs/internal/on-chain-settlement-readiness.md`), and MagicBlock's mainnet +validator-liveness/recovery answer. Flip `ER`/`PROGRAM` to mainnet only after those. diff --git a/agent-os/programs/settlement-ephemeral/README.md b/agent-os/programs/settlement-ephemeral/README.md new file mode 100644 index 000000000..e1276ab02 --- /dev/null +++ b/agent-os/programs/settlement-ephemeral/README.md @@ -0,0 +1,171 @@ +# settlement-ephemeral — MagicBlock ER build of the Covenant settlement program + +This crate is the **Ephemeral Rollup build** of the settlement program. It compiles +`../settlement/src/lib.rs` verbatim (single source of truth) with the `ephemeral` +feature on, which adds the credit-account delegation lifecycle: + +- `delegate_credits` — delegate `[b"credits", owner]` to the MagicBlock delegation + program. Pass an ER validator pubkey as the first remaining account to pin it. +- `commit_credits` — checkpoint the delegated state (balance + provenance root) + to L1 without releasing. **Permissionless**: any `payer` funds the commit; the + credit PDA is validated against its stored `owner`, so a verifier or keeper can + force an agent's provenance root on-chain without the owner. Accounts: `payer` + (signer), `credits`, then the `#[commit]`-injected `magic_program` + + `magic_context`. Note: each ER commit spends lamports from the account, so top + it up if you commit more than ~10 times before undelegating. +- `undelegate_credits` — commit final balance and return the account to L1. + Owner-gated (owner or recovery keeper). +- `#[ephemeral]` injects the L1 `process_undelegation` callback. + +Everything else (token custody, staking, slashing, governance) is unchanged and +stays on L1. Only the program-owned `u64` credit balance moves to the ER. + +## Why a separate crate + +`ephemeral-rollups-sdk` (via `magicblock-delegation-program-api`) forces the solana +runtime crates to 2.2.20, which is incompatible with the workspace's `litesvm 0.6` +test deps (pinned at 2.2.1, pre the `solana-feature-set` → `agave-feature-set` +rename). Keeping this crate **out of the agent-os workspace** (`[workspace].exclude`) +gives it its own `Cargo.lock` so the ER graph never perturbs the L1 program's tests. + +Two consequences of the isolated lock, both handled here: +- `ephemeral-rollups-sdk` must keep **default features** (the `solana-system-interface` + crate is referenced unconditionally in its `utils.rs`) plus `anchor-compat`. +- `anchor-compat` lets `anchor-lang` resolve to 0.32.1 for the SDK while our program + wants 0.31.1 → two versions → trait mismatch. Fixed by pinning anchor-lang to a + single 0.31.1 in this crate's lock (`cargo update -p anchor-lang --precise 0.31.1`). + +## Build (verified) + +```bash +# from this directory +cargo update -p anchor-lang --precise 0.31.1 # one-time, collapses anchor-lang to 0.31.1 +cargo build-sbf # -> target/deploy/covenant_settlement_program_er.so +``` + +Verified locally with anchor 0.31.1 + agave (cargo-build-sbf), solana-program 2.2.1, +ephemeral-rollups-sdk 0.15.5. Artifact ~693 KB (vs ~513 KB for the L1-only build). + +The L1-only program is unchanged: build it and run its tests from `../settlement` +(`cargo build-sbf`, `cargo test -p covenant-settlement-program`). Both stay green. + +## Endpoints (MagicBlock devnet, perpetual public infra) + +| | URL | +|---|---| +| Magic Router (auto-route ER vs L1) | `https://devnet-router.magicblock.app` (+`wss://`) | +| L1 | `https://api.devnet.solana.com` | + +Pin an ER validator by passing its pubkey as `delegate_credits`' first remaining +account (EU is closest): `MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e`. +Others: US `MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd`, Asia +`MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57`, TEE +`MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo`. + +Fee schedule: 0.0001 SOL/commit to L1, 0.0003 SOL/ER session (charged at undelegate). + +## Live spike — RUN AND VERIFIED on devnet (2026-06-16) + +The ER build is deployed to `cov9UDyp…` on devnet (an upgrade; authority is the +local `id.json`) and `spike/run-spike.mjs` ran the full flow end to end: + +- delegate on L1 → N×consume in the EU ER validator → undelegate → reconcile on L1. +- **N=100, amount=2: reconciled exactly** (4,999,999,995 → 4,999,999,795). Earlier + N=5 run also exact. +- **75.5 ms/op** confirmed for the ER consumes (vs ~400–800 ms L1 confirmed). +- **Whole session cost: ~0.000305 SOL** — the delegate + commit + session overhead. + The 100 consumes themselves are gasless, so the cost is ~constant in N: thousands + of metered consumes per session cost the same fixed ~0.0003 SOL. That is the + unit-economics result we wanted. + +The harness uses hand-encoded instructions (anchor global discriminators + the exact +`#[delegate]`/`#[commit]` account order) and the MagicBlock JS SDK only for the +delegation PDAs, so it needs no generated IDL. `check.mjs ` dumps current +config/credit state. Re-run: `N=100 AMOUNT=2 node run-spike.mjs`. + +Original spec of the flow that `spike/run-spike.mjs` drives: + +1. **L1 setup**: `initialize` (if needed), `open_credit_account`, `buy_credits` so the + owner has a known starting balance `B`. +2. **Delegate**: send `delegate_credits` on **L1** (Magic Router routes it), pinning + the EU validator. The `[b"credits", owner]` PDA is now ER-owned. +3. **Meter in the ER**: send N × `consume_credits(amount, receipt_hash)` through the + **Magic Router** — these land in the ER, gasless, ~ms latency. +4. **Commit**: `commit_credits` (optional mid-run checkpoint). +5. **Undelegate**: `undelegate_credits` → commits the final balance and returns the + account to L1. +6. **Reconcile**: read the credit account on **L1** and assert + `balance == B - N*amount`. Exact reconciliation is the success criterion. + +Deploy + run: + +```bash +solana program deploy target/deploy/covenant_settlement_program_er.so \ + --program-id --url https://api.devnet.solana.com +cd spike && npm i +PROGRAM_ID= PAYER= COVNT_MINT= \ + ROUTER=https://devnet-router.magicblock.app L1=https://api.devnet.solana.com \ + N=1000 AMOUNT=1 node run-spike.mjs +``` + +The client uses the program IDL (run `anchor build` once to emit it — the +`#[delegate]`/`#[commit]` macros add their accounts to the IDL) and the MagicBlock +JS SDK for the delegation PDAs + `GetCommitmentSignature`. See `spike/run-spike.mjs`. + +## x402-over-ER demo (Phase 2) — RUN AND VERIFIED on devnet (2026-06-16) + +`spike/x402-demo.mjs` is the flagship: a paid HTTP endpoint where each call is +settled by a gasless `consume_credits` in the ER instead of an on-chain SPL +transfer. It runs a real HTTP 402 boundary plus a verifying facilitator, end to end +against the live devnet ER. + +- facilitator returns **402 + {amount, nonce}** until a valid `x-payment` header is + presented, then **200 + content**. +- the agent settles on 402 by running `consume_credits(amount, sha256(nonce))` in the + ER; the `receipt_hash` binds the payment to that specific request. +- the facilitator verifies the signature **on the ER**: right program, right credit + account, `amount >= price`, `receipt_hash == sha256(nonce)`, and signature-unused + (anti-replay). No SPL transfer, no per-call L1 fee. + +Session model: delegate once → serve K gasless paid calls → undelegate → reconcile. +Verified run (K=5): `402 → ER pay → 200` at **~71 ms/call steady-state**, ER balance +moved by exactly K, **exact L1 reconciliation** after undelegate. This is +agent-to-agent pay-per-call at a price mainnet can't touch, demonstrated live. + +Run: `K=5 PRICE=1 node x402-demo.mjs`. + +Production path — BUILT and live-verified. `EphemeralSigner` in the `covenant-x402` +crate implements the `Signer` trait by metering: `build_payment()` submits a +`consume_credits` to the pinned ER validator and returns the signature envelope, so +the daemon's `pay_and_record()` path works unmodified. Gated by the crate's `solana` +feature, no on-chain ER SDK dep. Unit + wiremock tested, and live-verified against +devnet-eu via `cargo run -p covenant-x402 --features solana --example ephemeral_live` +(delegate with `spike/er-session.mjs delegate` first): it metered 3 credits in the +ER, balance dropped by exactly 3, reconciled to L1 on undelegate. + +The verifier counterpart is the `covenant-x402-facilitator` crate, and the full +loop is wired into the daemon: +- **Sidecar:** `covenant-x402-er-signer` (in `covenant-x402`, `solana` feature) is + the binary the daemon spawns — stdin `PaymentRequirements` → `consume_credits` in + the ER via `EphemeralSigner` → stdout `x-payment` header. +- **Daemon selection:** `X402Config::signer_for(network)` routes `solana-er:*` + networks to the ER sidecar (env `COVENANT_X402_ER_SIGNER_BINARY` + + `COVENANT_X402_ER_{KEYPAIR,PROGRAM,RPC}`), everything else to the default SPL + signer. The funding key stays in the sidecar's address space, never the daemon's. +- **Provider registration:** an operator registers ER-settled providers via + `COVENANT_X402_ER_PROVIDERS`, a JSON array of + `{slug, endpoint, per_call_cap, [method, network, asset, credits, description]}` + (only slug/endpoint/per_call_cap required; defaults to a GET on `solana-er:devnet` + in `credits`). Each registered provider is advertised to agents as an `er.` + tool. An agent calls it with `CallTool { name: "er." }` — gated by the + `tool.call.er.` capability — which routes through the same x402 pay path and + records a settlement receipt. Example: + `COVENANT_X402_ER_PROVIDERS='[{"slug":"quote","endpoint":"https://host/paid","per_call_cap":1}]'`. + +Both daemon paths are live-verified on devnet (`#[ignore]` tests in `covenantd`): +`pay_x402_settles_through_the_er_signer_live` (the raw op) and +`er_provider_tool_settles_in_the_er_live` (an agent calling a registered `er.*` tool). + +> Status: program + artifact verified; the live ER reconciliation ran (exact, 75.5 +> ms/op, ~0.0003 SOL/session); and the x402-over-ER pay-per-call demo ran end to end +> (exact reconciliation, ~71 ms/call, gasless). All numbers are from real devnet runs. diff --git a/agent-os/programs/settlement-ephemeral/spike/bond-slash-result.json b/agent-os/programs/settlement-ephemeral/spike/bond-slash-result.json new file mode 100644 index 000000000..50ea0676a --- /dev/null +++ b/agent-os/programs/settlement-ephemeral/spike/bond-slash-result.json @@ -0,0 +1,17 @@ +{ + "ts": "2026-07-01", + "program": "cov9UDypG7nsryxdgMcKhKU2spRVWLVjxT2iTv6do5Y", + "agentKey": "438fd44f5e8bd60f7ffa3368b71fe652c2de204e5ff9f1e3b81f46c5f5004a3d", + "agentPda": "G2bMkQkGXTPv2rDLZpXqbn5fAehLqKujXWidcJbYHPwj", + "position": "6RWupBJmygUo7hyN1ic7DQwicYpgjDVjop8id8QKoSd3", + "credits": "DrawYGmdbQ7sULxzzczUqyZT2nmP8SZeYPuJzy6TNksj", + "provenanceCited": "2769ee46c8c7dc49e38737c8a3c6d0f57a48553d9b2af08d0bc82cf80ce88933", + "bonded": "5000", + "slashed": "1000", + "remaining": "4000", + "sigs": { + "register": "595gF5R4yLxLAk7GG6aeewxWmzW1Jwp5nBxfFeVKfWCpUBGdrBzPAshPXtkba3rwF5U5pVhpa9hqiuwDbGLCJufd", + "stake": "3224mKBZKtzVYDVgE6UVcqX3DeRGifD9WpeVSp2f831XbvbuzZTnvF3zupiDhYaZefQgFkVKvw4WChAT3mHspKv8", + "slash": "2yuN3CzDSJs8p6cWQKGKLVwXja7gbo5TzxxQoeuP6CBrSHbmYDi2LuZSNgSGKEVUt36CpadQB1hqpUEhwVjU5w4r" + } +} diff --git a/agent-os/programs/settlement-ephemeral/spike/bond-slash.mjs b/agent-os/programs/settlement-ephemeral/spike/bond-slash.mjs new file mode 100644 index 000000000..ce8061d1d --- /dev/null +++ b/agent-os/programs/settlement-ephemeral/spike/bond-slash.mjs @@ -0,0 +1,108 @@ +// Bond our demo agent on mainnet and close the accountability loop: register the +// agent, stake a slashable CVNT position against it, then slash it with the +// reason read straight from the on-chain provenance_root that the reference run +// folded from the agent's real work (credits PDA [b"credits", operator] is +// DrawYGmd, the account we metered). Nothing caller-supplied to forge. +// +// node bond-slash.mjs # register + stake 5000 + slash 1000 CVNT +// STAKE=5000 SLASH=1000 node bond-slash.mjs +import fs from "node:fs"; +import os from "node:os"; +import crypto from "node:crypto"; +import { Connection, Keypair, PublicKey, SystemProgram, Transaction, TransactionInstruction, sendAndConfirmTransaction } from "@solana/web3.js"; + +const PROG = new PublicKey("cov9UDypG7nsryxdgMcKhKU2spRVWLVjxT2iTv6do5Y"); +const MINT = new PublicKey("2mNVZ6aEjrGwiUVCfz7XGWpiXuWzgBDoznwE579upump"); +const TOKEN22 = new PublicKey("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"); +const ATA_PROG = new PublicKey("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"); +const TREASURY = new PublicKey("8zBseLENQbY8gDQS8X1YoY7ijfxVUfcjvnS5dgjNrqXQ"); +const DEC = 6n, U = 10n ** DEC; +const STAKE = BigInt(process.env.STAKE || "5000") * U; +const SLASH = BigInt(process.env.SLASH || "1000") * U; + +const owner = Keypair.fromSecretKey(new Uint8Array(JSON.parse(fs.readFileSync(`${os.homedir()}/.config/solana/solana-id.json`, "utf8")))); +const l1 = new Connection(fs.readFileSync("/tmp/cov-deploy-rpc", "utf8").trim(), "confirmed"); +const pda = (s) => PublicKey.findProgramAddressSync(s, PROG)[0]; +const ata = (o) => PublicKey.findProgramAddressSync([o.toBuffer(), TOKEN22.toBuffer(), MINT.toBuffer()], ATA_PROG)[0]; +const disc = (n) => crypto.createHash("sha256").update(`global:${n}`).digest().subarray(0, 8); +const sha = (s) => crypto.createHash("sha256").update(s).digest(); +const m = (pubkey, s, w) => ({ pubkey, isSigner: s, isWritable: w }); +const send = (ix) => sendAndConfirmTransaction(l1, new Transaction().add(ix), [owner], { commitment: "confirmed" }); + +const SYSTEM_PROMPT = + "You are the demo agent on Covenant's public sandbox. Reply briefly, 2 to 4 sentences. " + + "You're running as a sandboxed agent inside a Covenant daemon that signs and logs every step."; +const agentKey = sha("covenant.demo-agent.v1"); +const config = pda([Buffer.from("config")]); +const agent = pda([Buffer.from("agent"), agentKey]); +const position = pda([Buffer.from("stake"), agentKey, owner.publicKey.toBuffer()]); +const credits = pda([Buffer.from("credits"), owner.publicKey.toBuffer()]); +const ownerCovnt = ata(owner.publicKey); +const stakeVault = ata(position); + +const agentAcct = (async () => { + console.log(`operator ${owner.publicKey.toBase58()}`); + console.log(`agent_key ${agentKey.toString("hex")}`); + console.log(`agent PDA ${agent.toBase58()}`); + console.log(`position ${position.toBase58()}`); + console.log(`credits ${credits.toBase58()} (metered account, holds the provenance)\n`); + + const prov = (await l1.getAccountInfo(credits)).data.subarray(49, 81); + console.log(`provenance_root to be cited: ${Buffer.from(prov).toString("hex")}\n`); + + if (!(await l1.getAccountInfo(agent))) { + const data = Buffer.concat([disc("register_agent"), agentKey, sha(SYSTEM_PROMPT), sha("llm.chat:haiku")]); + const ix = new TransactionInstruction({ programId: PROG, keys: [m(config, false, false), m(agent, false, true), m(owner.publicKey, true, true), m(SystemProgram.programId, false, false)], data }); + console.log(`registered ${await send(ix)}`); + } else console.log("agent already registered"); + + if (!(await l1.getAccountInfo(stakeVault))) { + const ix = new TransactionInstruction({ programId: ATA_PROG, keys: [m(owner.publicKey, true, true), m(stakeVault, false, true), m(position, false, false), m(MINT, false, false), m(SystemProgram.programId, false, false), m(TOKEN22, false, false)], data: Buffer.from([1]) }); + console.log(`vault ${await send(ix)} (${stakeVault.toBase58()})`); + } else console.log("stake vault exists"); + + const posInfo = await l1.getAccountInfo(position); + if (!posInfo) { + const now = Math.floor(Date.now() / 1000); + const lockUntil = BigInt(now + 604800 + 3600); + const data = Buffer.alloc(24); + disc("stake").copy(data, 0); + data.writeBigUInt64LE(STAKE, 8); + data.writeBigUInt64LE(lockUntil, 16); + const ix = new TransactionInstruction({ programId: PROG, keys: [ + m(config, false, false), m(agent, false, true), m(position, false, true), m(owner.publicKey, true, true), + m(ownerCovnt, false, true), m(stakeVault, false, true), m(MINT, false, false), m(TOKEN22, false, false), m(SystemProgram.programId, false, false), + ], data }); + console.log(`staked ${await send(ix)} (${STAKE / U} CVNT bonded, lock +7d)`); + } else console.log(`already staked (${posInfo.data.readBigUInt64LE(72) / U} CVNT)`); + + const agentStakeOffset = 136; // disc8 + agent_key32 + operator32 + metadata32 + capability32 + const agentBonded = (await l1.getAccountInfo(agent)).data.readBigUInt64LE(agentStakeOffset); + console.log(`\nagent.stake (bonded): ${agentBonded / U} CVNT`); + + if (SLASH > 0n) { + const data = Buffer.alloc(16); + disc("slash_for_actions").copy(data, 0); + data.writeBigUInt64LE(SLASH, 8); + const ix = new TransactionInstruction({ programId: PROG, keys: [ + m(config, false, false), m(owner.publicKey, true, false), m(agent, false, true), m(position, false, true), + m(credits, false, false), m(stakeVault, false, true), m(TREASURY, false, true), m(MINT, false, false), m(TOKEN22, false, false), + ], data }); + console.log(`slashed ${await send(ix)} (${SLASH / U} CVNT slashed for its on-chain actions)`); + } + + const after = (await l1.getAccountInfo(agent)).data.readBigUInt64LE(agentStakeOffset); + console.log("\n================ BOND + SLASH ================"); + console.log(`agent bonded ${agentBonded / U} CVNT`); + console.log(`slashed ${SLASH / U} CVNT (reason = on-chain provenance_root, not caller-supplied)`); + console.log(`agent remaining ${after / U} CVNT still bonded`); + console.log(`loop bond -> act (provenance from real work) -> slash: LIVE on mainnet`); + + fs.writeFileSync(new URL("./bond-slash-result.json", import.meta.url), JSON.stringify({ + ts: new Date().toISOString(), program: PROG.toBase58(), agentKey: agentKey.toString("hex"), + agentPda: agent.toBase58(), position: position.toBase58(), credits: credits.toBase58(), + provenanceCited: Buffer.from(prov).toString("hex"), bonded: (agentBonded / U).toString(), + slashed: (SLASH / U).toString(), remaining: (after / U).toString(), + }, null, 2)); +})(); +await agentAcct; diff --git a/agent-os/programs/settlement-ephemeral/spike/check.mjs b/agent-os/programs/settlement-ephemeral/spike/check.mjs new file mode 100644 index 000000000..4ca7def40 --- /dev/null +++ b/agent-os/programs/settlement-ephemeral/spike/check.mjs @@ -0,0 +1,28 @@ +import { Connection, PublicKey } from "@solana/web3.js"; +const L1 = new Connection("https://api.devnet.solana.com", "confirmed"); +const PROG = new PublicKey("cov9UDypG7nsryxdgMcKhKU2spRVWLVjxT2iTv6do5Y"); +const OWNER = new PublicKey(process.argv[2]); // payer/owner pubkey +const pda = (s) => PublicKey.findProgramAddressSync(s, PROG)[0]; +const config = pda([Buffer.from("config")]); +const credits = pda([Buffer.from("credits"), OWNER.toBuffer()]); +console.log("program ", PROG.toBase58()); +console.log("config ", config.toBase58()); +console.log("credits ", credits.toBase58()); +const cfg = await L1.getAccountInfo(config); +if (!cfg) console.log("config: DOES NOT EXIST (need initialize)"); +else { + const d = cfg.data; + console.log("config: exists, owner", cfg.owner.toBase58(), "len", d.length); + console.log(" authority ", new PublicKey(d.subarray(8, 40)).toBase58()); + console.log(" slash_authority", new PublicKey(d.subarray(40, 72)).toBase58()); + console.log(" covnt_mint ", new PublicKey(d.subarray(72, 104)).toBase58()); + console.log(" treasury ", new PublicKey(d.subarray(104, 136)).toBase58()); + console.log(" credits_per_covnt", Number(d.readBigUInt64LE(136))); +} +const cr = await L1.getAccountInfo(credits); +if (!cr) console.log("credits: DOES NOT EXIST (need open_credit_account + buy_credits)"); +else { + console.log("credits: exists, owner-program", cr.owner.toBase58(), "len", cr.data.length); + console.log(" balance", Number(cr.data.readBigUInt64LE(40))); + console.log(" (owner-program == delegation program means currently delegated)"); +} diff --git a/agent-os/programs/settlement-ephemeral/spike/covenant-tee-attest.mjs b/agent-os/programs/settlement-ephemeral/spike/covenant-tee-attest.mjs new file mode 100644 index 000000000..923b102d2 --- /dev/null +++ b/agent-os/programs/settlement-ephemeral/spike/covenant-tee-attest.mjs @@ -0,0 +1,60 @@ +// Covenant TEE attestation: pull a TDX quote from a MagicBlock Private ER, +// verify it with Intel DCAP (Phala QVL), and relay the verified result into a +// signed Covenant attestation binding "this ER ran in a genuine attested +// enclave". Self-serve on devnet-tee. Then verify the attestation round-trip. +import fs from "node:fs"; +import os from "node:os"; +import crypto from "node:crypto"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; +import { Keypair, Connection } from "@solana/web3.js"; +const here = dirname(fileURLToPath(import.meta.url)); + +const TEE = process.env.TEE || "https://devnet-tee.magicblock.app"; +const ACCEPTABLE = new Set(["UpToDate"]); + +// --- Covenant attester identity (ed25519 Solana key) --- +const keyPath = [process.env.COVENANT_ATTESTER_KEY, `${os.homedir()}/.config/solana/covenant-agent.json`, `${os.homedir()}/.config/solana/id.json`].find((p) => p && fs.existsSync(p)); +const kp = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(fs.readFileSync(keyPath, "utf8")))); +const signEd = (msg) => crypto.sign(null, Buffer.from(msg), crypto.createPrivateKey({ key: Buffer.concat([Buffer.from("302e020100300506032b657004220420", "hex"), Buffer.from(kp.secretKey.slice(0, 32))]), format: "der", type: "pkcs8" })); +const verifyEd = (msg, sig, pub) => crypto.verify(null, Buffer.from(msg), crypto.createPublicKey({ key: Buffer.concat([Buffer.from("302a300506032b6570032100", "hex"), Buffer.from(pub)]), format: "der", type: "spki" }), sig); +const canon = (o) => Array.isArray(o) ? "[" + o.map(canon).join(",") + "]" : (o && typeof o === "object") ? "{" + Object.keys(o).sort().map((k) => JSON.stringify(k) + ":" + canon(o[k])).join(",") + "}" : JSON.stringify(o); + +// 1. fresh challenge + quote + validator identity +const challenge = crypto.randomBytes(64); +const qr = await fetch(`${TEE}/quote?challenge=${encodeURIComponent(challenge.toString("base64"))}`).then((r) => r.json()); +if (!("quote" in qr)) throw new Error("no quote: " + JSON.stringify(qr).slice(0, 200)); +const rawQuote = Uint8Array.from(Buffer.from(qr.quote, "base64")); +const idr = await fetch(TEE, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "getIdentity" }) }).then((r) => r.json()); +const validator = idr.result?.identity ?? null; + +// 2. DCAP verify (wasm from local bytes -> headless-safe) +const { default: init, js_get_collateral, js_verify } = await import("@phala/dcap-qvl-web"); +await init(fs.readFileSync(join(here, "node_modules/@phala/dcap-qvl-web/dcap-qvl-web_bg.wasm"))); +const collateral = await js_get_collateral("https://pccs.phala.network/tdx/certification/v4", rawQuote); +const result = js_verify(rawQuote, collateral, BigInt(Math.floor(Date.now() / 1000))); +const report = result.report.TD10 || result.report.TD15 || Object.values(result.report)[0]; + +// 3. bind: the verified quote must echo our challenge (anti-replay) and be UpToDate +if (report.report_data !== challenge.toString("hex")) throw new Error("report_data != challenge (stale/replayed quote)"); +if (!ACCEPTABLE.has(result.status)) throw new Error(`unacceptable TCB status: ${result.status}`); + +// 4. relay into a signed Covenant attestation +const body = { + kind: "covenant.tee.attestation.v1", + er: { rpc_url: TEE, validator, tee: "intel-tdx" }, + enclave: { status: result.status, advisory_ids: result.advisory_ids, mr_td: report.mr_td, rt_mr0: report.rt_mr0, rt_mr1: report.rt_mr1, rt_mr2: report.rt_mr2 }, + challenge: challenge.toString("base64"), + verified_at: Number(process.env.NOW_TS || Math.floor(Date.now() / 1000)), +}; +const sig = signEd(canon(body)); +const attestation = { ...body, attester: kp.publicKey.toBase58(), sig_alg: "ed25519", signature: sig.toString("base64") }; + +// 5. independent round-trip verify of the attestation +const okSig = verifyEd(canon(body), sig, kp.publicKey.toBytes()); +console.log(JSON.stringify(attestation, null, 2)); +console.log(`\nDCAP status : ${result.status}`); +console.log(`anti-replay : report_data == challenge (${report.report_data.slice(0,16)}…)`); +console.log(`attester : ${attestation.attester}`); +console.log(`signature ok : ${okSig}`); +console.log(okSig ? "\nCOVENANT TEE ATTESTATION VERIFIED ✓" : "\nSIGNATURE FAILED ✗"); diff --git a/agent-os/programs/settlement-ephemeral/spike/covenant-tee.mjs b/agent-os/programs/settlement-ephemeral/spike/covenant-tee.mjs new file mode 100644 index 000000000..926f3d175 --- /dev/null +++ b/agent-os/programs/settlement-ephemeral/spike/covenant-tee.mjs @@ -0,0 +1,130 @@ +// covenant-tee — verify a MagicBlock Private ER's TDX enclave (Intel DCAP via the +// Phala QVL) and relay the result into a signed Covenant attestation. +// +// Optionally binds an agent and its on-chain provenance root into the quote +// challenge, so the attestation proves "this agent's verifiable record came from +// this genuine, attested enclave" — the link between Covenant's accountability +// layer (provenance + slashable bonds) and confidential execution on a PER. +// +// attest({ teeUrl, signer, agent?, provenanceRoot?, now? }) -> attestation +// verifyAttestation(attestation) -> { ok, reason } +// signerFromKeypairFile(path) -> signer +// +// CLI self-test (agent-bound, round-trip): node covenant-tee.mjs [teeUrl] +import fs from "node:fs"; +import os from "node:os"; +import crypto from "node:crypto"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; +import { Keypair, PublicKey } from "@solana/web3.js"; + +const here = dirname(fileURLToPath(import.meta.url)); +const WASM = join(here, "node_modules/@phala/dcap-qvl-web/dcap-qvl-web_bg.wasm"); +const PCCS = "https://pccs.phala.network/tdx/certification/v4"; +const ACCEPTABLE = new Set(["UpToDate"]); +const BIND_DOMAIN = "covenant.tee.bind.v1"; + +const canon = (o) => + Array.isArray(o) + ? "[" + o.map(canon).join(",") + "]" + : o && typeof o === "object" + ? "{" + Object.keys(o).sort().map((k) => JSON.stringify(k) + ":" + canon(o[k])).join(",") + "}" + : JSON.stringify(o); + +const edSign = (secret32, msg) => + crypto.sign(null, Buffer.from(msg), crypto.createPrivateKey({ + key: Buffer.concat([Buffer.from("302e020100300506032b657004220420", "hex"), Buffer.from(secret32)]), + format: "der", type: "pkcs8", + })); +const edVerify = (pub32, msg, sigB64) => + crypto.verify(null, Buffer.from(msg), crypto.createPublicKey({ + key: Buffer.concat([Buffer.from("302a300506032b6570032100", "hex"), Buffer.from(pub32)]), + format: "der", type: "spki", + }), Buffer.from(sigB64, "base64")); + +export function signerFromKeypairFile(path) { + const kp = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(fs.readFileSync(path, "utf8")))); + return { pubkey: kp.publicKey.toBase58(), sign: (msg) => edSign(kp.secretKey.slice(0, 32), msg) }; +} + +// 64-byte quote challenge. With an agent + provenance root, the first 32 bytes +// commit to sha256(domain || agent || provenance_root) and the last 32 are a +// fresh nonce (anti-replay). Unbound: 64 random bytes. +function buildChallenge(agentBytes, provBytes) { + const nonce = crypto.randomBytes(32); + if (!agentBytes || !provBytes) return { challenge: Buffer.concat([crypto.randomBytes(32), nonce]), nonce }; + const binding = crypto.createHash("sha256").update(BIND_DOMAIN).update(Buffer.from(agentBytes)).update(provBytes).digest(); + return { challenge: Buffer.concat([binding, nonce]), nonce }; +} + +async function dcapVerify(rawQuote) { + const { default: init, js_get_collateral, js_verify } = await import("@phala/dcap-qvl-web"); + await init(fs.readFileSync(WASM)); // load wasm from local bytes; the -web build's HTTP fetch fails headless + const collateral = await js_get_collateral(PCCS, rawQuote); + const result = js_verify(rawQuote, collateral, BigInt(Math.floor(Date.now() / 1000))); + const report = result.report.TD10 || result.report.TD15 || Object.values(result.report)[0]; + return { status: result.status, advisory_ids: result.advisory_ids, report }; +} + +export async function attest({ teeUrl, signer, agent = null, provenanceRoot = null, now = null }) { + const agentBytes = agent ? new PublicKey(agent).toBytes() : null; + const provBytes = provenanceRoot ? Buffer.from(provenanceRoot, "hex") : null; + const { challenge, nonce } = buildChallenge(agentBytes, provBytes); + + const qr = await fetch(`${teeUrl}/quote?challenge=${encodeURIComponent(challenge.toString("base64"))}`).then((r) => r.json()); + if (!("quote" in qr)) throw new Error("no quote: " + JSON.stringify(qr).slice(0, 200)); + const rawQuote = Uint8Array.from(Buffer.from(qr.quote, "base64")); + const idr = await fetch(teeUrl, { + method: "POST", headers: { "content-type": "application/json" }, + body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "getIdentity" }), + }).then((r) => r.json()); + + const { status, advisory_ids, report } = await dcapVerify(rawQuote); + if (report.report_data !== challenge.toString("hex")) throw new Error("report_data != challenge (stale/replayed quote)"); + if (!ACCEPTABLE.has(status)) throw new Error(`unacceptable TCB status: ${status}`); + + const body = { + kind: "covenant.tee.attestation.v1", + er: { rpc_url: teeUrl, validator: idr.result?.identity ?? null, tee: "intel-tdx" }, + enclave: { status, advisory_ids, mr_td: report.mr_td, rt_mr0: report.rt_mr0, rt_mr1: report.rt_mr1, rt_mr2: report.rt_mr2 }, + subject: agent ? { agent, provenance_root: provenanceRoot } : null, + challenge: challenge.toString("base64"), + nonce: nonce.toString("base64"), + verified_at: now ?? Math.floor(Date.now() / 1000), + }; + return { ...body, attester: signer.pubkey, sig_alg: "ed25519", signature: Buffer.from(signer.sign(canon(body))).toString("base64") }; +} + +// Verify a Covenant TEE attestation: the attester's ed25519 signature (the trust +// anchor — Covenant vouches it ran the DCAP verify), the recorded TCB status, the +// agent/provenance binding committed in the quote challenge, and the nonce. +export function verifyAttestation(att) { + const { attester, sig_alg, signature, ...body } = att; + if (sig_alg !== "ed25519") return { ok: false, reason: "unsupported sig_alg" }; + if (!edVerify(new PublicKey(attester).toBytes(), canon(body), signature)) return { ok: false, reason: "bad attester signature" }; + if (!ACCEPTABLE.has(att.enclave.status)) return { ok: false, reason: `TCB status ${att.enclave.status}` }; + const challenge = Buffer.from(att.challenge, "base64"); + if (att.subject) { + const binding = crypto.createHash("sha256").update(BIND_DOMAIN) + .update(Buffer.from(new PublicKey(att.subject.agent).toBytes())) + .update(Buffer.from(att.subject.provenance_root, "hex")).digest(); + if (!binding.equals(challenge.subarray(0, 32))) return { ok: false, reason: "agent/provenance not committed in the quote" }; + } + if (!Buffer.from(att.nonce, "base64").equals(challenge.subarray(32, 64))) return { ok: false, reason: "nonce mismatch" }; + return { ok: true, reason: "verified" }; +} + +if (import.meta.url === `file://${process.argv[1]}`) { + const teeUrl = process.argv[2] || process.env.TEE || "https://devnet-tee.magicblock.app"; + const keyPath = [process.env.COVENANT_ATTESTER_KEY, `${os.homedir()}/.config/solana/covenant-agent.json`, `${os.homedir()}/.config/solana/id.json`].find((p) => p && fs.existsSync(p)); + const signer = signerFromKeypairFile(keyPath); + // A real provenance_root comes from the agent's credit account; sampled here. + const agent = signer.pubkey; + const provenanceRoot = crypto.createHash("sha256").update("sample agent action chain").digest().toString("hex"); + const att = await attest({ teeUrl, signer, agent, provenanceRoot, now: Number(process.env.NOW_TS) || undefined }); + console.log(JSON.stringify(att, null, 2)); + const v = verifyAttestation(att); + console.log(`\nbinding : agent + provenance_root committed in the quote challenge`); + console.log(`verify : ${JSON.stringify(v)}`); + console.log(v.ok ? "\nCOVENANT TEE ATTESTATION (agent-bound) VERIFIED ✓" : "\nFAILED ✗"); +} diff --git a/agent-os/programs/settlement-ephemeral/spike/er-registry.mjs b/agent-os/programs/settlement-ephemeral/spike/er-registry.mjs new file mode 100644 index 000000000..444affc80 --- /dev/null +++ b/agent-os/programs/settlement-ephemeral/spike/er-registry.mjs @@ -0,0 +1,154 @@ +// Covenant verified-ER registry. Publishes "this MagicBlock ER is +// Covenant-verified" as a Solana Attestation Service attestation keyed to the +// validator identity that getRoutes returns, and resolves it the way an agent +// would: one account read from the validator pubkey, no router change, no indexer. +// +// The data comes from covenant-tee: the ER's TDX enclave is DCAP-verified, then +// the result (TCB status + mr_td measurement) is attested on chain under the +// Covenant credential. An agent trusts it by checking the attestation's signer +// is an authorized signer of that credential. +// +// SAS_RPC=https://api.devnet.solana.com node er-registry.mjs # attest + resolve mainnet-tee +// node er-registry.mjs resolve # agent-side lookup only +import { + createSolanaRpc, createKeyPairSignerFromBytes, address, lamports, pipe, + createTransactionMessage, setTransactionMessageFeePayerSigner, + setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstructions, + signTransactionMessageWithSigners, getBase64EncodedWireTransaction, +} from "@solana/kit"; +import { + deriveCredentialPda, deriveSchemaPda, deriveAttestationPda, + getCreateCredentialInstruction, getCreateSchemaInstruction, getCreateAttestationInstruction, + fetchCredential, fetchSchema, fetchMaybeCredential, fetchMaybeSchema, fetchMaybeAttestation, + serializeAttestationData, deserializeAttestationData, +} from "sas-lib"; +import fs from "node:fs"; +import os from "node:os"; + +const CRED_NAME = "covenant"; +const SCHEMA_NAME = "er-verified"; +const SCHEMA_VERSION = 1; +const LAYOUT = Uint8Array.from([10, 12, 13, 8, 12]); // bool, String, Vec, i64, String +const FIELDS = ["verified", "status", "mr_td", "verified_at", "endpoint"]; +const SCHEMA_DESC = "Covenant-verified MagicBlock ephemeral rollup (TDX enclave, DCAP attested)"; + +export const rpcFor = (url) => createSolanaRpc(url); +export const signerFromFile = (path) => + createKeyPairSignerFromBytes(Uint8Array.from(JSON.parse(fs.readFileSync(path, "utf8")))); + +async function send(rpc, signer, ixs) { + const { value: bh } = await rpc.getLatestBlockhash().send(); + const msg = pipe( + createTransactionMessage({ version: 0 }), + (m) => setTransactionMessageFeePayerSigner(signer, m), + (m) => setTransactionMessageLifetimeUsingBlockhash(bh, m), + (m) => appendTransactionMessageInstructions(ixs, m), + ); + const signed = await signTransactionMessageWithSigners(msg); + const wire = getBase64EncodedWireTransaction(signed); + const sig = await rpc.sendTransaction(wire, { encoding: "base64", preflightCommitment: "confirmed" }).send(); + for (let i = 0; i < 40; i++) { + const st = (await rpc.getSignatureStatuses([sig]).send()).value[0]; + if (st?.confirmationStatus === "confirmed" || st?.confirmationStatus === "finalized") { + if (st.err) throw new Error("tx failed: " + JSON.stringify(st.err)); + return sig; + } + await new Promise((r) => setTimeout(r, 1500)); + } + throw new Error("confirm timeout " + sig); +} + +async function issuerPdas(authority) { + const [credential] = await deriveCredentialPda({ authority: address(authority), name: CRED_NAME }); + const [schema] = await deriveSchemaPda({ credential, name: SCHEMA_NAME, version: SCHEMA_VERSION }); + return { credential, schema }; +} + +export async function ensureIssuer(rpc, authority) { + const { credential, schema } = await issuerPdas(authority.address); + if (!(await fetchMaybeCredential(rpc, credential)).exists) { + await send(rpc, authority, [getCreateCredentialInstruction({ + payer: authority, credential, authority, name: CRED_NAME, signers: [authority.address], + })]); + } + if (!(await fetchMaybeSchema(rpc, schema)).exists) { + await send(rpc, authority, [getCreateSchemaInstruction({ + payer: authority, authority, credential, schema, + name: SCHEMA_NAME, description: SCHEMA_DESC, layout: LAYOUT, fieldNames: FIELDS, + })]); + } + return { credential, schema }; +} + +export async function attestEr(rpc, authority, validator, v) { + const { credential, schema } = await ensureIssuer(rpc, authority); + const nonce = address(validator); + const [attestation] = await deriveAttestationPda({ credential, schema, nonce }); + if ((await fetchMaybeAttestation(rpc, attestation)).exists) return { attestation, sig: null, existed: true }; + const schemaAcct = await fetchSchema(rpc, schema); + const data = serializeAttestationData(schemaAcct.data, { + verified: v.verified, status: v.status, mr_td: Array.from(v.mrTd), + verified_at: BigInt(v.verifiedAt), endpoint: v.endpoint, + }); + const sig = await send(rpc, authority, [getCreateAttestationInstruction({ + payer: authority, authority, credential, schema, attestation, + nonce, data, expiry: BigInt(v.expiry ?? 0), + })]); + return { attestation, sig, existed: false }; +} + +export async function resolveEr(rpc, issuerAuthority, validator) { + const { credential, schema } = await issuerPdas(issuerAuthority); + const [attestation] = await deriveAttestationPda({ credential, schema, nonce: address(validator) }); + const acct = await fetchMaybeAttestation(rpc, attestation); + if (!acct.exists) return { verified: false, reason: "no attestation for this validator" }; + const cred = await fetchCredential(rpc, credential); + const trustedSigner = cred.data.authorizedSigners.includes(acct.data.signer); + const notExpired = acct.data.expiry === 0n || acct.data.expiry > BigInt(Math.floor(Date.now() / 1000)); + const d = deserializeAttestationData(await fetchSchema(rpc, schema).then((s) => s.data), Uint8Array.from(acct.data.data)); + return { verified: Boolean(trustedSigner && notExpired && d.verified), trustedSigner, notExpired, attestation, signer: acct.data.signer, data: d }; +} + +if (import.meta.url === `file://${process.argv[1]}`) { + const KEY = [process.env.COVENANT_ATTESTER_KEY, `${os.homedir()}/.config/solana/covenant-agent.json`, `${os.homedir()}/.config/solana/id.json`].find((p) => p && fs.existsSync(p)); + const RPC_URL = process.env.SAS_RPC || "https://api.devnet.solana.com"; + const rpc = rpcFor(RPC_URL); + const issuer = await signerFromFile(KEY); + + if (process.argv[2] === "resolve") { + const r = await resolveEr(rpc, issuer.address, process.argv[3]); + console.log(JSON.stringify(r, (_, x) => (typeof x === "bigint" ? Number(x) : x), 2)); + process.exit(r.verified ? 0 : 1); + } + + const TEE = process.env.TEE || "https://mainnet-tee.magicblock.app"; + console.log("issuer (Covenant):", issuer.address); + console.log("SAS RPC :", RPC_URL, "| TEE:", TEE); + let bal = (await rpc.getBalance(issuer.address).send()).value; + if (bal < 50_000_000n && /devnet|localhost|127\.0\.0\.1/.test(RPC_URL)) { + console.log("airdropping devnet SOL..."); + try { + await rpc.requestAirdrop(issuer.address, lamports(1_000_000_000n)).send(); + for (let i = 0; i < 20 && bal < 50_000_000n; i++) { await new Promise((r) => setTimeout(r, 1500)); bal = (await rpc.getBalance(issuer.address).send()).value; } + } catch (e) { console.log("airdrop failed:", e.message); } + } + console.log("issuer balance :", (Number(bal) / 1e9).toFixed(4), "SOL"); + + const { attest, signerFromKeypairFile } = await import("./covenant-tee.mjs"); + const att = await attest({ teeUrl: TEE, signer: signerFromKeypairFile(KEY) }); + const validator = att.er.validator; + const mrTd = Buffer.from(String(att.enclave.mr_td).replace(/^0x/, ""), "hex"); + console.log(`\nenclave verified : ${validator} | TCB ${att.enclave.status} | mr_td ${String(att.enclave.mr_td).slice(0, 18)}...`); + + const { attestation, sig, existed } = await attestEr(rpc, issuer, validator, { + verified: att.enclave.status === "UpToDate", status: att.enclave.status, mrTd, + verifiedAt: att.verified_at, endpoint: new URL(TEE).host, + }); + console.log(`SAS attestation : ${attestation}${existed ? " (already published)" : "\n tx : " + sig}`); + + const r = await resolveEr(rpc, issuer.address, validator); + console.log(`\nagent resolve(${validator.slice(0, 8)}...):`); + console.log(" verified :", r.verified, "| trusted signer:", r.trustedSigner, "| not expired:", r.notExpired); + console.log(" data :", JSON.stringify({ ...r.data, mr_td: `<${r.data.mr_td.length} bytes>`, verified_at: Number(r.data.verified_at) })); + console.log(r.verified ? "\nCOVENANT-VERIFIED ER (on-chain via SAS) ✓" : "\nNOT VERIFIED"); +} diff --git a/agent-os/programs/settlement-ephemeral/spike/er-session.mjs b/agent-os/programs/settlement-ephemeral/spike/er-session.mjs new file mode 100644 index 000000000..62974b6e3 --- /dev/null +++ b/agent-os/programs/settlement-ephemeral/spike/er-session.mjs @@ -0,0 +1,57 @@ +// Tiny session helper for live verification: delegate / undelegate / balance. +// node er-session.mjs delegate|undelegate|balance +import fs from "node:fs"; +import os from "node:os"; +import crypto from "node:crypto"; +import { + Connection, Keypair, PublicKey, SystemProgram, + Transaction, TransactionInstruction, sendAndConfirmTransaction, +} from "@solana/web3.js"; +import { DELEGATION_PROGRAM_ID, MAGIC_PROGRAM_ID, MAGIC_CONTEXT_ID, + delegateBufferPdaFromDelegatedAccountAndOwnerProgram, + delegationRecordPdaFromDelegatedAccount, + delegationMetadataPdaFromDelegatedAccount } from "@magicblock-labs/ephemeral-rollups-sdk"; + +const PROG = new PublicKey("cov9UDypG7nsryxdgMcKhKU2spRVWLVjxT2iTv6do5Y"); +const VALIDATOR = new PublicKey("MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e"); +const payer = Keypair.fromSecretKey(new Uint8Array(JSON.parse( + fs.readFileSync(`${os.homedir()}/.config/solana/id.json`, "utf8")))); +const owner = payer.publicKey; +const l1 = new Connection("https://api.devnet.solana.com", "confirmed"); +const er = new Connection("https://devnet-eu.magicblock.app", "confirmed"); +const pda = (s) => PublicKey.findProgramAddressSync(s, PROG)[0]; +const credits = pda([Buffer.from("credits"), owner.toBuffer()]); +const disc = (n) => crypto.createHash("sha256").update(`global:${n}`).digest().subarray(0, 8); +const m = (pubkey, s, w) => ({ pubkey, isSigner: s, isWritable: w }); + +async function bal(conn) { const ai = await conn.getAccountInfo(credits); return ai ? ai.data.readBigUInt64LE(40) : null; } + +const action = process.argv[2]; +if (action === "balance") { + console.log(`L1 ${await bal(l1)} | ER ${await bal(er).catch(() => "n/a")}`); +} else if (action === "delegate") { + await sendAndConfirmTransaction(l1, new Transaction().add(new TransactionInstruction({ + programId: PROG, + keys: [ + m(owner, true, true), + m(delegateBufferPdaFromDelegatedAccountAndOwnerProgram(credits, PROG), false, true), + m(delegationRecordPdaFromDelegatedAccount(credits), false, true), + m(delegationMetadataPdaFromDelegatedAccount(credits), false, true), + m(credits, false, true), m(PROG, false, false), + m(DELEGATION_PROGRAM_ID, false, false), m(SystemProgram.programId, false, false), + m(VALIDATOR, false, false), + ], + data: disc("delegate_credits"), + })), [payer], { commitment: "confirmed" }); + console.log("delegated to EU"); +} else if (action === "undelegate") { + await sendAndConfirmTransaction(er, new Transaction().add(new TransactionInstruction({ + programId: PROG, + keys: [m(owner, true, true), m(credits, false, true), m(MAGIC_PROGRAM_ID, false, false), m(MAGIC_CONTEXT_ID, false, true)], + data: disc("undelegate_credits"), + })), [payer], { commitment: "confirmed" }); + console.log("undelegated"); +} else { + console.error("usage: node er-session.mjs delegate|undelegate|balance"); + process.exit(1); +} diff --git a/agent-os/programs/settlement-ephemeral/spike/experiment-mixed.mjs b/agent-os/programs/settlement-ephemeral/spike/experiment-mixed.mjs new file mode 100644 index 000000000..af641f61c --- /dev/null +++ b/agent-os/programs/settlement-ephemeral/spike/experiment-mixed.mjs @@ -0,0 +1,102 @@ +// Experiment for Q4: how is a transaction handled when its writable set spans a +// delegated account (credits, in the ER) AND a non-delegated L1 state change +// (a SystemProgram.transfer between normal accounts)? Send the same mixed tx to +// the Magic Router, the ER validator, and L1, and report each outcome. + +import fs from "node:fs"; +import os from "node:os"; +import crypto from "node:crypto"; +import { + Connection, Keypair, PublicKey, SystemProgram, + Transaction, TransactionInstruction, sendAndConfirmTransaction, +} from "@solana/web3.js"; +import { DELEGATION_PROGRAM_ID, MAGIC_PROGRAM_ID, MAGIC_CONTEXT_ID, + delegateBufferPdaFromDelegatedAccountAndOwnerProgram, + delegationRecordPdaFromDelegatedAccount, + delegationMetadataPdaFromDelegatedAccount } from "@magicblock-labs/ephemeral-rollups-sdk"; + +const PROG = new PublicKey("cov9UDypG7nsryxdgMcKhKU2spRVWLVjxT2iTv6do5Y"); +const VALIDATOR = new PublicKey("MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e"); +const payer = Keypair.fromSecretKey(new Uint8Array(JSON.parse( + fs.readFileSync(`${os.homedir()}/.config/solana/id.json`, "utf8")))); +const owner = payer.publicKey; +const throwaway = Keypair.generate().publicKey; + +const l1 = new Connection("https://api.devnet.solana.com", "confirmed"); +const er = new Connection("https://devnet-eu.magicblock.app", "confirmed"); +const router = new Connection("https://devnet-router.magicblock.app", "confirmed"); + +const pda = (s) => PublicKey.findProgramAddressSync(s, PROG)[0]; +const config = pda([Buffer.from("config")]); +const credits = pda([Buffer.from("credits"), owner.toBuffer()]); +const disc = (n) => crypto.createHash("sha256").update(`global:${n}`).digest().subarray(0, 8); +const le64 = (v) => { const b = Buffer.alloc(8); b.writeBigUInt64LE(BigInt(v)); return b; }; +const m = (pubkey, s, w) => ({ pubkey, isSigner: s, isWritable: w }); +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +const consumeIx = () => new TransactionInstruction({ + programId: PROG, + keys: [m(config, false, false), m(credits, false, true), m(owner, true, false)], + data: Buffer.concat([disc("consume_credits"), le64(1n), Buffer.alloc(32)]), +}); +// mixed: delegated write (credits) + a real non-delegated L1 lamport move. +const mixedTx = () => new Transaction() + .add(consumeIx()) + .add(SystemProgram.transfer({ fromPubkey: owner, toPubkey: throwaway, lamports: 1000 })); + +async function isDelegated() { + const ai = await l1.getAccountInfo(credits); + return ai && ai.owner.equals(DELEGATION_PROGRAM_ID); +} + +async function attempt(name, conn, txFactory) { + try { + const sig = await sendAndConfirmTransaction(conn, txFactory(), [payer], { commitment: "confirmed" }); + console.log(` ${name}: SUCCEEDED ${sig}`); + } catch (e) { + const logs = e?.logs ? "\n " + e.logs.slice(0, 4).join("\n ") : ""; + console.log(` ${name}: REJECTED -> ${(e.message || e).toString().split("\n")[0]}${logs}`); + } +} + +async function main() { + console.log(`credits ${credits} | throwaway ${throwaway}`); + if (!(await isDelegated())) { + console.log("delegating credits to EU validator first..."); + await sendAndConfirmTransaction(l1, new Transaction().add(new TransactionInstruction({ + programId: PROG, + keys: [ + m(owner, true, true), + m(delegateBufferPdaFromDelegatedAccountAndOwnerProgram(credits, PROG), false, true), + m(delegationRecordPdaFromDelegatedAccount(credits), false, true), + m(delegationMetadataPdaFromDelegatedAccount(credits), false, true), + m(credits, false, true), m(PROG, false, false), + m(DELEGATION_PROGRAM_ID, false, false), m(SystemProgram.programId, false, false), + m(VALIDATOR, false, false), + ], + data: disc("delegate_credits"), + })), [payer], { commitment: "confirmed" }); + await sleep(6000); + } + console.log(`delegated: ${await isDelegated()}\n`); + + console.log("[control] pure consume_credits (delegated-only writable set):"); + await attempt("-> ER ", er, () => new Transaction().add(consumeIx())); + await attempt("-> L1 ", l1, () => new Transaction().add(consumeIx())); + + console.log("\n[mixed] consume_credits (delegated) + system transfer (non-delegated L1 write):"); + await attempt("-> router", router, mixedTx); + await attempt("-> ER ", er, mixedTx); + await attempt("-> L1 ", l1, mixedTx); + + console.log("\ncleanup: undelegate"); + try { + await sendAndConfirmTransaction(er, new Transaction().add(new TransactionInstruction({ + programId: PROG, + keys: [m(owner, true, true), m(credits, false, true), m(MAGIC_PROGRAM_ID, false, false), m(MAGIC_CONTEXT_ID, false, true)], + data: disc("undelegate_credits"), + })), [payer], { commitment: "confirmed" }); + console.log(" undelegated."); + } catch (e) { console.log(" undelegate err:", (e.message || e).toString().split("\n")[0]); } +} +main().catch((e) => { console.error("FATAL:", e.message || e); process.exit(1); }); diff --git a/agent-os/programs/settlement-ephemeral/spike/migrate-credits.mjs b/agent-os/programs/settlement-ephemeral/spike/migrate-credits.mjs new file mode 100644 index 000000000..a07486692 --- /dev/null +++ b/agent-os/programs/settlement-ephemeral/spike/migrate-credits.mjs @@ -0,0 +1,38 @@ +// Post-upgrade migration: realloc a legacy 49-byte CreditAccount to the new +// 81-byte layout (zero-fills provenance_root). Owner-gated, idempotent. Run once +// per account after the settlement program upgrade ships provenance_root. +// KEYPAIR=~/.config/solana/solana-id.json node migrate-credits.mjs # migrate DrawYGmd (8xbXHA) +import { Connection, Keypair, PublicKey, Transaction, TransactionInstruction, SystemProgram, sendAndConfirmTransaction } from "@solana/web3.js"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; + +const PROG = new PublicKey("cov9UDypG7nsryxdgMcKhKU2spRVWLVjxT2iTv6do5Y"); +const NEW_LEN = 81; // 8 disc + 32 owner + 8 balance + 1 bump + 32 provenance_root +const RPC = (() => { try { return fs.readFileSync("/tmp/cov-deploy-rpc", "utf8").trim(); } catch { return process.env.L1 || "https://solana-rpc.publicnode.com"; } })(); +const c = new Connection(RPC, "confirmed"); + +const path = (process.env.KEYPAIR || `${os.homedir()}/.config/solana/solana-id.json`).replace(/^~/, os.homedir()); +const kp = Keypair.fromSecretKey(new Uint8Array(JSON.parse(fs.readFileSync(path, "utf8")))); +const disc = (n) => crypto.createHash("sha256").update(`global:${n}`).digest().subarray(0, 8); +const credits = PublicKey.findProgramAddressSync([Buffer.from("credits"), kp.publicKey.toBuffer()], PROG)[0]; + +const before = await c.getAccountInfo(credits); +console.log("owner :", kp.publicKey.toBase58()); +console.log("credits :", credits.toBase58(), "| len", before ? before.data.length : "none"); +if (!before) { console.log("no credit account for this owner; nothing to migrate"); process.exit(0); } +if (before.data.length >= NEW_LEN) { console.log("already migrated"); process.exit(0); } + +const ix = new TransactionInstruction({ + programId: PROG, + keys: [ + { pubkey: credits, isSigner: false, isWritable: true }, + { pubkey: kp.publicKey, isSigner: true, isWritable: true }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + ], + data: disc("migrate_credit_account"), +}); +const sig = await sendAndConfirmTransaction(c, new Transaction().add(ix), [kp], { commitment: "confirmed" }); +const after = await c.getAccountInfo(credits); +console.log(`migrated: ${sig}`); +console.log(`len ${before.data.length} -> ${after.data.length}`); diff --git a/agent-os/programs/settlement-ephemeral/spike/package-lock.json b/agent-os/programs/settlement-ephemeral/spike/package-lock.json new file mode 100644 index 000000000..f84403566 --- /dev/null +++ b/agent-os/programs/settlement-ephemeral/spike/package-lock.json @@ -0,0 +1,1369 @@ +{ + "name": "settlement-er-spike", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "settlement-er-spike", + "dependencies": { + "@coral-xyz/anchor": "^0.31.1", + "@magicblock-labs/ephemeral-rollups-sdk": "^0.2.6", + "@solana/web3.js": "^1.98.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@coral-xyz/anchor": { + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/@coral-xyz/anchor/-/anchor-0.31.1.tgz", + "integrity": "sha512-QUqpoEK+gi2S6nlYc2atgT2r41TT3caWr/cPUEL8n8Md9437trZ68STknq897b82p5mW0XrTBNOzRbmIRJtfsA==", + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "@coral-xyz/anchor-errors": "^0.31.1", + "@coral-xyz/borsh": "^0.31.1", + "@noble/hashes": "^1.3.1", + "@solana/web3.js": "^1.69.0", + "bn.js": "^5.1.2", + "bs58": "^4.0.1", + "buffer-layout": "^1.2.2", + "camelcase": "^6.3.0", + "cross-fetch": "^3.1.5", + "eventemitter3": "^4.0.7", + "pako": "^2.0.3", + "superstruct": "^0.15.4", + "toml": "^3.0.0" + }, + "engines": { + "node": ">=17" + } + }, + "node_modules/@coral-xyz/anchor-errors": { + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/@coral-xyz/anchor-errors/-/anchor-errors-0.31.1.tgz", + "integrity": "sha512-NhNEku4F3zzUSBtrYz84FzYWm48+9OvmT1Hhnwr6GnPQry2dsEqH/ti/7ASjjpoFTWRnPXrjAIT1qM6Isop+LQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/@coral-xyz/borsh": { + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/@coral-xyz/borsh/-/borsh-0.31.1.tgz", + "integrity": "sha512-9N8AU9F0ubriKfNE3g1WF0/4dtlGXoBN/hd1PvbNBamBNwRgHxH4P+o3Zt7rSEloW1HUs6LfZEchlx9fW7POYw==", + "license": "Apache-2.0", + "dependencies": { + "bn.js": "^5.1.2", + "buffer-layout": "^1.2.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@solana/web3.js": "^1.69.0" + } + }, + "node_modules/@magicblock-labs/ephemeral-rollups-sdk": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@magicblock-labs/ephemeral-rollups-sdk/-/ephemeral-rollups-sdk-0.2.11.tgz", + "integrity": "sha512-I9pqYBsWhkUpndt9b4P6jqpVhAKaMdyyiBuxEX+Y2VdWeXuRpF0qXngyn26i1oKCVvbPNQYLu033LpNeSB4BKg==", + "license": "MIT", + "dependencies": { + "@metaplex-foundation/beet": "^0.7.2", + "@phala/dcap-qvl-web": "^0.2.7", + "@solana/web3.js": "^1.98.0", + "bs58": "^6.0.0", + "rpc-websockets": "^9.0.4", + "typescript": "^5.3.0" + } + }, + "node_modules/@magicblock-labs/ephemeral-rollups-sdk/node_modules/base-x": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", + "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", + "license": "MIT" + }, + "node_modules/@magicblock-labs/ephemeral-rollups-sdk/node_modules/bs58": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", + "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", + "license": "MIT", + "dependencies": { + "base-x": "^5.0.0" + } + }, + "node_modules/@metaplex-foundation/beet": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/beet/-/beet-0.7.2.tgz", + "integrity": "sha512-K+g3WhyFxKPc0xIvcIjNyV1eaTVJTiuaHZpig7Xx0MuYRMoJLLvhLTnUXhFdR5Tu2l2QSyKwfyXDgZlzhULqFg==", + "license": "Apache-2.0", + "dependencies": { + "ansicolors": "^0.3.2", + "assert": "^2.1.0", + "bn.js": "^5.2.0", + "debug": "^4.3.3" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@phala/dcap-qvl-web": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@phala/dcap-qvl-web/-/dcap-qvl-web-0.2.7.tgz", + "integrity": "sha512-OgDIN8ZRsLg0dJgUAk0HCXMjkAmrif7p0C+P74YrtxgE/8fNSFpqNDjVW3mCVB2Q/V7X6mUhbEQWa5wJmM9OSQ==", + "license": "MIT" + }, + "node_modules/@solana/buffer-layout": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz", + "integrity": "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==", + "license": "MIT", + "dependencies": { + "buffer": "~6.0.3" + }, + "engines": { + "node": ">=5.10" + } + }, + "node_modules/@solana/codecs-core": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.3.0.tgz", + "integrity": "sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==", + "license": "MIT", + "dependencies": { + "@solana/errors": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/codecs-numbers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.3.0.tgz", + "integrity": "sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.3.0", + "@solana/errors": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/errors": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.3.0.tgz", + "integrity": "sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==", + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^14.0.0" + }, + "bin": { + "errors": "bin/cli.mjs" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/web3.js": { + "version": "1.98.4", + "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.98.4.tgz", + "integrity": "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "@noble/curves": "^1.4.2", + "@noble/hashes": "^1.4.0", + "@solana/buffer-layout": "^4.0.1", + "@solana/codecs-numbers": "^2.1.0", + "agentkeepalive": "^4.5.0", + "bn.js": "^5.2.1", + "borsh": "^0.7.0", + "bs58": "^4.0.1", + "buffer": "6.0.3", + "fast-stable-stringify": "^1.0.0", + "jayson": "^4.1.1", + "node-fetch": "^2.7.0", + "rpc-websockets": "^9.0.2", + "superstruct": "^2.0.2" + } + }, + "node_modules/@solana/web3.js/node_modules/superstruct": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz", + "integrity": "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "license": "MIT" + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "7.4.7", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", + "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/ansicolors": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", + "integrity": "sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==", + "license": "MIT" + }, + "node_modules/assert": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/base-x": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", + "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bn.js": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", + "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", + "license": "MIT" + }, + "node_modules/borsh": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz", + "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==", + "license": "Apache-2.0", + "dependencies": { + "bn.js": "^5.2.0", + "bs58": "^4.0.0", + "text-encoding-utf-8": "^1.0.2" + } + }, + "node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "license": "MIT", + "dependencies": { + "base-x": "^3.0.2" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-layout": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/buffer-layout/-/buffer-layout-1.2.2.tgz", + "integrity": "sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA==", + "license": "MIT", + "engines": { + "node": ">=4.5" + } + }, + "node_modules/bufferutil": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz", + "integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/cross-fetch": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", + "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delay": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz", + "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "license": "MIT" + }, + "node_modules/es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", + "license": "MIT", + "dependencies": { + "es6-promise": "^4.0.3" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", + "engines": { + "node": "> 0.1.90" + } + }, + "node_modules/fast-stable-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz", + "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==", + "license": "MIT" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isomorphic-ws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/jayson": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/jayson/-/jayson-4.3.0.tgz", + "integrity": "sha512-AauzHcUcqs8OBnCHOkJY280VaTiCm57AbuO7lqzcw7JapGj50BisE3xhksye4zlTSR1+1tAz67wLTl8tEH1obQ==", + "license": "MIT", + "dependencies": { + "@types/connect": "^3.4.33", + "@types/node": "^12.12.54", + "@types/ws": "^7.4.4", + "commander": "^2.20.3", + "delay": "^5.0.0", + "es6-promisify": "^5.0.0", + "eyes": "^0.1.8", + "isomorphic-ws": "^4.0.1", + "json-stringify-safe": "^5.0.1", + "stream-json": "^1.9.1", + "uuid": "^8.3.2", + "ws": "^7.5.10" + }, + "bin": { + "jayson": "bin/jayson.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jayson/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/rpc-websockets": { + "version": "9.3.9", + "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-9.3.9.tgz", + "integrity": "sha512-2iQDaTB4g5fDB2ihrTFSJSibCEuxaRi1q7qTW7ZO9/M5/TC+ToHA4D9/ffNLEbAoHNNrcdeP05oATNk44SKZXA==", + "license": "LGPL-3.0-only", + "dependencies": { + "@swc/helpers": "^0.5.11", + "@types/uuid": "^10.0.0", + "@types/ws": "^8.2.2", + "buffer": "^6.0.3", + "eventemitter3": "^5.0.1", + "uuid": "^14.0.0", + "ws": "^8.5.0" + }, + "funding": { + "type": "paypal", + "url": "https://paypal.me/kozjak" + }, + "optionalDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^6.0.0" + } + }, + "node_modules/rpc-websockets/node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/rpc-websockets/node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/rpc-websockets/node_modules/utf-8-validate": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.6.tgz", + "integrity": "sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/rpc-websockets/node_modules/uuid": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/rpc-websockets/node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/stream-chain": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", + "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==", + "license": "BSD-3-Clause" + }, + "node_modules/stream-json": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz", + "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==", + "license": "BSD-3-Clause", + "dependencies": { + "stream-chain": "^2.2.5" + } + }, + "node_modules/superstruct": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.15.5.tgz", + "integrity": "sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ==", + "license": "MIT" + }, + "node_modules/text-encoding-utf-8": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", + "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==" + }, + "node_modules/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", + "license": "MIT" + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "extraneous": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ws": { + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", + "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/agent-os/programs/settlement-ephemeral/spike/package.json b/agent-os/programs/settlement-ephemeral/spike/package.json new file mode 100644 index 000000000..c496d69c2 --- /dev/null +++ b/agent-os/programs/settlement-ephemeral/spike/package.json @@ -0,0 +1,13 @@ +{ + "name": "settlement-er-spike", + "private": true, + "type": "module", + "description": "Devnet spike: delegate a Covenant credit account, meter in the MagicBlock ER, undelegate, reconcile on L1.", + "scripts": { "spike": "node run-spike.mjs", "tee-attest": "node covenant-tee.mjs" }, + "dependencies": { + "@coral-xyz/anchor": "^0.31.1", + "@solana/web3.js": "^1.98.0", + "@magicblock-labs/ephemeral-rollups-sdk": "^0.2.6", + "@phala/dcap-qvl-web": "^0.2.7" + } +} diff --git a/agent-os/programs/settlement-ephemeral/spike/pick-verified-er.mjs b/agent-os/programs/settlement-ephemeral/spike/pick-verified-er.mjs new file mode 100644 index 000000000..2a9db5d22 --- /dev/null +++ b/agent-os/programs/settlement-ephemeral/spike/pick-verified-er.mjs @@ -0,0 +1,32 @@ +// Agent-side discovery: ask the Magic Router for its ERs, then keep only the +// ones a Covenant credential has attested as verified. This is the whole point +// of the registry: an agent picks an execution endpoint and can check the trust +// signal in one account read per validator, no router change, no indexer. +// +// node pick-verified-er.mjs # mainnet router, devnet attestations (the demo split) +// ROUTER=... SAS_RPC=... COVENANT_ISSUER=... node pick-verified-er.mjs +import { rpcFor, resolveEr } from "./er-registry.mjs"; + +const ROUTER = process.env.ROUTER || "https://router.magicblock.app"; +const SAS_RPC = process.env.SAS_RPC || "https://api.devnet.solana.com"; +const ISSUER = process.env.COVENANT_ISSUER || "AdChcSmDKX57rU9qChMJ3MKnqNZbmiQAjuns9VCjzqRb"; + +const routes = (await (await fetch(ROUTER, { + method: "POST", headers: { "content-type": "application/json" }, + body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "getRoutes" }), +})).json()).result; + +const rpc = rpcFor(SAS_RPC); +console.log(`router ${ROUTER}: ${routes.length} ERs`); +console.log(`Covenant issuer ${ISSUER} via ${SAS_RPC}\n`); + +const checked = []; +for (const v of routes) { + const r = await resolveEr(rpc, ISSUER, v.identity); + checked.push({ ...v, verified: r.verified, tcb: r.data?.status }); + console.log(` ${r.verified ? "OK verified " : " unverified"} ${v.identity} ${v.fqdn} ${v.countryCode}${r.verified ? ` [TCB ${r.data.status}]` : ""}`); +} + +const verified = checked.filter((v) => v.verified); +console.log(`\n${verified.length}/${checked.length} Covenant-verified.`); +if (verified.length) console.log(`agent picks -> ${verified[0].fqdn} (${verified[0].identity})`); diff --git a/agent-os/programs/settlement-ephemeral/spike/recovery-keeper.mjs b/agent-os/programs/settlement-ephemeral/spike/recovery-keeper.mjs new file mode 100644 index 000000000..7a0063c1b --- /dev/null +++ b/agent-os/programs/settlement-ephemeral/spike/recovery-keeper.mjs @@ -0,0 +1,133 @@ +// Covenant recovery keeper — keeps delegated credit accounts recoverable automatically. +// Watches each owner's credit PDA on the pinned ER. When a delegated session goes idle, +// exceeds its max lifetime, or the validator stalls, it issues undelegate_credits to bring +// the account (with its committed balance) home to L1. No manual step: "comes home on its own." +// +// Scope: today this issues the validator-cooperative commit_and_undelegate, which recovers +// whenever the validator is reachable (idle / lifetime / soft stall). The validator-DOWN case is +// ALSO recoverable without MagicBlock: per the dev, issue an undelegation request on L1 and after +// the 30-min timeout the state unlocks permissionlessly at the last committed balance. Wiring that +// permissionless L1 request here (or adopting the simplified SDK call once it ships) is a TODO, +// not a MagicBlock dependency. +// +// KEYPAIR=~/.config/solana/id.json node recovery-keeper.mjs +// KEYPAIRS=a.json,b.json ER=https://eu.magicblock.app IDLE_MS=600000 node recovery-keeper.mjs +// DRY_RUN=1 node recovery-keeper.mjs # decide + log, send nothing +// ONCE=1 DRY_RUN=1 node recovery-keeper.mjs # single pass then exit (smoke test) +import fs from "node:fs"; +import os from "node:os"; +import crypto from "node:crypto"; +import { Connection, Keypair, PublicKey, Transaction, TransactionInstruction, sendAndConfirmTransaction } from "@solana/web3.js"; +import { DELEGATION_PROGRAM_ID, MAGIC_PROGRAM_ID, MAGIC_CONTEXT_ID } from "@magicblock-labs/ephemeral-rollups-sdk"; +import { getAuthToken } from "@magicblock-labs/ephemeral-rollups-sdk/privacy"; + +const PROG = new PublicKey("cov9UDypG7nsryxdgMcKhKU2spRVWLVjxT2iTv6do5Y"); +const ER_URL = process.env.ER || "https://mainnet.magicblock.app"; +const POLL_MS = +(process.env.POLL_MS || 30_000); +const IDLE_MS = +(process.env.IDLE_MS || 15 * 60_000); +const MAX_LIFETIME_MS = +(process.env.MAX_LIFETIME_MS || 30 * 60_000); +const STALL_PROBES = +(process.env.STALL_PROBES || 3); +const RETRY_MS = +(process.env.RETRY_MS || 60_000); +const DRY_RUN = !!process.env.DRY_RUN; +const ONCE = !!process.env.ONCE; + +const expand = (p) => p.replace(/^~/, os.homedir()); +const kpPaths = (process.env.KEYPAIRS || process.env.KEYPAIR || `${os.homedir()}/.config/solana/id.json`) + .split(",").map((s) => expand(s.trim())).filter(Boolean); +const owners = kpPaths.map((p) => Keypair.fromSecretKey(new Uint8Array(JSON.parse(fs.readFileSync(p, "utf8"))))); + +const l1Url = (() => { try { return fs.readFileSync("/tmp/cov-deploy-rpc", "utf8").trim(); } catch { return process.env.L1 || "https://solana-rpc.publicnode.com"; } })(); +const l1 = new Connection(l1Url, "confirmed"); +const signWith = (owner) => async (msg) => { + const seed = Buffer.from(owner.secretKey.slice(0, 32)); + const der = Buffer.concat([Buffer.from("302e020100300506032b657004220420", "hex"), seed]); + const key = crypto.createPrivateKey({ key: der, format: "der", type: "pkcs8" }); + return new Uint8Array(crypto.sign(null, Buffer.from(msg), key)); +}; +const erUrl = await (async () => { // TEE endpoints need a self-serve auth token (per-pubkey JWT) + if (process.env.ER_TOKEN) return ER_URL + (ER_URL.includes("?") ? "&" : "?") + "token=" + process.env.ER_TOKEN; + if (!/tee\./.test(ER_URL)) return ER_URL; + const token = await getAuthToken(ER_URL, owners[0].publicKey, signWith(owners[0])); + return ER_URL + (ER_URL.includes("?") ? "&" : "?") + "token=" + token; +})(); +const er = new Connection(erUrl, "confirmed"); + +const disc = (n) => crypto.createHash("sha256").update(`global:${n}`).digest().subarray(0, 8); +const m = (pubkey, s, w) => ({ pubkey, isSigner: s, isWritable: w }); +const creditsPda = (owner) => PublicKey.findProgramAddressSync([Buffer.from("credits"), owner.toBuffer()], PROG)[0]; +const bal = (ai) => (ai ? ai.data.readBigUInt64LE(40) : null); +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +const short = (s) => `${s.slice(0, 8)}…`; +const log = (...a) => console.log(new Date().toISOString(), ...a); +const withTimeout = (p, ms) => Promise.race([p, new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), ms))]); + +const state = new Map(); + +async function validatorOk() { + try { await withTimeout(er.getVersion(), 8_000); return true; } catch { return false; } +} + +async function undelegate(owner, credits) { + const ix = new TransactionInstruction({ + programId: PROG, + keys: [m(owner.publicKey, true, true), m(credits, false, true), m(MAGIC_PROGRAM_ID, false, false), m(MAGIC_CONTEXT_ID, false, true)], + data: disc("undelegate_credits"), + }); + return withTimeout(sendAndConfirmTransaction(er, new Transaction().add(ix), [owner], { commitment: "confirmed" }), 30_000); +} + +async function checkAccount(owner, healthFails) { + const credits = creditsPda(owner.publicKey); + const key = credits.toBase58(); + let st = state.get(key); + if (!st) { st = { since: 0, bal: null, active: 0, attempt: 0 }; state.set(key, st); } + + const ai = await l1.getAccountInfo(credits).catch(() => null); + if (!ai) return; // never opened + if (!ai.owner.equals(DELEGATION_PROGRAM_ID)) { // home / not delegated + if (st.since) log(`${short(key)} home (owner ${short(ai.owner.toBase58())}, bal ${bal(ai)})`); + st.since = 0; st.bal = null; st.active = 0; + return; + } + + const t = Date.now(); + if (!st.since) { st.since = t; st.active = t; log(`${short(key)} delegated — watching`); } + const erbal = bal(await er.getAccountInfo(credits).catch(() => null)); + if (erbal !== null && erbal !== st.bal) { st.bal = erbal; st.active = t; } + + const idleFor = t - st.active, lifeFor = t - st.since; + let reason = null; + if (lifeFor >= MAX_LIFETIME_MS) reason = `max-lifetime ${(lifeFor / 1e3) | 0}s`; + else if (idleFor >= IDLE_MS) reason = `idle ${(idleFor / 1e3) | 0}s`; + else if (healthFails >= STALL_PROBES) reason = `validator stall (${healthFails} probes)`; + if (!reason) return; + if (t - st.attempt < RETRY_MS) return; // throttle retries + st.attempt = t; + + log(`${short(key)} RECOVER (${reason}) erbal=${erbal}`); + if (DRY_RUN) { log(` DRY_RUN — would send undelegate_credits`); return; } + try { + const sig = await undelegate(owner, credits); + log(` undelegated ${sig}`); + await sleep(8_000); + const after = await l1.getAccountInfo(credits).catch(() => null); + if (after && after.owner.equals(PROG)) log(` HOME ✓ bal ${bal(after)}`); + else log(` still delegated — will retry`); + } catch (e) { + log(` undelegate failed (${e.message}); validator may be down. fallback: permissionless L1 undelegation request + 30-min timeout (TODO)`); + } +} + +async function tick() { + const ok = await validatorOk(); + tick.fails = ok ? 0 : (tick.fails || 0) + 1; + if (!ok) log(`validator probe failed (${tick.fails}/${STALL_PROBES}) ${ER_URL}`); + for (const o of owners) { + try { await checkAccount(o, tick.fails); } catch (e) { log(`check error ${short(o.publicKey.toBase58())}: ${e.message}`); } + } +} + +log(`recovery-keeper up — ER ${ER_URL} — ${owners.length} account(s) — idle ${IDLE_MS}ms lifetime ${MAX_LIFETIME_MS}ms poll ${POLL_MS}ms${DRY_RUN ? " [DRY_RUN]" : ""}`); +for (const o of owners) log(` watch ${creditsPda(o.publicKey).toBase58()} owner ${o.publicKey.toBase58()}`); +log(` L1 ${l1Url.replace(/\?.*$/, "?…")}`); +do { await tick(); if (!ONCE) await sleep(POLL_MS); } while (!ONCE); diff --git a/agent-os/programs/settlement-ephemeral/spike/reference-run-result.json b/agent-os/programs/settlement-ephemeral/spike/reference-run-result.json new file mode 100644 index 000000000..213d65d14 --- /dev/null +++ b/agent-os/programs/settlement-ephemeral/spike/reference-run-result.json @@ -0,0 +1,50 @@ +{ + "ts": "2026-07-01T11:57:21.858Z", + "program": "cov9UDypG7nsryxdgMcKhKU2spRVWLVjxT2iTv6do5Y", + "operator": "8xbXHAhiVe2BrYDq4qpTA5SSYJG9XNjNN6jcrudhTKCM", + "credits": "DrawYGmdbQ7sULxzzczUqyZT2nmP8SZeYPuJzy6TNksj", + "er": "https://mainnet-tee.magicblock.app", + "validator": "MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo", + "model": "claude-haiku-4-5", + "rootBefore": "0000000000000000000000000000000000000000000000000000000000000000", + "rootAfterL1": "2769ee46c8c7dc49e38737c8a3c6d0f57a48553d9b2af08d0bc82cf80ce88933", + "recomputed": "2769ee46c8c7dc49e38737c8a3c6d0f57a48553d9b2af08d0bc82cf80ce88933", + "provenanceVerified": true, + "work": [ + { + "intent": "What is Covenant and what does this sandbox show?", + "reply": "Covenant is a framework for building AI agent systems with cryptographic signing and audit trails—every action gets logged and verified so you can trust what your agents actually did. This sandbox demonstrates how Covenant routes your requests through a signed capability system: your question hits the daemon, gets checked for permissions, gets dispatched to me (a Haiku-backed demo agent), and the whole exchange gets logged and signed. You can open the activity log to see exactly that happen. The real power emerges when you run Covenant locally and plug in your own specialized agents with custom capabilities—then you get auditability and control at scale.", + "receipt": "9515226c6cb6219575194926c879f0b07015196e42a8a3ab3c1cd85da36e33b6" + }, + { + "intent": "Can you read my files or reach the internet from here?", + "reply": "No to both—I'm running in a sandboxed environment with no file access and no internet connectivity. I'm mainly here to demo how Covenant routes requests, checks capabilities, and logs everything with cryptographic signatures. If you want to see that in action, check the activity log for this exchange; all the steps are signed and auditable.", + "receipt": "4727b9737ad1538e1581f37bf512a723e32d585de311054ec445dadf0302124a" + }, + { + "intent": "How would I verify this exchange actually happened?", + "reply": "Great question! You can open the **activity log** in your Covenant interface—it will show this entire exchange cryptographically signed by the daemon, proving I actually ran and replied. Each step (your request, the capability check, my response) gets logged and signed, so you have an immutable record that this happened in the sandbox.\n\nThat's one of Covenant's core features: every agent interaction is auditable. When you run Covenant locally with your own agents, you get the same signed logs for verification and compliance.", + "receipt": "f3766305b23f81c07ec9e710c68e947e5b2b7da422537b7edfee6b9278619f7e" + }, + { + "intent": "What changes if I run Covenant locally with my own agent?", + "reply": "Running Covenant locally with your own agent unlocks the real power: you can connect it to your actual tools, data, and systems—databases, APIs, internal services—while keeping the same cryptographic signing and audit trail for every action. Instead of a demo with no capabilities, you get a fully auditable AI agent that can actually do meaningful work in your environment. You can also customize the agent's instructions, integrate it with your workflows, and control exactly what it has access to. The sandboxed demo here just shows the *routing and logging mechanism*—locally, you get that mechanism protecting real capabilities.", + "receipt": "d963f2eff7c1c3d5cbfb0f267366562217d526ea59350cfc0142986c12739247" + }, + { + "intent": "In one sentence, what did you just prove?", + "reply": "I just proved that I can receive a system prompt, understand my constraints (sandboxed, no memory, signed logging), and respond appropriately—demonstrating how Covenant routes tasks through signed agents with audit trails.", + "receipt": "d391321e209787133d7cd10f9ffc90dd50dd4107eaa5ec04725a05018d23936d" + } + ], + "sigs": { + "meter": [ + "3C5GcXioyZGYW6dZ9n6bcZ4rH9Ww3cEmqpopZ5VxfPesyh6nFo19RQ9uSSMdAiZzKXprimeykzy19w8hGWTJsF1n", + "4h2mY4nks1omrybwV8uZReeJzXKzosMCLUHi4eKKyQJJagtMYyMhk3A4EJerJ3RbK3k13DeYqDaeGLLXQhUioqYf", + "3NtvjfvR9s6aVbf4tCnZJYf5iPYUX5ns711xc1CBkHzPLYRkh26swfRcA4RAhxyDufuCYwmvPkTx344va7xfKdG1", + "5APCnuRUt1Jw4Bv8wTswrDZUMqWxaF3FUCRjPiJcwq6gfwyyEnEhHgD3ZP4gQsYGyXMbXhCRXgXcWrxEFQbL5Fhq", + "3uBYcwAC9EDTUuujuqse9mhdB65UpKJjqZJ8daEdbY71FAcPJaekL1KiVo4bMVt94hC4rJHeCU9tJ6iqTw8ymZ28" + ], + "undelegate": "9fASRWbyGGZJMa46GBPKBZ2ZW3SWxbtTDWB5uMCkjRQpHjk32ZPBiui9t1sJzWP8dkYsncf5jMbnRSrfexNrBEK" + } +} \ No newline at end of file diff --git a/agent-os/programs/settlement-ephemeral/spike/reference-run.mjs b/agent-os/programs/settlement-ephemeral/spike/reference-run.mjs new file mode 100644 index 000000000..309357afd --- /dev/null +++ b/agent-os/programs/settlement-ephemeral/spike/reference-run.mjs @@ -0,0 +1,178 @@ +// Covenant reference deployment: our own demo agent, real work, metered on +// mainnet through the verified MagicBlock ER, with the on-chain provenance root +// proven to equal the hash-chain of that real work. +// +// For each intent the demo agent answers for real (Haiku 4.5, its exact sandbox +// system prompt). Each answer is metered by consume_credits in the ER (gasless), +// which folds sha256(prev_root || sha256(work)) into the credit account's +// provenance_root and commits it to L1 on undelegate. We then recompute the fold +// off chain from the work items and assert it matches the on-chain root, so the +// record is provably the agent's real output, not an arbitrary hash. +// +// ER=https://mainnet-tee.magicblock.app VALIDATOR=MTEW... node reference-run.mjs +// ER=https://mainnet.magicblock.app VALIDATOR=MAS1... node reference-run.mjs # open fallback +import fs from "node:fs"; +import os from "node:os"; +import crypto from "node:crypto"; +import { Connection, Keypair, PublicKey, SystemProgram, Transaction, TransactionInstruction, sendAndConfirmTransaction } from "@solana/web3.js"; +import { + DELEGATION_PROGRAM_ID, MAGIC_PROGRAM_ID, MAGIC_CONTEXT_ID, + delegateBufferPdaFromDelegatedAccountAndOwnerProgram, + delegationRecordPdaFromDelegatedAccount, + delegationMetadataPdaFromDelegatedAccount, +} from "@magicblock-labs/ephemeral-rollups-sdk"; +import { getAuthToken } from "@magicblock-labs/ephemeral-rollups-sdk/privacy"; + +const PROG = new PublicKey("cov9UDypG7nsryxdgMcKhKU2spRVWLVjxT2iTv6do5Y"); +const ER_URL = process.env.ER || "https://mainnet-tee.magicblock.app"; +const VALIDATOR = new PublicKey(process.env.VALIDATOR || "MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo"); +const MODEL = "claude-haiku-4-5"; +const IS_TEE = /tee\./.test(ER_URL); + +const SYSTEM_PROMPT = + "You are the demo agent on Covenant's public sandbox. Reply briefly, 2 to 4 sentences. " + + "You're running as a sandboxed agent inside a Covenant daemon that signs and logs every step " + + "(capability check, dispatch, your reply). When it fits the user's question, mention that they " + + "can open the activity log to see this exchange signed. You have no internet, no file access, " + + "and no memory of prior turns on this sandbox. If asked what you can do: say you're a Haiku-backed " + + "demo for showing how Covenant routes, signs, and audits tasks, and that the real value shows up " + + "when the user runs Covenant locally and plugs in their own agents."; + +const INTENTS = [ + "What is Covenant and what does this sandbox show?", + "Can you read my files or reach the internet from here?", + "How would I verify this exchange actually happened?", + "What changes if I run Covenant locally with my own agent?", + "In one sentence, what did you just prove?", +]; + +const ANTHROPIC_KEY = (() => { + if (process.env.ANTHROPIC_API_KEY) return process.env.ANTHROPIC_API_KEY; + for (const p of [`${os.homedir()}/Projects/covenant/.env`]) { + try { + const m = fs.readFileSync(p, "utf8").match(/^ANTHROPIC_API_KEY=(.+)$/m); + if (m) return m[1].trim().replace(/^["']|["']$/g, ""); + } catch {} + } + return null; +})(); + +const owner = Keypair.fromSecretKey(new Uint8Array(JSON.parse(fs.readFileSync(`${os.homedir()}/.config/solana/solana-id.json`, "utf8")))); +const l1 = new Connection(fs.readFileSync("/tmp/cov-deploy-rpc", "utf8").trim(), "confirmed"); +const pda = (s) => PublicKey.findProgramAddressSync(s, PROG)[0]; +const credits = pda([Buffer.from("credits"), owner.publicKey.toBuffer()]); +const config = pda([Buffer.from("config")]); +const disc = (n) => crypto.createHash("sha256").update(`global:${n}`).digest().subarray(0, 8); +const sha256 = (...b) => crypto.createHash("sha256").update(Buffer.concat(b.map(Buffer.from))).digest(); +const m = (pubkey, s, w) => ({ pubkey, isSigner: s, isWritable: w }); +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +const send = (conn, ix, opts = {}) => sendAndConfirmTransaction(conn, new Transaction().add(ix), [owner], { commitment: "confirmed", ...opts }); +const provRoot = async (conn) => { const ai = await conn.getAccountInfo(credits); return ai && ai.data.length >= 81 ? Buffer.from(ai.data.subarray(49, 81)) : null; }; +const canon = (o) => JSON.stringify(Object.keys(o).sort().reduce((a, k) => ((a[k] = o[k]), a), {})); + +async function agentWork(intent) { + if (!ANTHROPIC_KEY) return "[no ANTHROPIC_API_KEY: agent offline fallback] Covenant routes, signs, and audits each task; run it locally to plug in your own agent."; + const r = await fetch("https://api.anthropic.com/v1/messages", { + method: "POST", + headers: { "content-type": "application/json", "x-api-key": ANTHROPIC_KEY, "anthropic-version": "2023-06-01" }, + body: JSON.stringify({ model: MODEL, max_tokens: 200, system: SYSTEM_PROMPT, messages: [{ role: "user", content: intent }] }), + }); + if (!r.ok) throw new Error(`anthropic ${r.status}: ${(await r.text()).slice(0, 120)}`); + return (await r.json()).content.map((c) => c.text).join("").trim(); +} + +async function erConnection() { + if (!IS_TEE) return new Connection(ER_URL, "confirmed"); + const token = await getAuthToken(ER_URL, owner.publicKey, async (msg) => new Uint8Array(crypto.sign(null, Buffer.from(msg), + crypto.createPrivateKey({ key: Buffer.concat([Buffer.from("302e020100300506032b657004220420", "hex"), Buffer.from(owner.secretKey.slice(0, 32))]), format: "der", type: "pkcs8" })))); + console.log("minted TEE auth token (self-serve)"); + return new Connection(`${ER_URL}?token=${token}`, "confirmed"); +} + +console.log(`operator ${owner.publicKey.toBase58()}`); +console.log(`credits ${credits.toBase58()} (our demo agent's metered account)`); +console.log(`ER ${ER_URL} validator ${VALIDATOR.toBase58()}`); +console.log(`model ${MODEL} | work items ${INTENTS.length} | key ${ANTHROPIC_KEY ? "present" : "absent (fallback)"}\n`); + +const rootBefore = await provRoot(l1); +if (!rootBefore) { console.log("credit account missing or not migrated"); process.exit(1); } +console.log(`provenance_root before: ${rootBefore.toString("hex")}\n`); + +console.log("agent doing real work:"); +const work = []; +for (const intent of INTENTS) { + const reply = await agentWork(intent); + const item = canon({ model: MODEL, intent, reply }); + const receipt = sha256(Buffer.from(item)); + work.push({ intent, reply, receipt }); + console.log(` Q: ${intent}\n A: ${reply.replace(/\s+/g, " ").slice(0, 100)}${reply.length > 100 ? "..." : ""}\n receipt ${receipt.toString("hex").slice(0, 16)}...`); +} + +const er = await erConnection(); + +{ + const ix = new TransactionInstruction({ + programId: PROG, + keys: [ + m(owner.publicKey, true, true), + m(delegateBufferPdaFromDelegatedAccountAndOwnerProgram(credits, PROG), false, true), + m(delegationRecordPdaFromDelegatedAccount(credits), false, true), + m(delegationMetadataPdaFromDelegatedAccount(credits), false, true), + m(credits, false, true), m(PROG, false, false), + m(DELEGATION_PROGRAM_ID, false, false), m(SystemProgram.programId, false, false), + m(VALIDATOR, false, false), + ], + data: disc("delegate_credits"), + }); + console.log(`\ndelegated ${await send(l1, ix)}`); +} + +let ready = false; +for (let i = 0; i < 30; i++) { + await sleep(1000); + const ai = await er.getAccountInfo(credits).catch(() => null); + if (ai) { ready = true; console.log(`ER ready ~${i + 1}s\n`); break; } +} +if (!ready) { console.log("ER never picked up; try the open validator (MAS1)"); process.exit(1); } + +const meterSigs = []; +console.log("metering each answer in the ER (gasless):"); +for (const w of work) { + const data = Buffer.alloc(48); + disc("consume_credits").copy(data, 0); + data.writeBigUInt64LE(1n, 8); + w.receipt.copy(data, 16); + const ix = new TransactionInstruction({ programId: PROG, keys: [m(config, false, false), m(credits, false, true), m(owner.publicKey, true, false)], data }); + const sig = await send(er, ix, { skipPreflight: true }); + meterSigs.push(sig); + console.log(` metered ${w.receipt.toString("hex").slice(0, 16)}... ${sig.slice(0, 16)}...`); +} +console.log(`\nER provenance_root: ${(await provRoot(er))?.toString("hex")}`); + +const undelSig = await send(er, new TransactionInstruction({ programId: PROG, keys: [m(owner.publicKey, true, true), m(credits, false, true), m(MAGIC_PROGRAM_ID, false, false), m(MAGIC_CONTEXT_ID, false, true)], data: disc("undelegate_credits") })); +console.log(`undelegated ${undelSig} (commits provenance to L1)`); + +let rootAfter = null; +const expected = work.reduce((root, w) => sha256(root, w.receipt), rootBefore); +for (let i = 0; i < 30; i++) { await sleep(1000); rootAfter = await provRoot(l1); if (rootAfter && rootAfter.equals(expected)) break; } + +const matched = rootAfter && rootAfter.equals(expected); +console.log("\n================ REFERENCE RUN ================"); +console.log(`agent our Covenant demo agent (${MODEL})`); +console.log(`work items ${work.length} real answers, metered gaslessly in the ER`); +console.log(`ER / validator ${ER_URL} / ${VALIDATOR.toBase58()}`); +console.log(`root before ${rootBefore.toString("hex")}`); +console.log(`root on L1 after ${rootAfter?.toString("hex")}`); +console.log(`recomputed root ${expected.toString("hex")}`); +console.log(`provenance check ${matched ? "MATCH — on-chain root IS the hash-chain of the agent's real work ✓" : "MISMATCH ✗"}`); +console.log(`undelegate sig ${undelSig}`); + +fs.writeFileSync(new URL("./reference-run-result.json", import.meta.url), JSON.stringify({ + ts: new Date().toISOString(), program: PROG.toBase58(), operator: owner.publicKey.toBase58(), + credits: credits.toBase58(), er: ER_URL, validator: VALIDATOR.toBase58(), model: MODEL, + rootBefore: rootBefore.toString("hex"), rootAfterL1: rootAfter?.toString("hex"), + recomputed: expected.toString("hex"), provenanceVerified: matched, + work: work.map((w) => ({ intent: w.intent, reply: w.reply, receipt: w.receipt.toString("hex") })), + sigs: { meter: meterSigs, undelegate: undelSig }, +}, null, 2)); +console.log(`\nwrote reference-run-result.json (${matched ? "VERIFIED" : "CHECK"})`); diff --git a/agent-os/programs/settlement-ephemeral/spike/remote-check.mjs b/agent-os/programs/settlement-ephemeral/spike/remote-check.mjs new file mode 100644 index 000000000..65d2114d5 --- /dev/null +++ b/agent-os/programs/settlement-ephemeral/spike/remote-check.mjs @@ -0,0 +1,52 @@ +// Pay a remote (deployed) x402 ER facilitator end to end: GET /paid -> 402 -> +// settle by consume_credits in the ER (receipt_hash = sha256(nonce string), matching +// the Rust facilitator/EphemeralSigner) -> retry with x-payment -> expect 200. +// The credit account must already be delegated. Env: URL (the facilitator /paid). + +import fs from "node:fs"; +import os from "node:os"; +import crypto from "node:crypto"; +import { Connection, Keypair, PublicKey, Transaction, TransactionInstruction, sendAndConfirmTransaction } from "@solana/web3.js"; + +const PROG = new PublicKey("cov9UDypG7nsryxdgMcKhKU2spRVWLVjxT2iTv6do5Y"); +const URL = process.env.URL || "https://covenant-x402-er-facilitator.onrender.com/paid"; +const ER = process.env.ER || "https://devnet-eu.magicblock.app"; +const payer = Keypair.fromSecretKey(new Uint8Array(JSON.parse( + fs.readFileSync(process.env.PAYER || `${os.homedir()}/.config/solana/id.json`, "utf8")))); +const owner = payer.publicKey; +const er = new Connection(ER, "confirmed"); +const pda = (s) => PublicKey.findProgramAddressSync(s, PROG)[0]; +const config = pda([Buffer.from("config")]); +const credits = pda([Buffer.from("credits"), owner.toBuffer()]); +const disc = (n) => crypto.createHash("sha256").update(`global:${n}`).digest().subarray(0, 8); +const le64 = (v) => { const b = Buffer.alloc(8); b.writeBigUInt64LE(BigInt(v)); return b; }; +const m = (pubkey, s, w) => ({ pubkey, isSigner: s, isWritable: w }); + +async function main() { + console.log(`calling deployed facilitator: ${URL}`); + const r1 = await fetch(URL); + if (r1.status !== 402) throw new Error(`expected 402, got ${r1.status}`); + const arr = await r1.json(); + const ch = Array.isArray(arr) ? arr[0] : arr; + const nonce = ch.extra.nonce; + const amount = BigInt(ch.amount); + console.log(` 402 challenge: amount ${amount} ${ch.asset}, nonce ${nonce}`); + + // receipt_hash = sha256(nonce string bytes) -- matches the Rust facilitator. + const receipt = crypto.createHash("sha256").update(nonce, "utf8").digest(); + const ix = new TransactionInstruction({ + programId: PROG, + keys: [m(config, false, false), m(credits, false, true), m(owner, true, false)], + data: Buffer.concat([disc("consume_credits"), le64(amount), receipt]), + }); + const sig = await sendAndConfirmTransaction(er, new Transaction().add(ix), [payer], { commitment: "confirmed" }); + console.log(` paid in ER: ${sig}`); + + const header = Buffer.from(JSON.stringify({ x402Version: 1, scheme: "exact-er", payload: { signature: sig, nonce } })).toString("base64"); + const r2 = await fetch(URL, { headers: { "x-payment": header } }); + const body = await r2.text(); + console.log(` retry with x-payment -> ${r2.status}: ${body}`); + if (r2.status !== 200) throw new Error(`paid retry got ${r2.status}`); + console.log("\nOK — deployed facilitator verified a live ER-settled payment."); +} +main().catch((e) => { console.error("FAILED:", e.message || e); process.exit(1); }); diff --git a/agent-os/programs/settlement-ephemeral/spike/run-spike.mjs b/agent-os/programs/settlement-ephemeral/spike/run-spike.mjs new file mode 100644 index 000000000..a600ad0b0 --- /dev/null +++ b/agent-os/programs/settlement-ephemeral/spike/run-spike.mjs @@ -0,0 +1,128 @@ +// Live devnet spike for the Covenant settlement ER build. +// +// Uses the already-initialized cov9UDyp devnet deployment (config + a credit +// account with a positive balance), so no token/mint setup is needed. Flow: +// delegate_credits (L1) -> N x consume_credits (ER) -> undelegate_credits (ER) +// -> read balance on L1 and assert exact reconciliation. +// +// Instructions are encoded by hand (anchor global discriminator = sha256("global:")[:8]) +// with the exact account order the #[delegate]/#[commit] macros generate. ER txs go +// straight to the pinned validator endpoint; L1 txs to devnet. +// +// Env: PAYER (keypair json, default id.json), N (default 50), AMOUNT (default 1), +// ER (validator RPC, default EU), VALIDATOR (pinned pubkey, default EU). + +import fs from "node:fs"; +import os from "node:os"; +import crypto from "node:crypto"; +import { + Connection, Keypair, PublicKey, SystemProgram, + Transaction, TransactionInstruction, sendAndConfirmTransaction, +} from "@solana/web3.js"; +import { DELEGATION_PROGRAM_ID, MAGIC_PROGRAM_ID, MAGIC_CONTEXT_ID, + delegateBufferPdaFromDelegatedAccountAndOwnerProgram, + delegationRecordPdaFromDelegatedAccount, + delegationMetadataPdaFromDelegatedAccount } from "@magicblock-labs/ephemeral-rollups-sdk"; + +const PROG = new PublicKey("cov9UDypG7nsryxdgMcKhKU2spRVWLVjxT2iTv6do5Y"); +const pk = (x) => (x instanceof PublicKey ? x : new PublicKey(x)); +const env = (k, d) => process.env[k] ?? d; +const N = Number(env("N", "50")); +const AMOUNT = BigInt(env("AMOUNT", "1")); +const ER_URL = env("ER", "https://devnet-eu.magicblock.app"); +const VALIDATOR = new PublicKey(env("VALIDATOR", "MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e")); + +const payer = Keypair.fromSecretKey(new Uint8Array(JSON.parse( + fs.readFileSync(env("PAYER", `${os.homedir()}/.config/solana/id.json`), "utf8")))); +const owner = payer.publicKey; + +const l1 = new Connection("https://api.devnet.solana.com", "confirmed"); +const er = new Connection(ER_URL, "confirmed"); + +const pda = (seeds) => PublicKey.findProgramAddressSync(seeds, PROG)[0]; +const config = pda([Buffer.from("config")]); +const credits = pda([Buffer.from("credits"), owner.toBuffer()]); +const disc = (name) => crypto.createHash("sha256").update(`global:${name}`).digest().subarray(0, 8); +const m = (pubkey, isSigner, isWritable) => ({ pubkey: pk(pubkey), isSigner, isWritable }); + +async function balanceL1() { + const ai = await l1.getAccountInfo(credits); + return ai ? ai.data.readBigUInt64LE(40) : null; +} +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +async function send(conn, ix, label) { + const tx = new Transaction().add(ix); + const sig = await sendAndConfirmTransaction(conn, tx, [payer], { commitment: "confirmed", skipPreflight: false }); + console.log(` ${label}: ${sig}`); + return sig; +} + +async function main() { + console.log(`program ${PROG} | credits ${credits}`); + const before = await balanceL1(); + console.log(`starting balance (L1): ${before}`); + if (before === null) throw new Error("credit account missing"); + if (before < AMOUNT * BigInt(N)) throw new Error(`need >= ${AMOUNT * BigInt(N)} credits`); + + // 1) delegate on L1. Account order matches the #[delegate] macro expansion. + console.log(`\n[1] delegate_credits (L1), pinning validator ${VALIDATOR}`); + const delIx = new TransactionInstruction({ + programId: PROG, + keys: [ + m(owner, true, true), // payer + m(delegateBufferPdaFromDelegatedAccountAndOwnerProgram(credits, PROG), false, true), // buffer_pda + m(delegationRecordPdaFromDelegatedAccount(credits), false, true), // delegation_record + m(delegationMetadataPdaFromDelegatedAccount(credits), false, true), // delegation_metadata + m(credits, false, true), // pda (credits) + m(PROG, false, false), // owner_program + m(DELEGATION_PROGRAM_ID, false, false), // delegation_program + m(SystemProgram.programId, false, false), // system_program + m(VALIDATOR, false, false), // remaining[0]: pinned validator + ], + data: disc("delegate_credits"), + }); + await send(l1, delIx, "delegate"); + console.log(" waiting for the ER to pick up the delegated account..."); + await sleep(6000); + + // 2) N x consume_credits in the ER. + console.log(`\n[2] ${N} x consume_credits in the ER (${ER_URL})`); + const receipt = Buffer.alloc(32); + const consumeData = (amount) => Buffer.concat([disc("consume_credits"), le64(amount), receipt]); + const consumeKeys = [m(config, false, false), m(credits, false, true), m(owner, true, false)]; + const t0 = Date.now(); + for (let i = 0; i < N; i++) { + const ix = new TransactionInstruction({ programId: PROG, keys: consumeKeys, data: consumeData(AMOUNT) }); + await send(er, ix, `consume ${i + 1}/${N}`); + } + const ms = Date.now() - t0; + console.log(` ${N} consume in ER: ${ms} ms total, ${(ms / N).toFixed(1)} ms/op`); + + // 3) undelegate (commit final + return to L1). + console.log(`\n[3] undelegate_credits (ER)`); + const undelIx = new TransactionInstruction({ + programId: PROG, + keys: [ + m(owner, true, true), // owner + m(credits, false, true), // credits + m(MAGIC_PROGRAM_ID, false, false), // magic_program + m(MAGIC_CONTEXT_ID, false, true), // magic_context + ], + data: disc("undelegate_credits"), + }); + await send(er, undelIx, "undelegate"); + console.log(" waiting for L1 finalization of the commit+undelegate..."); + await sleep(12000); + + // 4) reconcile on L1. + console.log(`\n[4] reconcile on L1`); + const after = await balanceL1(); + const expected = before - AMOUNT * BigInt(N); + console.log(` before ${before} | after ${after} | expected ${expected}`); + if (after !== expected) throw new Error(`RECONCILE FAILED: ${after} != ${expected}`); + console.log(`\nRECONCILE OK — ${N} ER-metered consumes reconciled exactly to L1.`); +} + +function le64(v) { const b = Buffer.alloc(8); b.writeBigUInt64LE(BigInt(v)); return b; } +main().catch((e) => { console.error("\nSPIKE FAILED:", e.message || e); process.exit(1); }); diff --git a/agent-os/programs/settlement-ephemeral/spike/x402-demo.mjs b/agent-os/programs/settlement-ephemeral/spike/x402-demo.mjs new file mode 100644 index 000000000..42330693f --- /dev/null +++ b/agent-os/programs/settlement-ephemeral/spike/x402-demo.mjs @@ -0,0 +1,186 @@ +// x402-over-ER demo: a paid HTTP endpoint settled by gasless credit metering in a +// MagicBlock ephemeral rollup, end to end and live on devnet. +// +// - facilitator (HTTP): GET /quote returns 402 + a challenge {amount, nonce} until +// a valid x-payment header is presented; then 200 + the content. +// - agent (client): on 402, settles by running consume_credits in the ER with +// receipt_hash = sha256(nonce) (binds the payment to this request), then retries +// with an x-payment envelope carrying the ER tx signature. +// - the facilitator verifies the signature ON THE ER: right program, right credit +// account, amount >= price, receipt_hash == sha256(nonce), and the signature is +// unused (anti-replay). No SPL transfer, no per-call L1 fee. +// +// Session model: delegate the credit account once, serve K paid calls each settled +// gaslessly in the ER, undelegate, reconcile on L1. +// +// Env: PAYER (keypair, default id.json), K (paid calls, default 5), PRICE (credits/call, +// default 1), VALIDATOR (pinned ER validator, default EU), ER (validator RPC). + +import fs from "node:fs"; +import os from "node:os"; +import http from "node:http"; +import crypto from "node:crypto"; +import { + Connection, Keypair, PublicKey, SystemProgram, + Transaction, TransactionInstruction, sendAndConfirmTransaction, +} from "@solana/web3.js"; +import bs58 from "bs58"; +import { DELEGATION_PROGRAM_ID, MAGIC_PROGRAM_ID, MAGIC_CONTEXT_ID, + delegateBufferPdaFromDelegatedAccountAndOwnerProgram, + delegationRecordPdaFromDelegatedAccount, + delegationMetadataPdaFromDelegatedAccount } from "@magicblock-labs/ephemeral-rollups-sdk"; + +const PROG = new PublicKey("cov9UDypG7nsryxdgMcKhKU2spRVWLVjxT2iTv6do5Y"); +const env = (k, d) => process.env[k] ?? d; +const K = Number(env("K", "5")); +const PRICE = BigInt(env("PRICE", "1")); +const ER_URL = env("ER", "https://devnet-eu.magicblock.app"); +const VALIDATOR = new PublicKey(env("VALIDATOR", "MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e")); +const PORT = 8402; + +const payer = Keypair.fromSecretKey(new Uint8Array(JSON.parse( + fs.readFileSync(env("PAYER", `${os.homedir()}/.config/solana/id.json`), "utf8")))); +const owner = payer.publicKey; +const l1 = new Connection("https://api.devnet.solana.com", "confirmed"); +const er = new Connection(ER_URL, "confirmed"); +const pda = (s) => PublicKey.findProgramAddressSync(s, PROG)[0]; +const config = pda([Buffer.from("config")]); +const credits = pda([Buffer.from("credits"), owner.toBuffer()]); +const disc = (n) => crypto.createHash("sha256").update(`global:${n}`).digest().subarray(0, 8); +const sha = (b) => crypto.createHash("sha256").update(b).digest(); +const m = (pubkey, isSigner, isWritable) => ({ pubkey, isSigner, isWritable }); +const le64 = (v) => { const b = Buffer.alloc(8); b.writeBigUInt64LE(BigInt(v)); return b; }; +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +const CONSUME_DISC = disc("consume_credits"); + +async function erBalance() { + const ai = await er.getAccountInfo(credits); + return ai ? ai.data.readBigUInt64LE(40) : null; +} + +// ---- facilitator: verifies an ER consume_credits as proof of payment ---- +const usedSigs = new Set(); +async function verifyPayment({ signature, nonce }) { + if (usedSigs.has(signature)) return { ok: false, why: "signature replayed" }; + const tx = await er.getTransaction(signature, { commitment: "confirmed", maxSupportedTransactionVersion: 0 }); + if (!tx) return { ok: false, why: "tx not found on ER" }; + if (tx.meta?.err) return { ok: false, why: "tx failed" }; + const msg = tx.transaction.message; + const keys = msg.staticAccountKeys ?? msg.accountKeys; + const ixs = msg.compiledInstructions ?? msg.instructions; + for (const ix of ixs) { + if (!keys[ix.programIdIndex].equals(PROG)) continue; + const data = Buffer.from(typeof ix.data === "string" ? bs58.decode(ix.data) : ix.data); + if (!data.subarray(0, 8).equals(CONSUME_DISC)) continue; + const amount = data.readBigUInt64LE(8); + const receipt = data.subarray(16, 48); + const acctIdx = ix.accountKeyIndexes ?? ix.accounts; // [config, credits, owner] + const creditAcct = keys[acctIdx[1]]; + if (!creditAcct.equals(credits)) return { ok: false, why: "wrong credit account" }; + if (amount < PRICE) return { ok: false, why: `underpaid ${amount} < ${PRICE}` }; + if (!receipt.equals(sha(Buffer.from(nonce, "hex")))) return { ok: false, why: "receipt_hash != sha256(nonce)" }; + usedSigs.add(signature); + return { ok: true, amount }; + } + return { ok: false, why: "no consume_credits to our program in tx" }; +} + +function startFacilitator() { + return new Promise((resolve) => { + const server = http.createServer(async (req, res) => { + const hdr = req.headers["x-payment"]; + const send = (code, obj, extra = {}) => + res.writeHead(code, { "content-type": "application/json", ...extra }).end(JSON.stringify(obj)); + if (!hdr) { + const nonce = crypto.randomBytes(16).toString("hex"); + return send(402, { + x402Version: 1, scheme: "exact-er", network: "solana-er:devnet", + program: PROG.toBase58(), creditAccount: credits.toBase58(), + amountCredits: PRICE.toString(), nonce, + }); + } + let proof; + try { proof = JSON.parse(Buffer.from(hdr, "base64").toString()); } + catch { return send(400, { error: "bad x-payment" }); } + const v = await verifyPayment(proof); + if (!v.ok) return send(402, { error: "payment invalid", why: v.why }); + send(200, { quote: "BTC looks coiled. Position for a breakout.", paidCredits: v.amount.toString() }); + }); + server.listen(PORT, () => resolve(server)); + }); +} + +// ---- agent: pays per call by metering in the ER ---- +async function paidGet(url) { + const r1 = await fetch(url); + if (r1.status !== 402) throw new Error(`expected 402, got ${r1.status}`); + const ch = await r1.json(); + const receipt = sha(Buffer.from(ch.nonce, "hex")); + const ix = new TransactionInstruction({ + programId: PROG, + keys: [m(config, false, false), m(credits, false, true), m(owner, true, false)], + data: Buffer.concat([CONSUME_DISC, le64(BigInt(ch.amountCredits)), receipt]), + }); + const sig = await sendAndConfirmTransaction(er, new Transaction().add(ix), [payer], { commitment: "confirmed" }); + const header = Buffer.from(JSON.stringify({ signature: sig, nonce: ch.nonce })).toString("base64"); + const r2 = await fetch(url, { headers: { "x-payment": header } }); + if (r2.status !== 200) throw new Error(`paid retry got ${r2.status}: ${await r2.text()}`); + return { body: await r2.json(), sig }; +} + +async function send(conn, ix, label) { + const sig = await sendAndConfirmTransaction(conn, new Transaction().add(ix), [payer], { commitment: "confirmed" }); + console.log(` ${label}: ${sig}`); +} + +async function main() { + const server = await startFacilitator(); + const url = `http://127.0.0.1:${PORT}/quote`; + console.log(`facilitator up on ${url} | program ${PROG} | credits ${credits}`); + + console.log(`\n[1] delegate credit account to ER (pin ${VALIDATOR})`); + await send(l1, new TransactionInstruction({ + programId: PROG, + keys: [ + m(owner, true, true), + m(delegateBufferPdaFromDelegatedAccountAndOwnerProgram(credits, PROG), false, true), + m(delegationRecordPdaFromDelegatedAccount(credits), false, true), + m(delegationMetadataPdaFromDelegatedAccount(credits), false, true), + m(credits, false, true), m(PROG, false, false), + m(DELEGATION_PROGRAM_ID, false, false), m(SystemProgram.programId, false, false), + m(VALIDATOR, false, false), + ], + data: disc("delegate_credits"), + }), "delegate"); + await sleep(6000); + + const before = await erBalance(); + console.log(`\n[2] ${K} paid API calls, each settled gasless in the ER (ER balance ${before})`); + const lat = []; + for (let i = 0; i < K; i++) { + const t = Date.now(); + const { body, sig } = await paidGet(url); + lat.push(Date.now() - t); + console.log(` call ${i + 1}/${K}: 402 -> ER pay ${sig.slice(0, 8)}.. -> 200 "${body.quote.slice(0, 28)}.." (${lat[i]}ms)`); + } + const afterEr = await erBalance(); + const avg = (lat.reduce((a, b) => a + b, 0) / K).toFixed(0); + console.log(` paid calls: ${K}, ER balance ${before} -> ${afterEr} (-${before - afterEr}), ${avg}ms/call end-to-end`); + + console.log(`\n[3] undelegate + reconcile on L1`); + await send(er, new TransactionInstruction({ + programId: PROG, + keys: [m(owner, true, true), m(credits, false, true), m(MAGIC_PROGRAM_ID, false, false), m(MAGIC_CONTEXT_ID, false, true)], + data: disc("undelegate_credits"), + }), "undelegate"); + await sleep(12000); + const l1ai = await l1.getAccountInfo(credits); + const l1bal = l1ai.data.readBigUInt64LE(40); + const expected = before - PRICE * BigInt(K); + console.log(` L1 balance ${l1bal} | expected ${expected}`); + server.close(); + if (l1bal !== expected) throw new Error(`RECONCILE FAILED: ${l1bal} != ${expected}`); + console.log(`\nOK — ${K} x402 calls settled gaslessly in the ER and reconciled exactly to L1.`); +} + +main().catch((e) => { console.error("\nDEMO FAILED:", e.message || e); process.exit(1); }); diff --git a/agent-os/programs/settlement-ephemeral/src/_shared.rs b/agent-os/programs/settlement-ephemeral/src/_shared.rs new file mode 100644 index 000000000..d8e3b3e52 --- /dev/null +++ b/agent-os/programs/settlement-ephemeral/src/_shared.rs @@ -0,0 +1,1752 @@ +// Covenant Solana protocol program. +// +// `$COVNT` is an external SPL mint. The program never mints it; protocol +// utility comes from staking, escrowing, burning, and metering it into +// non-transferable credits. +// +// `allow(deprecated)` / `allow(unexpected_cfgs)` live in `[lints]` (Cargo.toml) +// rather than as crate inner attributes, so this file can be `include!`d verbatim +// by the isolated ER crate (settlement-ephemeral). Inner attributes break include!. + +use anchor_lang::prelude::*; +use anchor_spl::token_interface::{ + self, Burn, Mint, TokenAccount, TokenInterface, TransferChecked, +}; + +#[cfg(feature = "ephemeral")] +use ephemeral_rollups_sdk::anchor::{commit, delegate, ephemeral}; +#[cfg(feature = "ephemeral")] +use ephemeral_rollups_sdk::cpi::DelegateConfig; +#[cfg(feature = "ephemeral")] +use ephemeral_rollups_sdk::ephem::MagicIntentBundleBuilder; + +declare_id!("cov9UDypG7nsryxdgMcKhKU2spRVWLVjxT2iTv6do5Y"); + +#[cfg(not(feature = "no-entrypoint"))] +solana_security_txt::security_txt! { + name: "Covenant Settlement", + project_url: "https://opencovenant.org", + contacts: "email:security@opencovenant.org", + policy: "https://github.com/open-covenant/covenant/blob/main/SECURITY.md", + preferred_languages: "en", + source_code: "https://github.com/open-covenant/covenant", + source_release: "v0.1.0-alpha.1", + auditors: "None" +} + +#[cfg_attr(feature = "ephemeral", ephemeral)] +#[program] +pub mod settlement { + use super::*; + + pub fn initialize(ctx: Context, args: InitializeArgs) -> Result<()> { + require!(args.credits_per_covnt > 0, CovenantError::ZeroAmount); + + let config = &mut ctx.accounts.config; + config.authority = ctx.accounts.authority.key(); + config.slash_authority = args.slash_authority; + config.covnt_mint = ctx.accounts.covnt_mint.key(); + config.treasury = ctx.accounts.treasury.key(); + config.credits_per_covnt = args.credits_per_covnt; + config.paused = false; + config.bump = ctx.bumps.config; + config.min_stake_lock = args.min_stake_lock; + + emit!(ProtocolInitialized { + authority: config.authority, + slash_authority: config.slash_authority, + covnt_mint: config.covnt_mint, + treasury: config.treasury, + credits_per_covnt: config.credits_per_covnt, + }); + Ok(()) + } + + pub fn set_pause(ctx: Context, paused: bool) -> Result<()> { + ctx.accounts.config.paused = paused; + emit!(ProtocolPauseUpdated { paused }); + Ok(()) + } + + pub fn register_agent(ctx: Context, args: RegisterAgentArgs) -> Result<()> { + require!(!ctx.accounts.config.paused, CovenantError::ProtocolPaused); + + let agent = &mut ctx.accounts.agent; + agent.agent_key = args.agent_key; + agent.operator = ctx.accounts.operator.key(); + agent.metadata_hash = args.metadata_hash; + agent.capability_hash = args.capability_hash; + agent.stake = 0; + agent.reputation = 0; + agent.active = true; + agent.bump = ctx.bumps.agent; + + emit!(AgentRegistered { + agent_key: agent.agent_key, + operator: agent.operator, + metadata_hash: agent.metadata_hash, + capability_hash: agent.capability_hash, + }); + Ok(()) + } + + pub fn set_agent_active(ctx: Context, active: bool) -> Result<()> { + ctx.accounts.agent.active = active; + emit!(AgentStatusUpdated { + agent_key: ctx.accounts.agent.agent_key, + active, + }); + Ok(()) + } + + pub fn open_credit_account(ctx: Context) -> Result<()> { + let credits = &mut ctx.accounts.credits; + credits.owner = ctx.accounts.owner.key(); + credits.balance = 0; + credits.bump = ctx.bumps.credits; + credits.provenance_root = [0u8; 32]; + + emit!(CreditAccountOpened { + owner: credits.owner, + credit_account: credits.key(), + }); + Ok(()) + } + + pub fn buy_credits(ctx: Context, amount_covnt: u64) -> Result<()> { + require!(!ctx.accounts.config.paused, CovenantError::ProtocolPaused); + require!(amount_covnt > 0, CovenantError::ZeroAmount); + + token_interface::transfer_checked( + ctx.accounts.buy_transfer_ctx(), + amount_covnt, + ctx.accounts.covnt_mint.decimals, + )?; + + let credits = amount_covnt + .checked_mul(ctx.accounts.config.credits_per_covnt) + .ok_or(CovenantError::Overflow)?; + ctx.accounts.credits.balance = ctx + .accounts + .credits + .balance + .checked_add(credits) + .ok_or(CovenantError::Overflow)?; + + emit!(CreditsPurchased { + owner: ctx.accounts.owner.key(), + amount_covnt, + credits, + }); + Ok(()) + } + + pub fn consume_credits( + ctx: Context, + amount: u64, + receipt_hash: [u8; 32], + ) -> Result<()> { + require!(!ctx.accounts.config.paused, CovenantError::ProtocolPaused); + require!(amount > 0, CovenantError::ZeroAmount); + require!( + ctx.accounts.credits.balance >= amount, + CovenantError::InsufficientCredits + ); + + ctx.accounts.credits.balance -= amount; + + // Fold the receipt into the account's provenance hash-chain. This runs in + // the ER per consume and commits to L1 with the balance, making the root a + // real-time, on-chain record of every metered action. + let provenance_root = anchor_lang::solana_program::hash::hashv(&[ + &ctx.accounts.credits.provenance_root, + &receipt_hash, + ]) + .to_bytes(); + ctx.accounts.credits.provenance_root = provenance_root; + + emit!(CreditsConsumed { + owner: ctx.accounts.owner.key(), + amount, + receipt_hash, + provenance_root, + }); + Ok(()) + } + + pub fn stake(ctx: Context, amount: u64, lock_until: u64) -> Result<()> { + require!(!ctx.accounts.config.paused, CovenantError::ProtocolPaused); + require!(amount > 0, CovenantError::ZeroAmount); + require!(ctx.accounts.agent.active, CovenantError::AgentInactive); + + let min_lock = ctx.accounts.config.min_stake_lock; + if min_lock > 0 { + let now = Clock::get()?.unix_timestamp.max(0) as u64; + let min_unlock = now.checked_add(min_lock).ok_or(CovenantError::Overflow)?; + require!(lock_until >= min_unlock, CovenantError::LockTooShort); + } + + token_interface::transfer_checked( + ctx.accounts.stake_transfer_ctx(), + amount, + ctx.accounts.covnt_mint.decimals, + )?; + + let position = &mut ctx.accounts.position; + position.agent_key = ctx.accounts.agent.agent_key; + position.owner = ctx.accounts.owner.key(); + position.amount = amount; + position.vault = ctx.accounts.stake_vault.key(); + position.lock_until = lock_until; + position.active = true; + position.bump = ctx.bumps.position; + + ctx.accounts.agent.stake = ctx + .accounts + .agent + .stake + .checked_add(amount) + .ok_or(CovenantError::Overflow)?; + + emit!(StakeOpened { + agent_key: position.agent_key, + owner: position.owner, + amount, + lock_until, + position: position.key(), + }); + Ok(()) + } + + /// Owner-signed withdrawal of a staked position once `lock_until` + /// has passed. Transfers the full position balance back to the + /// owner's COVNT account, decrements `agent.stake`, and closes the + /// position account (rent returned to the owner). Closing frees the + /// canonical `[b"stake", agent_key, owner]` PDA so the owner can + /// re-stake against the same agent afterwards. + pub fn unstake(ctx: Context) -> Result<()> { + require!(!ctx.accounts.config.paused, CovenantError::ProtocolPaused); + require!(ctx.accounts.position.active, CovenantError::StakeInactive); + require!(ctx.accounts.position.amount > 0, CovenantError::ZeroAmount); + let now = Clock::get()?.unix_timestamp.max(0) as u64; + require!( + now >= ctx.accounts.position.lock_until, + CovenantError::StakeLocked + ); + + let amount = ctx.accounts.position.amount; + let agent_key = ctx.accounts.position.agent_key; + let owner = ctx.accounts.position.owner; + let signer_seeds: &[&[u8]] = &[ + b"stake", + agent_key.as_ref(), + owner.as_ref(), + &[ctx.accounts.position.bump], + ]; + token_interface::transfer_checked( + ctx.accounts + .unstake_transfer_ctx() + .with_signer(&[signer_seeds]), + amount, + ctx.accounts.covnt_mint.decimals, + )?; + + ctx.accounts.agent.stake = ctx + .accounts + .agent + .stake + .checked_sub(amount) + .ok_or(CovenantError::InsufficientStake)?; + + emit!(StakeWithdrawn { + agent_key, + owner, + amount, + withdrawn_at: now, + }); + Ok(()) + } + + pub fn slash_stake(ctx: Context, amount: u64, reason_hash: [u8; 32]) -> Result<()> { + require!(!ctx.accounts.config.paused, CovenantError::ProtocolPaused); + require!(amount > 0, CovenantError::ZeroAmount); + require!(ctx.accounts.position.active, CovenantError::StakeInactive); + require!( + ctx.accounts.position.amount >= amount, + CovenantError::InsufficientStake + ); + + let agent_key = ctx.accounts.position.agent_key; + let owner = ctx.accounts.position.owner; + let signer_seeds: &[&[u8]] = &[ + b"stake", + agent_key.as_ref(), + owner.as_ref(), + &[ctx.accounts.position.bump], + ]; + token_interface::transfer_checked( + ctx.accounts + .slash_transfer_ctx() + .with_signer(&[signer_seeds]), + amount, + ctx.accounts.covnt_mint.decimals, + )?; + + ctx.accounts.position.amount -= amount; + ctx.accounts.agent.stake = ctx + .accounts + .agent + .stake + .checked_sub(amount) + .ok_or(CovenantError::InsufficientStake)?; + if ctx.accounts.position.amount == 0 { + ctx.accounts.position.active = false; + } + + emit!(StakeSlashed { + agent_key, + owner, + amount, + reason_hash, + }); + Ok(()) + } + + /// Slash an agent's bond citing its on-chain actions. The reason is not + /// supplied by the caller: it is read from the `provenance_root` of the + /// operator's canonical credit account (`[b"credits", agent.operator]`), the + /// hash-chain `consume_credits` folds gaslessly in the ER and commits to L1. + /// So the reason is the operator's own committed record, not an arbitrary claim. + /// + /// Two limits the caller must know. The credit account must be undelegated: + /// `Account` cannot load while owned by the delegation program, + /// so an operator can defer this slash for up to the delegation timeout. And + /// the binding is to the operator's canonical credit account, not a per-agent + /// log, so it assumes the operator meters through that account. For a slash + /// that depends on neither, use `slash_stake`. + pub fn slash_for_actions(ctx: Context, amount: u64) -> Result<()> { + require!(!ctx.accounts.config.paused, CovenantError::ProtocolPaused); + require!(amount > 0, CovenantError::ZeroAmount); + require!(ctx.accounts.position.active, CovenantError::StakeInactive); + require!( + ctx.accounts.position.amount >= amount, + CovenantError::InsufficientStake + ); + + // A slash "for actions" requires recorded actions: refuse to cite a + // genesis (never-folded) provenance root. + let reason_hash = ctx.accounts.credits.provenance_root; + require!(reason_hash != [0u8; 32], CovenantError::NoRecordedActions); + let agent_key = ctx.accounts.position.agent_key; + let owner = ctx.accounts.position.owner; + let signer_seeds: &[&[u8]] = &[ + b"stake", + agent_key.as_ref(), + owner.as_ref(), + &[ctx.accounts.position.bump], + ]; + token_interface::transfer_checked( + ctx.accounts + .slash_transfer_ctx() + .with_signer(&[signer_seeds]), + amount, + ctx.accounts.covnt_mint.decimals, + )?; + + ctx.accounts.position.amount -= amount; + ctx.accounts.agent.stake = ctx + .accounts + .agent + .stake + .checked_sub(amount) + .ok_or(CovenantError::InsufficientStake)?; + if ctx.accounts.position.amount == 0 { + ctx.accounts.position.active = false; + } + + emit!(StakeSlashed { + agent_key, + owner, + amount, + reason_hash, + }); + Ok(()) + } + + pub fn create_task(ctx: Context, args: CreateTaskArgs) -> Result<()> { + require!(cfg!(feature = "task-escrow"), CovenantError::TasksDisabled); + require!(!ctx.accounts.config.paused, CovenantError::ProtocolPaused); + require!(args.amount_covnt > 0, CovenantError::ZeroAmount); + require!(ctx.accounts.agent.active, CovenantError::AgentInactive); + + token_interface::transfer_checked( + ctx.accounts.task_fund_ctx(), + args.amount_covnt, + ctx.accounts.covnt_mint.decimals, + )?; + + let task = &mut ctx.accounts.task; + task.task_id = args.task_id; + task.client = ctx.accounts.client.key(); + task.agent_key = ctx.accounts.agent.agent_key; + task.provider = args.provider; + task.amount_covnt = args.amount_covnt; + task.task_hash = args.task_hash; + task.criteria_hash = args.criteria_hash; + task.deadline = args.deadline; + task.status = TASK_FUNDED; + task.bump = ctx.bumps.task; + + emit!(TaskCreated { + task_id: task.task_id, + client: task.client, + agent_key: task.agent_key, + provider: task.provider, + amount_covnt: task.amount_covnt, + task_hash: task.task_hash, + criteria_hash: task.criteria_hash, + deadline: task.deadline, + }); + Ok(()) + } + + pub fn release_task( + ctx: Context, + result_hash: [u8; 32], + receipt_hash: [u8; 32], + ) -> Result<()> { + require!(cfg!(feature = "task-escrow"), CovenantError::TasksDisabled); + require!(!ctx.accounts.config.paused, CovenantError::ProtocolPaused); + require!( + ctx.accounts.task.status == TASK_FUNDED, + CovenantError::WrongTaskStatus + ); + let now = Clock::get()?.unix_timestamp; + require!( + now <= ctx.accounts.task.deadline, + CovenantError::TaskExpired + ); + + let task_id = ctx.accounts.task.task_id; + let signer_seeds: &[&[u8]] = &[b"task", task_id.as_ref(), &[ctx.accounts.task.bump]]; + token_interface::transfer_checked( + ctx.accounts.task_release_ctx().with_signer(&[signer_seeds]), + ctx.accounts.task.amount_covnt, + ctx.accounts.covnt_mint.decimals, + )?; + + ctx.accounts.task.status = TASK_RELEASED; + ctx.accounts.task.result_hash = result_hash; + + emit!(TaskReleased { + task_id, + provider: ctx.accounts.task.provider, + amount_covnt: ctx.accounts.task.amount_covnt, + result_hash, + receipt_hash, + }); + Ok(()) + } + + /// Refund the escrowed COVNT back to the client after the task + /// deadline has passed. Only the client signs; the provider has no + /// recourse here. Mirrors the escrow-agent norm where the funder + /// recovers their funds when the counterparty failed to deliver in + /// time. Pause check matches `release_task` so a paused protocol + /// halts all escrow movement uniformly. + pub fn refund_task(ctx: Context) -> Result<()> { + require!(cfg!(feature = "task-escrow"), CovenantError::TasksDisabled); + require!(!ctx.accounts.config.paused, CovenantError::ProtocolPaused); + require!( + ctx.accounts.task.status == TASK_FUNDED, + CovenantError::WrongTaskStatus + ); + let now = Clock::get()?.unix_timestamp; + require!( + now > ctx.accounts.task.deadline, + CovenantError::TaskNotExpired + ); + + let task_id = ctx.accounts.task.task_id; + let signer_seeds: &[&[u8]] = &[b"task", task_id.as_ref(), &[ctx.accounts.task.bump]]; + token_interface::transfer_checked( + ctx.accounts.task_refund_ctx().with_signer(&[signer_seeds]), + ctx.accounts.task.amount_covnt, + ctx.accounts.covnt_mint.decimals, + )?; + + ctx.accounts.task.status = TASK_REFUNDED; + + emit!(TaskRefunded { + task_id, + client: ctx.accounts.task.client, + amount_covnt: ctx.accounts.task.amount_covnt, + deadline: ctx.accounts.task.deadline, + refunded_at: now, + }); + Ok(()) + } + + pub fn burn_covnt(ctx: Context, amount: u64, reason_hash: [u8; 32]) -> Result<()> { + require!(!ctx.accounts.config.paused, CovenantError::ProtocolPaused); + require!(amount > 0, CovenantError::ZeroAmount); + + token_interface::burn(ctx.accounts.burn_ctx(), amount)?; + + emit!(CovntBurned { + owner: ctx.accounts.owner.key(), + amount, + reason_hash, + }); + Ok(()) + } + + pub fn anchor_receipt_batch( + ctx: Context, + args: AnchorReceiptBatchArgs, + ) -> Result<()> { + require!(!ctx.accounts.config.paused, CovenantError::ProtocolPaused); + require!(args.receipt_count > 0, CovenantError::ZeroAmount); + + let batch = &mut ctx.accounts.batch; + batch.batch_id = args.batch_id; + batch.authority = ctx.accounts.authority.key(); + batch.merkle_root = args.merkle_root; + batch.receipt_count = args.receipt_count; + batch.created_at = Clock::get()?.unix_timestamp; + batch.bump = ctx.bumps.batch; + + emit!(ReceiptBatchAnchored { + batch_id: batch.batch_id, + authority: batch.authority, + merkle_root: batch.merkle_root, + receipt_count: batch.receipt_count, + created_at: batch.created_at, + }); + Ok(()) + } + + /// Owner-signed reclaim of a spent position account (rent returned to + /// the owner). A position is spent once it is fully slashed + /// (`amount == 0`, `active == false`); the normal exit path closes the + /// position inside `unstake`. This exists so a fully-slashed owner can + /// reclaim rent and re-stake against the same agent. + pub fn close_position(ctx: Context) -> Result<()> { + require!( + !ctx.accounts.position.active, + CovenantError::StakeStillActive + ); + emit!(StakePositionClosed { + agent_key: ctx.accounts.position.agent_key, + owner: ctx.accounts.position.owner, + }); + Ok(()) + } + + pub fn update_authority(ctx: Context, new_authority: Pubkey) -> Result<()> { + let config = &mut ctx.accounts.config; + let previous = config.authority; + config.authority = new_authority; + emit!(AuthorityUpdated { + previous, + new_authority, + }); + Ok(()) + } + + pub fn update_slash_authority( + ctx: Context, + new_slash_authority: Pubkey, + ) -> Result<()> { + let config = &mut ctx.accounts.config; + let previous = config.slash_authority; + config.slash_authority = new_slash_authority; + emit!(SlashAuthorityUpdated { + previous, + new_slash_authority, + }); + Ok(()) + } + + pub fn update_treasury(ctx: Context) -> Result<()> { + let previous = ctx.accounts.config.treasury; + ctx.accounts.config.treasury = ctx.accounts.treasury.key(); + emit!(TreasuryUpdated { + previous, + new_treasury: ctx.accounts.treasury.key(), + }); + Ok(()) + } + + pub fn set_credits_per_covnt(ctx: Context, credits_per_covnt: u64) -> Result<()> { + require!(credits_per_covnt > 0, CovenantError::ZeroAmount); + let config = &mut ctx.accounts.config; + let previous = config.credits_per_covnt; + config.credits_per_covnt = credits_per_covnt; + emit!(CreditsRateUpdated { + previous, + credits_per_covnt, + }); + Ok(()) + } + + pub fn set_min_stake_lock(ctx: Context, min_stake_lock: u64) -> Result<()> { + let config = &mut ctx.accounts.config; + let previous = config.min_stake_lock; + config.min_stake_lock = min_stake_lock; + emit!(MinStakeLockUpdated { + previous, + min_stake_lock, + }); + Ok(()) + } + + /// One-time migration of a legacy `Config` (predates `min_stake_lock`) to + /// the current layout: grows the account by 8 bytes and writes the field. + /// Uses a raw account because the on-chain bytes cannot deserialize into + /// the new struct until the realloc completes. Authority is checked by + /// reading the on-chain `authority` field directly. Idempotent: re-running + /// on a current-layout config just rewrites the value. + pub fn migrate_config(ctx: Context, min_stake_lock: u64) -> Result<()> { + let info = ctx.accounts.config.to_account_info(); + { + let data = info.try_borrow_data()?; + require!(data.len() >= 40, CovenantError::Unauthorized); + let onchain_authority = + Pubkey::try_from(&data[8..40]).map_err(|_| error!(CovenantError::Unauthorized))?; + require_keys_eq!( + onchain_authority, + ctx.accounts.authority.key(), + CovenantError::Unauthorized + ); + } + + let new_len = 8 + Config::INIT_SPACE; + if info.data_len() < new_len { + let deficit = Rent::get()? + .minimum_balance(new_len) + .saturating_sub(info.lamports()); + if deficit > 0 { + anchor_lang::system_program::transfer( + CpiContext::new( + ctx.accounts.system_program.to_account_info(), + anchor_lang::system_program::Transfer { + from: ctx.accounts.authority.to_account_info(), + to: info.clone(), + }, + ), + deficit, + )?; + } + info.realloc(new_len, false)?; + } + + let mut data = info.try_borrow_mut_data()?; + let off = new_len - 8; + data[off..new_len].copy_from_slice(&min_stake_lock.to_le_bytes()); + + emit!(ConfigMigrated { min_stake_lock }); + Ok(()) + } + + /// One-time migration of a legacy `CreditAccount` (predates `provenance_root`) + /// to the current layout: grows the account by 32 bytes. `realloc` zero-fills + /// the new bytes, so the provenance root starts at genesis. Owner-gated by the + /// `[b"credits", owner]` seed binding; idempotent (a no-op realloc on an + /// already-current account). + pub fn migrate_credit_account(ctx: Context) -> Result<()> { + let info = ctx.accounts.credits.to_account_info(); + let new_len = 8 + CreditAccount::INIT_SPACE; + if info.data_len() < new_len { + let deficit = Rent::get()? + .minimum_balance(new_len) + .saturating_sub(info.lamports()); + if deficit > 0 { + anchor_lang::system_program::transfer( + CpiContext::new( + ctx.accounts.system_program.to_account_info(), + anchor_lang::system_program::Transfer { + from: ctx.accounts.owner.to_account_info(), + to: info.clone(), + }, + ), + deficit, + )?; + } + info.realloc(new_len, true)?; + } + Ok(()) + } + + /// Delegate the caller's credit account `[b"credits", owner]` to the + /// MagicBlock delegation program so metering (`consume_credits`) can run in + /// an ephemeral rollup. Only the program-owned accounting PDA moves; no token + /// custody is involved. Pass an ER validator pubkey as the first remaining + /// account to pin it (see `DelegateConfig.validator`). + #[cfg(feature = "ephemeral")] + pub fn delegate_credits(ctx: Context) -> Result<()> { + let owner = ctx.accounts.payer.key(); + let (expected, _) = Pubkey::find_program_address(&[b"credits", owner.as_ref()], &crate::ID); + require_keys_eq!(ctx.accounts.pda.key(), expected, CovenantError::Unauthorized); + let validator = ctx + .remaining_accounts + .first() + .ok_or(CovenantError::ValidatorRequired)? + .key(); + ctx.accounts.delegate_pda( + &ctx.accounts.payer, + &[b"credits".as_ref(), owner.as_ref()], + DelegateConfig { + validator: Some(validator), + ..Default::default() + }, + )?; + Ok(()) + } + + /// Checkpoint the delegated credit account's state (including the + /// provenance root) back to L1 without releasing it. Permissionless: any + /// payer can force the record on-chain, since a commit moves no tokens and + /// releases nothing. This is what lets a verifier or the slash authority + /// surface an agent's provenance root without the owner's cooperation. + #[cfg(feature = "ephemeral")] + pub fn commit_credits(ctx: Context) -> Result<()> { + MagicIntentBundleBuilder::new( + ctx.accounts.payer.to_account_info(), + ctx.accounts.magic_context.to_account_info(), + ctx.accounts.magic_program.to_account_info(), + ) + .commit(&[ctx.accounts.credits.to_account_info()]) + .build_and_invoke()?; + Ok(()) + } + + /// Commit the final credit balance and undelegate, returning the account to + /// L1 writability. Triggered before any L1 op that must move tokens against + /// this owner (e.g. a top-up via `buy_credits`) or on idle timeout. + #[cfg(feature = "ephemeral")] + pub fn undelegate_credits(ctx: Context) -> Result<()> { + MagicIntentBundleBuilder::new( + ctx.accounts.owner.to_account_info(), + ctx.accounts.magic_context.to_account_info(), + ctx.accounts.magic_program.to_account_info(), + ) + .commit_and_undelegate(&[ctx.accounts.credits.to_account_info()]) + .build_and_invoke()?; + Ok(()) + } +} + +#[derive(Accounts)] +pub struct Initialize<'info> { + #[account( + init, + payer = authority, + space = 8 + Config::INIT_SPACE, + seeds = [b"config"], + bump, + )] + pub config: Account<'info, Config>, + #[account(mut)] + pub authority: Signer<'info>, + pub covnt_mint: InterfaceAccount<'info, Mint>, + #[account( + constraint = treasury.mint == covnt_mint.key() @ CovenantError::WrongMint, + )] + pub treasury: InterfaceAccount<'info, TokenAccount>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct SetPause<'info> { + #[account( + mut, + seeds = [b"config"], + bump = config.bump, + has_one = authority @ CovenantError::Unauthorized, + )] + pub config: Account<'info, Config>, + pub authority: Signer<'info>, +} + +#[derive(Accounts)] +#[instruction(args: RegisterAgentArgs)] +pub struct RegisterAgent<'info> { + #[account( + seeds = [b"config"], + bump = config.bump, + )] + pub config: Account<'info, Config>, + #[account( + init, + payer = operator, + space = 8 + Agent::INIT_SPACE, + seeds = [b"agent", args.agent_key.as_ref()], + bump, + )] + pub agent: Account<'info, Agent>, + #[account(mut)] + pub operator: Signer<'info>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct SetAgentActive<'info> { + #[account( + seeds = [b"config"], + bump = config.bump, + has_one = authority @ CovenantError::Unauthorized, + )] + pub config: Account<'info, Config>, + pub authority: Signer<'info>, + #[account( + mut, + seeds = [b"agent", agent.agent_key.as_ref()], + bump = agent.bump, + )] + pub agent: Account<'info, Agent>, +} + +#[derive(Accounts)] +pub struct OpenCreditAccount<'info> { + #[account( + init, + payer = owner, + space = 8 + CreditAccount::INIT_SPACE, + seeds = [b"credits", owner.key().as_ref()], + bump, + )] + pub credits: Account<'info, CreditAccount>, + #[account(mut)] + pub owner: Signer<'info>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct BuyCredits<'info> { + #[account( + seeds = [b"config"], + bump = config.bump, + has_one = treasury, + )] + pub config: Account<'info, Config>, + #[account( + mut, + has_one = owner, + seeds = [b"credits", owner.key().as_ref()], + bump = credits.bump, + )] + pub credits: Account<'info, CreditAccount>, + #[account(mut)] + pub owner: Signer<'info>, + #[account( + mut, + constraint = owner_covnt.owner == owner.key() @ CovenantError::Unauthorized, + constraint = owner_covnt.mint == config.covnt_mint @ CovenantError::WrongMint, + )] + pub owner_covnt: InterfaceAccount<'info, TokenAccount>, + #[account(mut, constraint = treasury.mint == config.covnt_mint @ CovenantError::WrongMint)] + pub treasury: InterfaceAccount<'info, TokenAccount>, + #[account(constraint = covnt_mint.key() == config.covnt_mint @ CovenantError::WrongMint)] + pub covnt_mint: InterfaceAccount<'info, Mint>, + pub token_program: Interface<'info, TokenInterface>, +} + +impl<'info> BuyCredits<'info> { + fn buy_transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, TransferChecked<'info>> { + CpiContext::new( + self.token_program.to_account_info(), + TransferChecked { + mint: self.covnt_mint.to_account_info(), + from: self.owner_covnt.to_account_info(), + to: self.treasury.to_account_info(), + authority: self.owner.to_account_info(), + }, + ) + } +} + +#[derive(Accounts)] +pub struct ConsumeCredits<'info> { + #[account( + seeds = [b"config"], + bump = config.bump, + )] + pub config: Account<'info, Config>, + #[account( + mut, + has_one = owner, + seeds = [b"credits", owner.key().as_ref()], + bump = credits.bump, + )] + pub credits: Account<'info, CreditAccount>, + pub owner: Signer<'info>, +} + +#[derive(Accounts)] +pub struct Stake<'info> { + #[account( + seeds = [b"config"], + bump = config.bump, + )] + pub config: Account<'info, Config>, + #[account( + mut, + seeds = [b"agent", agent.agent_key.as_ref()], + bump = agent.bump, + )] + pub agent: Account<'info, Agent>, + #[account( + init, + payer = owner, + space = 8 + StakePosition::INIT_SPACE, + seeds = [b"stake", agent.agent_key.as_ref(), owner.key().as_ref()], + bump, + )] + pub position: Account<'info, StakePosition>, + #[account(mut)] + pub owner: Signer<'info>, + #[account( + mut, + constraint = owner_covnt.owner == owner.key() @ CovenantError::Unauthorized, + constraint = owner_covnt.mint == config.covnt_mint @ CovenantError::WrongMint, + )] + pub owner_covnt: InterfaceAccount<'info, TokenAccount>, + #[account( + mut, + constraint = stake_vault.owner == position.key() @ CovenantError::Unauthorized, + constraint = stake_vault.mint == config.covnt_mint @ CovenantError::WrongMint, + )] + pub stake_vault: InterfaceAccount<'info, TokenAccount>, + #[account(constraint = covnt_mint.key() == config.covnt_mint @ CovenantError::WrongMint)] + pub covnt_mint: InterfaceAccount<'info, Mint>, + pub token_program: Interface<'info, TokenInterface>, + pub system_program: Program<'info, System>, +} + +impl<'info> Stake<'info> { + fn stake_transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, TransferChecked<'info>> { + CpiContext::new( + self.token_program.to_account_info(), + TransferChecked { + mint: self.covnt_mint.to_account_info(), + from: self.owner_covnt.to_account_info(), + to: self.stake_vault.to_account_info(), + authority: self.owner.to_account_info(), + }, + ) + } +} + +#[derive(Accounts)] +pub struct Unstake<'info> { + #[account( + seeds = [b"config"], + bump = config.bump, + )] + pub config: Account<'info, Config>, + #[account( + mut, + seeds = [b"agent", position.agent_key.as_ref()], + bump = agent.bump, + constraint = agent.agent_key == position.agent_key @ CovenantError::AgentMismatch, + )] + pub agent: Account<'info, Agent>, + #[account( + mut, + seeds = [b"stake", position.agent_key.as_ref(), position.owner.as_ref()], + bump = position.bump, + constraint = position.owner == owner.key() @ CovenantError::Unauthorized, + close = owner, + )] + pub position: Account<'info, StakePosition>, + #[account(mut)] + pub owner: Signer<'info>, + #[account( + mut, + constraint = stake_vault.key() == position.vault @ CovenantError::Unauthorized, + constraint = stake_vault.owner == position.key() @ CovenantError::Unauthorized, + constraint = stake_vault.mint == config.covnt_mint @ CovenantError::WrongMint, + )] + pub stake_vault: InterfaceAccount<'info, TokenAccount>, + #[account( + mut, + constraint = owner_covnt.owner == owner.key() @ CovenantError::Unauthorized, + constraint = owner_covnt.mint == config.covnt_mint @ CovenantError::WrongMint, + )] + pub owner_covnt: InterfaceAccount<'info, TokenAccount>, + #[account(constraint = covnt_mint.key() == config.covnt_mint @ CovenantError::WrongMint)] + pub covnt_mint: InterfaceAccount<'info, Mint>, + pub token_program: Interface<'info, TokenInterface>, +} + +impl<'info> Unstake<'info> { + fn unstake_transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, TransferChecked<'info>> { + CpiContext::new( + self.token_program.to_account_info(), + TransferChecked { + mint: self.covnt_mint.to_account_info(), + from: self.stake_vault.to_account_info(), + to: self.owner_covnt.to_account_info(), + authority: self.position.to_account_info(), + }, + ) + } +} + +#[derive(Accounts)] +pub struct SlashStake<'info> { + #[account( + seeds = [b"config"], + bump = config.bump, + has_one = slash_authority @ CovenantError::Unauthorized, + )] + pub config: Account<'info, Config>, + pub slash_authority: Signer<'info>, + #[account( + mut, + seeds = [b"agent", agent.agent_key.as_ref()], + bump = agent.bump, + constraint = agent.agent_key == position.agent_key @ CovenantError::AgentMismatch, + )] + pub agent: Account<'info, Agent>, + #[account( + mut, + seeds = [b"stake", position.agent_key.as_ref(), position.owner.as_ref()], + bump = position.bump, + )] + pub position: Account<'info, StakePosition>, + #[account( + mut, + constraint = stake_vault.key() == position.vault @ CovenantError::Unauthorized, + constraint = stake_vault.owner == position.key() @ CovenantError::Unauthorized, + constraint = stake_vault.mint == config.covnt_mint @ CovenantError::WrongMint, + )] + pub stake_vault: InterfaceAccount<'info, TokenAccount>, + #[account( + mut, + constraint = slash_vault.key() == config.treasury @ CovenantError::Unauthorized, + constraint = slash_vault.mint == config.covnt_mint @ CovenantError::WrongMint, + )] + pub slash_vault: InterfaceAccount<'info, TokenAccount>, + #[account(constraint = covnt_mint.key() == config.covnt_mint @ CovenantError::WrongMint)] + pub covnt_mint: InterfaceAccount<'info, Mint>, + pub token_program: Interface<'info, TokenInterface>, +} + +impl<'info> SlashStake<'info> { + fn slash_transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, TransferChecked<'info>> { + CpiContext::new( + self.token_program.to_account_info(), + TransferChecked { + mint: self.covnt_mint.to_account_info(), + from: self.stake_vault.to_account_info(), + to: self.slash_vault.to_account_info(), + authority: self.position.to_account_info(), + }, + ) + } +} + +#[derive(Accounts)] +pub struct SlashForActions<'info> { + #[account( + seeds = [b"config"], + bump = config.bump, + has_one = slash_authority @ CovenantError::Unauthorized, + )] + pub config: Box>, + pub slash_authority: Signer<'info>, + #[account( + mut, + seeds = [b"agent", agent.agent_key.as_ref()], + bump = agent.bump, + constraint = agent.agent_key == position.agent_key @ CovenantError::AgentMismatch, + )] + pub agent: Box>, + #[account( + mut, + seeds = [b"stake", position.agent_key.as_ref(), position.owner.as_ref()], + bump = position.bump, + )] + pub position: Box>, + /// The agent's credit account, bound to the agent by its operator. Its + /// `provenance_root` is read as the slash reason, so the slash can only cite + /// the agent's own verifiable on-chain record. + #[account( + seeds = [b"credits", agent.operator.as_ref()], + bump = credits.bump, + )] + pub credits: Box>, + #[account( + mut, + constraint = stake_vault.key() == position.vault @ CovenantError::Unauthorized, + constraint = stake_vault.owner == position.key() @ CovenantError::Unauthorized, + constraint = stake_vault.mint == config.covnt_mint @ CovenantError::WrongMint, + )] + pub stake_vault: InterfaceAccount<'info, TokenAccount>, + #[account( + mut, + constraint = slash_vault.key() == config.treasury @ CovenantError::Unauthorized, + constraint = slash_vault.mint == config.covnt_mint @ CovenantError::WrongMint, + )] + pub slash_vault: InterfaceAccount<'info, TokenAccount>, + #[account(constraint = covnt_mint.key() == config.covnt_mint @ CovenantError::WrongMint)] + pub covnt_mint: InterfaceAccount<'info, Mint>, + pub token_program: Interface<'info, TokenInterface>, +} + +impl<'info> SlashForActions<'info> { + fn slash_transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, TransferChecked<'info>> { + CpiContext::new( + self.token_program.to_account_info(), + TransferChecked { + mint: self.covnt_mint.to_account_info(), + from: self.stake_vault.to_account_info(), + to: self.slash_vault.to_account_info(), + authority: self.position.to_account_info(), + }, + ) + } +} + +#[derive(Accounts)] +#[instruction(args: CreateTaskArgs)] +pub struct CreateTask<'info> { + #[account( + seeds = [b"config"], + bump = config.bump, + )] + pub config: Account<'info, Config>, + #[account( + seeds = [b"agent", agent.agent_key.as_ref()], + bump = agent.bump, + )] + pub agent: Account<'info, Agent>, + #[account( + init, + payer = client, + space = 8 + Task::INIT_SPACE, + seeds = [b"task", args.task_id.as_ref()], + bump, + )] + pub task: Box>, + #[account(mut)] + pub client: Signer<'info>, + #[account( + mut, + constraint = client_covnt.owner == client.key() @ CovenantError::Unauthorized, + constraint = client_covnt.mint == config.covnt_mint @ CovenantError::WrongMint, + )] + pub client_covnt: InterfaceAccount<'info, TokenAccount>, + #[account( + mut, + constraint = escrow_vault.owner == task.key() @ CovenantError::Unauthorized, + constraint = escrow_vault.mint == config.covnt_mint @ CovenantError::WrongMint, + )] + pub escrow_vault: InterfaceAccount<'info, TokenAccount>, + #[account(constraint = covnt_mint.key() == config.covnt_mint @ CovenantError::WrongMint)] + pub covnt_mint: InterfaceAccount<'info, Mint>, + pub token_program: Interface<'info, TokenInterface>, + pub system_program: Program<'info, System>, +} + +impl<'info> CreateTask<'info> { + fn task_fund_ctx(&self) -> CpiContext<'_, '_, '_, 'info, TransferChecked<'info>> { + CpiContext::new( + self.token_program.to_account_info(), + TransferChecked { + mint: self.covnt_mint.to_account_info(), + from: self.client_covnt.to_account_info(), + to: self.escrow_vault.to_account_info(), + authority: self.client.to_account_info(), + }, + ) + } +} + +#[derive(Accounts)] +pub struct ReleaseTask<'info> { + #[account( + seeds = [b"config"], + bump = config.bump, + )] + pub config: Account<'info, Config>, + #[account( + mut, + seeds = [b"task", task.task_id.as_ref()], + bump = task.bump, + has_one = client @ CovenantError::Unauthorized, + )] + pub task: Box>, + pub client: Signer<'info>, + #[account( + mut, + constraint = escrow_vault.owner == task.key() @ CovenantError::Unauthorized, + constraint = escrow_vault.mint == config.covnt_mint @ CovenantError::WrongMint, + )] + pub escrow_vault: InterfaceAccount<'info, TokenAccount>, + #[account( + mut, + constraint = provider_covnt.owner == task.provider @ CovenantError::Unauthorized, + constraint = provider_covnt.mint == config.covnt_mint @ CovenantError::WrongMint, + )] + pub provider_covnt: InterfaceAccount<'info, TokenAccount>, + #[account(constraint = covnt_mint.key() == config.covnt_mint @ CovenantError::WrongMint)] + pub covnt_mint: InterfaceAccount<'info, Mint>, + pub token_program: Interface<'info, TokenInterface>, +} + +impl<'info> ReleaseTask<'info> { + fn task_release_ctx(&self) -> CpiContext<'_, '_, '_, 'info, TransferChecked<'info>> { + CpiContext::new( + self.token_program.to_account_info(), + TransferChecked { + mint: self.covnt_mint.to_account_info(), + from: self.escrow_vault.to_account_info(), + to: self.provider_covnt.to_account_info(), + authority: self.task.to_account_info(), + }, + ) + } +} + +#[derive(Accounts)] +pub struct RefundTask<'info> { + #[account( + seeds = [b"config"], + bump = config.bump, + )] + pub config: Account<'info, Config>, + #[account( + mut, + seeds = [b"task", task.task_id.as_ref()], + bump = task.bump, + has_one = client @ CovenantError::Unauthorized, + )] + pub task: Box>, + pub client: Signer<'info>, + #[account( + mut, + constraint = escrow_vault.owner == task.key() @ CovenantError::Unauthorized, + constraint = escrow_vault.mint == config.covnt_mint @ CovenantError::WrongMint, + )] + pub escrow_vault: InterfaceAccount<'info, TokenAccount>, + #[account( + mut, + constraint = client_covnt.owner == client.key() @ CovenantError::Unauthorized, + constraint = client_covnt.mint == config.covnt_mint @ CovenantError::WrongMint, + )] + pub client_covnt: InterfaceAccount<'info, TokenAccount>, + #[account(constraint = covnt_mint.key() == config.covnt_mint @ CovenantError::WrongMint)] + pub covnt_mint: InterfaceAccount<'info, Mint>, + pub token_program: Interface<'info, TokenInterface>, +} + +impl<'info> RefundTask<'info> { + fn task_refund_ctx(&self) -> CpiContext<'_, '_, '_, 'info, TransferChecked<'info>> { + CpiContext::new( + self.token_program.to_account_info(), + TransferChecked { + mint: self.covnt_mint.to_account_info(), + from: self.escrow_vault.to_account_info(), + to: self.client_covnt.to_account_info(), + authority: self.task.to_account_info(), + }, + ) + } +} + +#[derive(Accounts)] +pub struct BurnCovnt<'info> { + #[account( + seeds = [b"config"], + bump = config.bump, + )] + pub config: Account<'info, Config>, + #[account(mut)] + pub owner: Signer<'info>, + #[account(mut, address = config.covnt_mint)] + pub covnt_mint: InterfaceAccount<'info, Mint>, + #[account( + mut, + constraint = owner_covnt.owner == owner.key() @ CovenantError::Unauthorized, + constraint = owner_covnt.mint == config.covnt_mint @ CovenantError::WrongMint, + )] + pub owner_covnt: InterfaceAccount<'info, TokenAccount>, + pub token_program: Interface<'info, TokenInterface>, +} + +impl<'info> BurnCovnt<'info> { + fn burn_ctx(&self) -> CpiContext<'_, '_, '_, 'info, Burn<'info>> { + CpiContext::new( + self.token_program.to_account_info(), + Burn { + mint: self.covnt_mint.to_account_info(), + from: self.owner_covnt.to_account_info(), + authority: self.owner.to_account_info(), + }, + ) + } +} + +#[derive(Accounts)] +#[instruction(args: AnchorReceiptBatchArgs)] +pub struct AnchorReceiptBatch<'info> { + #[account( + seeds = [b"config"], + bump = config.bump, + has_one = authority @ CovenantError::Unauthorized, + )] + pub config: Account<'info, Config>, + #[account( + init, + payer = authority, + space = 8 + ReceiptBatch::INIT_SPACE, + seeds = [b"receipt_batch", args.batch_id.as_ref()], + bump, + )] + pub batch: Account<'info, ReceiptBatch>, + #[account(mut)] + pub authority: Signer<'info>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct ClosePosition<'info> { + #[account( + mut, + seeds = [b"stake", position.agent_key.as_ref(), position.owner.as_ref()], + bump = position.bump, + constraint = position.owner == owner.key() @ CovenantError::Unauthorized, + close = owner, + )] + pub position: Account<'info, StakePosition>, + #[account(mut)] + pub owner: Signer<'info>, +} + +#[derive(Accounts)] +pub struct UpdateConfig<'info> { + #[account( + mut, + seeds = [b"config"], + bump = config.bump, + has_one = authority @ CovenantError::Unauthorized, + )] + pub config: Account<'info, Config>, + pub authority: Signer<'info>, +} + +#[derive(Accounts)] +pub struct UpdateTreasury<'info> { + #[account( + mut, + seeds = [b"config"], + bump = config.bump, + has_one = authority @ CovenantError::Unauthorized, + )] + pub config: Account<'info, Config>, + pub authority: Signer<'info>, + #[account(constraint = treasury.mint == config.covnt_mint @ CovenantError::WrongMint)] + pub treasury: InterfaceAccount<'info, TokenAccount>, +} + +#[derive(Accounts)] +pub struct MigrateConfig<'info> { + /// CHECK: config PDA validated by seeds; deserialized manually because the + /// legacy on-chain bytes do not fit the current `Config` layout. + #[account(mut, seeds = [b"config"], bump)] + pub config: UncheckedAccount<'info>, + #[account(mut)] + pub authority: Signer<'info>, + pub system_program: Program<'info, System>, +} + +/// Migrate a legacy `CreditAccount` to the current layout. Raw account because +/// the legacy bytes do not fit the new struct until realloc; the owner is +/// validated against the on-chain `owner` field in the handler. +#[derive(Accounts)] +pub struct MigrateCreditAccount<'info> { + /// CHECK: raw bytes (the legacy layout cannot deserialize until realloc); the + /// `[b"credits", owner]` seeds bind it to the signing owner. + #[account(mut, seeds = [b"credits", owner.key().as_ref()], bump)] + pub credits: UncheckedAccount<'info>, + #[account(mut)] + pub owner: Signer<'info>, + pub system_program: Program<'info, System>, +} + +/// Delegate the credit-account PDA to the ER. `#[delegate]` adds the +/// `delegate_pda` helper plus the buffer/record/metadata accounts and the +/// delegation + owner programs. The PDA is passed unchecked because delegation +/// transfers its ownership to the delegation program. +#[cfg(feature = "ephemeral")] +#[delegate] +#[derive(Accounts)] +pub struct DelegateCredits<'info> { + #[account(mut)] + pub payer: Signer<'info>, + /// CHECK: the `[b"credits", payer]` PDA, validated by the seeds passed to + /// `delegate_pda`. + #[account(mut, del)] + pub pda: UncheckedAccount<'info>, +} + +/// Undelegate the delegated credit account back to L1 writability. `#[commit]` +/// injects `magic_context` and `magic_program`. Owner-gated: returning the +/// account to L1 is the owner's (or the recovery keeper's) call. +#[cfg(feature = "ephemeral")] +#[commit] +#[derive(Accounts)] +pub struct CommitCredits<'info> { + #[account(mut)] + pub owner: Signer<'info>, + #[account( + mut, + has_one = owner @ CovenantError::Unauthorized, + seeds = [b"credits", owner.key().as_ref()], + bump = credits.bump, + )] + pub credits: Account<'info, CreditAccount>, +} + +/// Permissionless commit of the delegated credit account. Any `payer` funds +/// the checkpoint; the credit PDA is validated against its own stored `owner`, +/// so no owner signature is needed. A commit only writes the current ER state +/// to L1 (no token movement, no release), so opening it lets a verifier or +/// keeper force an agent's provenance root on-chain. +#[cfg(feature = "ephemeral")] +#[commit] +#[derive(Accounts)] +pub struct CommitCreditsPermissionless<'info> { + #[account(mut)] + pub payer: Signer<'info>, + #[account( + mut, + seeds = [b"credits", credits.owner.as_ref()], + bump = credits.bump, + )] + pub credits: Account<'info, CreditAccount>, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct InitializeArgs { + pub slash_authority: Pubkey, + pub credits_per_covnt: u64, + pub min_stake_lock: u64, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct RegisterAgentArgs { + pub agent_key: [u8; 32], + pub metadata_hash: [u8; 32], + pub capability_hash: [u8; 32], +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct CreateTaskArgs { + pub task_id: [u8; 32], + pub provider: Pubkey, + pub amount_covnt: u64, + pub task_hash: [u8; 32], + pub criteria_hash: [u8; 32], + pub deadline: i64, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct AnchorReceiptBatchArgs { + pub batch_id: [u8; 32], + pub merkle_root: [u8; 32], + pub receipt_count: u32, +} + +pub const TASK_FUNDED: u8 = 1; +pub const TASK_RELEASED: u8 = 2; +pub const TASK_REFUNDED: u8 = 3; + +#[account] +#[derive(InitSpace)] +pub struct Config { + pub authority: Pubkey, + pub slash_authority: Pubkey, + pub covnt_mint: Pubkey, + pub treasury: Pubkey, + pub credits_per_covnt: u64, + pub paused: bool, + pub bump: u8, + /// Minimum seconds a stake must remain locked past the staking instant. + /// `0` disables the floor (a staker may pick any `lock_until`). Appended + /// last so legacy 146-byte configs migrate by realloc (see `migrate_config`). + pub min_stake_lock: u64, +} + +#[account] +#[derive(InitSpace)] +pub struct Agent { + pub agent_key: [u8; 32], + pub operator: Pubkey, + pub metadata_hash: [u8; 32], + pub capability_hash: [u8; 32], + pub stake: u64, + pub reputation: u64, + pub active: bool, + pub bump: u8, +} + +#[account] +#[derive(InitSpace)] +pub struct CreditAccount { + pub owner: Pubkey, + pub balance: u64, + pub bump: u8, + /// Rolling hash-chain root over every consumed `receipt_hash`: + /// `root = sha256(root || receipt_hash)`, genesis = 32 zero bytes. Updated + /// on each `consume_credits` (gaslessly in the ER) and committed to L1 with + /// the balance, so it is a real-time, on-chain provenance record of the + /// metered actions. Appended last so legacy accounts migrate by realloc + /// (see `migrate_credit_account`). + pub provenance_root: [u8; 32], +} + +#[account] +#[derive(InitSpace)] +pub struct StakePosition { + pub agent_key: [u8; 32], + pub owner: Pubkey, + pub amount: u64, + pub lock_until: u64, + pub vault: Pubkey, + pub active: bool, + pub bump: u8, +} + +#[account] +#[derive(InitSpace)] +pub struct Task { + pub task_id: [u8; 32], + pub client: Pubkey, + pub agent_key: [u8; 32], + pub provider: Pubkey, + pub amount_covnt: u64, + pub task_hash: [u8; 32], + pub criteria_hash: [u8; 32], + pub result_hash: [u8; 32], + pub deadline: i64, + pub status: u8, + pub bump: u8, +} + +#[account] +#[derive(InitSpace)] +pub struct ReceiptBatch { + pub batch_id: [u8; 32], + pub authority: Pubkey, + pub merkle_root: [u8; 32], + pub receipt_count: u32, + pub created_at: i64, + pub bump: u8, +} + +#[event] +pub struct ProtocolInitialized { + pub authority: Pubkey, + pub slash_authority: Pubkey, + pub covnt_mint: Pubkey, + pub treasury: Pubkey, + pub credits_per_covnt: u64, +} + +#[event] +pub struct ProtocolPauseUpdated { + pub paused: bool, +} + +#[event] +pub struct AgentRegistered { + pub agent_key: [u8; 32], + pub operator: Pubkey, + pub metadata_hash: [u8; 32], + pub capability_hash: [u8; 32], +} + +#[event] +pub struct AgentStatusUpdated { + pub agent_key: [u8; 32], + pub active: bool, +} + +#[event] +pub struct CreditAccountOpened { + pub owner: Pubkey, + pub credit_account: Pubkey, +} + +#[event] +pub struct CreditsPurchased { + pub owner: Pubkey, + pub amount_covnt: u64, + pub credits: u64, +} + +#[event] +pub struct CreditsConsumed { + pub owner: Pubkey, + pub amount: u64, + pub receipt_hash: [u8; 32], + pub provenance_root: [u8; 32], +} + +#[event] +pub struct StakeWithdrawn { + pub agent_key: [u8; 32], + pub owner: Pubkey, + pub amount: u64, + pub withdrawn_at: u64, +} + +#[event] +pub struct StakeOpened { + pub agent_key: [u8; 32], + pub owner: Pubkey, + pub amount: u64, + pub lock_until: u64, + pub position: Pubkey, +} + +#[event] +pub struct StakeSlashed { + pub agent_key: [u8; 32], + pub owner: Pubkey, + pub amount: u64, + pub reason_hash: [u8; 32], +} + +#[event] +pub struct TaskCreated { + pub task_id: [u8; 32], + pub client: Pubkey, + pub agent_key: [u8; 32], + pub provider: Pubkey, + pub amount_covnt: u64, + pub task_hash: [u8; 32], + pub criteria_hash: [u8; 32], + pub deadline: i64, +} + +#[event] +pub struct TaskReleased { + pub task_id: [u8; 32], + pub provider: Pubkey, + pub amount_covnt: u64, + pub result_hash: [u8; 32], + pub receipt_hash: [u8; 32], +} + +#[event] +pub struct TaskRefunded { + pub task_id: [u8; 32], + pub client: Pubkey, + pub amount_covnt: u64, + pub deadline: i64, + pub refunded_at: i64, +} + +#[event] +pub struct CovntBurned { + pub owner: Pubkey, + pub amount: u64, + pub reason_hash: [u8; 32], +} + +#[event] +pub struct ReceiptBatchAnchored { + pub batch_id: [u8; 32], + pub authority: Pubkey, + pub merkle_root: [u8; 32], + pub receipt_count: u32, + pub created_at: i64, +} + +#[event] +pub struct StakePositionClosed { + pub agent_key: [u8; 32], + pub owner: Pubkey, +} + +#[event] +pub struct AuthorityUpdated { + pub previous: Pubkey, + pub new_authority: Pubkey, +} + +#[event] +pub struct SlashAuthorityUpdated { + pub previous: Pubkey, + pub new_slash_authority: Pubkey, +} + +#[event] +pub struct TreasuryUpdated { + pub previous: Pubkey, + pub new_treasury: Pubkey, +} + +#[event] +pub struct CreditsRateUpdated { + pub previous: u64, + pub credits_per_covnt: u64, +} + +#[event] +pub struct MinStakeLockUpdated { + pub previous: u64, + pub min_stake_lock: u64, +} + +#[event] +pub struct ConfigMigrated { + pub min_stake_lock: u64, +} + +#[error_code] +pub enum CovenantError { + #[msg("amount must be greater than zero")] + ZeroAmount, + #[msg("arithmetic overflow")] + Overflow, + #[msg("protocol is paused")] + ProtocolPaused, + #[msg("unauthorized")] + Unauthorized, + #[msg("wrong COVNT mint")] + WrongMint, + #[msg("agent is inactive")] + AgentInactive, + #[msg("agent mismatch")] + AgentMismatch, + #[msg("insufficient credits")] + InsufficientCredits, + #[msg("insufficient stake")] + InsufficientStake, + #[msg("stake position is inactive")] + StakeInactive, + #[msg("wrong task status")] + WrongTaskStatus, + #[msg("task deadline has passed; release no longer allowed (use refund_task)")] + TaskExpired, + #[msg("task deadline has not passed; refund not yet available")] + TaskNotExpired, + #[msg("stake position is still locked")] + StakeLocked, + #[msg("stake position is still active; unstake before closing")] + StakeStillActive, + #[msg("lock_until is shorter than the protocol minimum stake lock")] + LockTooShort, + #[msg("task escrow is disabled in this build")] + TasksDisabled, + #[msg("an explicit ER validator account is required to delegate")] + ValidatorRequired, + #[msg("the agent has no recorded actions to slash for (provenance root is genesis)")] + NoRecordedActions, +} diff --git a/agent-os/programs/settlement-ephemeral/src/lib.rs b/agent-os/programs/settlement-ephemeral/src/lib.rs new file mode 100644 index 000000000..c410c713a --- /dev/null +++ b/agent-os/programs/settlement-ephemeral/src/lib.rs @@ -0,0 +1,7 @@ +// ER build of the settlement program. `_shared.rs` is a verbatim copy of the +// sibling `settlement/src/lib.rs` (the source of truth), kept here so this crate +// is self-contained (no cross-crate include!). That self-containment is what lets +// the reproducible / OtterSec build mount this crate directly. Regenerate after +// any settlement change: `cp ../../settlement/src/lib.rs src/_shared.rs` — CI +// diffs the two and fails if they drift. +include!("_shared.rs"); diff --git a/agent-os/programs/settlement/Cargo.toml b/agent-os/programs/settlement/Cargo.toml index 653febbe7..10c463014 100644 --- a/agent-os/programs/settlement/Cargo.toml +++ b/agent-os/programs/settlement/Cargo.toml @@ -30,3 +30,9 @@ litesvm = "0.6" solana-sdk = "2.2" spl-token = "7.0" spl-token-2022 = "6.0" + +[lints.rust] +deprecated = "allow" +# The `ephemeral` feature is defined only in the sibling settlement-ephemeral +# crate, which include!s this source; here the cfg is intentionally unknown. +unexpected_cfgs = "allow" diff --git a/agent-os/programs/settlement/src/lib.rs b/agent-os/programs/settlement/src/lib.rs index a6dc5c96a..d8e3b3e52 100644 --- a/agent-os/programs/settlement/src/lib.rs +++ b/agent-os/programs/settlement/src/lib.rs @@ -1,17 +1,25 @@ -//! Covenant Solana protocol program. -//! -//! `$COVNT` is an external SPL mint. The program never mints it; protocol -//! utility comes from staking, escrowing, burning, and metering it into -//! non-transferable credits. - -#![allow(deprecated)] -#![allow(unexpected_cfgs)] +// Covenant Solana protocol program. +// +// `$COVNT` is an external SPL mint. The program never mints it; protocol +// utility comes from staking, escrowing, burning, and metering it into +// non-transferable credits. +// +// `allow(deprecated)` / `allow(unexpected_cfgs)` live in `[lints]` (Cargo.toml) +// rather than as crate inner attributes, so this file can be `include!`d verbatim +// by the isolated ER crate (settlement-ephemeral). Inner attributes break include!. use anchor_lang::prelude::*; use anchor_spl::token_interface::{ self, Burn, Mint, TokenAccount, TokenInterface, TransferChecked, }; +#[cfg(feature = "ephemeral")] +use ephemeral_rollups_sdk::anchor::{commit, delegate, ephemeral}; +#[cfg(feature = "ephemeral")] +use ephemeral_rollups_sdk::cpi::DelegateConfig; +#[cfg(feature = "ephemeral")] +use ephemeral_rollups_sdk::ephem::MagicIntentBundleBuilder; + declare_id!("cov9UDypG7nsryxdgMcKhKU2spRVWLVjxT2iTv6do5Y"); #[cfg(not(feature = "no-entrypoint"))] @@ -26,6 +34,7 @@ solana_security_txt::security_txt! { auditors: "None" } +#[cfg_attr(feature = "ephemeral", ephemeral)] #[program] pub mod settlement { use super::*; @@ -95,6 +104,7 @@ pub mod settlement { credits.owner = ctx.accounts.owner.key(); credits.balance = 0; credits.bump = ctx.bumps.credits; + credits.provenance_root = [0u8; 32]; emit!(CreditAccountOpened { owner: credits.owner, @@ -145,10 +155,21 @@ pub mod settlement { ctx.accounts.credits.balance -= amount; + // Fold the receipt into the account's provenance hash-chain. This runs in + // the ER per consume and commits to L1 with the balance, making the root a + // real-time, on-chain record of every metered action. + let provenance_root = anchor_lang::solana_program::hash::hashv(&[ + &ctx.accounts.credits.provenance_root, + &receipt_hash, + ]) + .to_bytes(); + ctx.accounts.credits.provenance_root = provenance_root; + emit!(CreditsConsumed { owner: ctx.accounts.owner.key(), amount, receipt_hash, + provenance_root, }); Ok(()) } @@ -175,6 +196,7 @@ pub mod settlement { position.agent_key = ctx.accounts.agent.agent_key; position.owner = ctx.accounts.owner.key(); position.amount = amount; + position.vault = ctx.accounts.stake_vault.key(); position.lock_until = lock_until; position.active = true; position.bump = ctx.bumps.position; @@ -229,7 +251,12 @@ pub mod settlement { ctx.accounts.covnt_mint.decimals, )?; - ctx.accounts.agent.stake = ctx.accounts.agent.stake.saturating_sub(amount); + ctx.accounts.agent.stake = ctx + .accounts + .agent + .stake + .checked_sub(amount) + .ok_or(CovenantError::InsufficientStake)?; emit!(StakeWithdrawn { agent_key, @@ -266,7 +293,73 @@ pub mod settlement { )?; ctx.accounts.position.amount -= amount; - ctx.accounts.agent.stake = ctx.accounts.agent.stake.saturating_sub(amount); + ctx.accounts.agent.stake = ctx + .accounts + .agent + .stake + .checked_sub(amount) + .ok_or(CovenantError::InsufficientStake)?; + if ctx.accounts.position.amount == 0 { + ctx.accounts.position.active = false; + } + + emit!(StakeSlashed { + agent_key, + owner, + amount, + reason_hash, + }); + Ok(()) + } + + /// Slash an agent's bond citing its on-chain actions. The reason is not + /// supplied by the caller: it is read from the `provenance_root` of the + /// operator's canonical credit account (`[b"credits", agent.operator]`), the + /// hash-chain `consume_credits` folds gaslessly in the ER and commits to L1. + /// So the reason is the operator's own committed record, not an arbitrary claim. + /// + /// Two limits the caller must know. The credit account must be undelegated: + /// `Account` cannot load while owned by the delegation program, + /// so an operator can defer this slash for up to the delegation timeout. And + /// the binding is to the operator's canonical credit account, not a per-agent + /// log, so it assumes the operator meters through that account. For a slash + /// that depends on neither, use `slash_stake`. + pub fn slash_for_actions(ctx: Context, amount: u64) -> Result<()> { + require!(!ctx.accounts.config.paused, CovenantError::ProtocolPaused); + require!(amount > 0, CovenantError::ZeroAmount); + require!(ctx.accounts.position.active, CovenantError::StakeInactive); + require!( + ctx.accounts.position.amount >= amount, + CovenantError::InsufficientStake + ); + + // A slash "for actions" requires recorded actions: refuse to cite a + // genesis (never-folded) provenance root. + let reason_hash = ctx.accounts.credits.provenance_root; + require!(reason_hash != [0u8; 32], CovenantError::NoRecordedActions); + let agent_key = ctx.accounts.position.agent_key; + let owner = ctx.accounts.position.owner; + let signer_seeds: &[&[u8]] = &[ + b"stake", + agent_key.as_ref(), + owner.as_ref(), + &[ctx.accounts.position.bump], + ]; + token_interface::transfer_checked( + ctx.accounts + .slash_transfer_ctx() + .with_signer(&[signer_seeds]), + amount, + ctx.accounts.covnt_mint.decimals, + )?; + + ctx.accounts.position.amount -= amount; + ctx.accounts.agent.stake = ctx + .accounts + .agent + .stake + .checked_sub(amount) + .ok_or(CovenantError::InsufficientStake)?; if ctx.accounts.position.amount == 0 { ctx.accounts.position.active = false; } @@ -555,6 +648,93 @@ pub mod settlement { emit!(ConfigMigrated { min_stake_lock }); Ok(()) } + + /// One-time migration of a legacy `CreditAccount` (predates `provenance_root`) + /// to the current layout: grows the account by 32 bytes. `realloc` zero-fills + /// the new bytes, so the provenance root starts at genesis. Owner-gated by the + /// `[b"credits", owner]` seed binding; idempotent (a no-op realloc on an + /// already-current account). + pub fn migrate_credit_account(ctx: Context) -> Result<()> { + let info = ctx.accounts.credits.to_account_info(); + let new_len = 8 + CreditAccount::INIT_SPACE; + if info.data_len() < new_len { + let deficit = Rent::get()? + .minimum_balance(new_len) + .saturating_sub(info.lamports()); + if deficit > 0 { + anchor_lang::system_program::transfer( + CpiContext::new( + ctx.accounts.system_program.to_account_info(), + anchor_lang::system_program::Transfer { + from: ctx.accounts.owner.to_account_info(), + to: info.clone(), + }, + ), + deficit, + )?; + } + info.realloc(new_len, true)?; + } + Ok(()) + } + + /// Delegate the caller's credit account `[b"credits", owner]` to the + /// MagicBlock delegation program so metering (`consume_credits`) can run in + /// an ephemeral rollup. Only the program-owned accounting PDA moves; no token + /// custody is involved. Pass an ER validator pubkey as the first remaining + /// account to pin it (see `DelegateConfig.validator`). + #[cfg(feature = "ephemeral")] + pub fn delegate_credits(ctx: Context) -> Result<()> { + let owner = ctx.accounts.payer.key(); + let (expected, _) = Pubkey::find_program_address(&[b"credits", owner.as_ref()], &crate::ID); + require_keys_eq!(ctx.accounts.pda.key(), expected, CovenantError::Unauthorized); + let validator = ctx + .remaining_accounts + .first() + .ok_or(CovenantError::ValidatorRequired)? + .key(); + ctx.accounts.delegate_pda( + &ctx.accounts.payer, + &[b"credits".as_ref(), owner.as_ref()], + DelegateConfig { + validator: Some(validator), + ..Default::default() + }, + )?; + Ok(()) + } + + /// Checkpoint the delegated credit account's state (including the + /// provenance root) back to L1 without releasing it. Permissionless: any + /// payer can force the record on-chain, since a commit moves no tokens and + /// releases nothing. This is what lets a verifier or the slash authority + /// surface an agent's provenance root without the owner's cooperation. + #[cfg(feature = "ephemeral")] + pub fn commit_credits(ctx: Context) -> Result<()> { + MagicIntentBundleBuilder::new( + ctx.accounts.payer.to_account_info(), + ctx.accounts.magic_context.to_account_info(), + ctx.accounts.magic_program.to_account_info(), + ) + .commit(&[ctx.accounts.credits.to_account_info()]) + .build_and_invoke()?; + Ok(()) + } + + /// Commit the final credit balance and undelegate, returning the account to + /// L1 writability. Triggered before any L1 op that must move tokens against + /// this owner (e.g. a top-up via `buy_credits`) or on idle timeout. + #[cfg(feature = "ephemeral")] + pub fn undelegate_credits(ctx: Context) -> Result<()> { + MagicIntentBundleBuilder::new( + ctx.accounts.owner.to_account_info(), + ctx.accounts.magic_context.to_account_info(), + ctx.accounts.magic_program.to_account_info(), + ) + .commit_and_undelegate(&[ctx.accounts.credits.to_account_info()]) + .build_and_invoke()?; + Ok(()) + } } #[derive(Accounts)] @@ -784,6 +964,7 @@ pub struct Unstake<'info> { pub owner: Signer<'info>, #[account( mut, + constraint = stake_vault.key() == position.vault @ CovenantError::Unauthorized, constraint = stake_vault.owner == position.key() @ CovenantError::Unauthorized, constraint = stake_vault.mint == config.covnt_mint @ CovenantError::WrongMint, )] @@ -837,11 +1018,16 @@ pub struct SlashStake<'info> { pub position: Account<'info, StakePosition>, #[account( mut, + constraint = stake_vault.key() == position.vault @ CovenantError::Unauthorized, constraint = stake_vault.owner == position.key() @ CovenantError::Unauthorized, constraint = stake_vault.mint == config.covnt_mint @ CovenantError::WrongMint, )] pub stake_vault: InterfaceAccount<'info, TokenAccount>, - #[account(mut, constraint = slash_vault.mint == config.covnt_mint @ CovenantError::WrongMint)] + #[account( + mut, + constraint = slash_vault.key() == config.treasury @ CovenantError::Unauthorized, + constraint = slash_vault.mint == config.covnt_mint @ CovenantError::WrongMint, + )] pub slash_vault: InterfaceAccount<'info, TokenAccount>, #[account(constraint = covnt_mint.key() == config.covnt_mint @ CovenantError::WrongMint)] pub covnt_mint: InterfaceAccount<'info, Mint>, @@ -862,6 +1048,68 @@ impl<'info> SlashStake<'info> { } } +#[derive(Accounts)] +pub struct SlashForActions<'info> { + #[account( + seeds = [b"config"], + bump = config.bump, + has_one = slash_authority @ CovenantError::Unauthorized, + )] + pub config: Box>, + pub slash_authority: Signer<'info>, + #[account( + mut, + seeds = [b"agent", agent.agent_key.as_ref()], + bump = agent.bump, + constraint = agent.agent_key == position.agent_key @ CovenantError::AgentMismatch, + )] + pub agent: Box>, + #[account( + mut, + seeds = [b"stake", position.agent_key.as_ref(), position.owner.as_ref()], + bump = position.bump, + )] + pub position: Box>, + /// The agent's credit account, bound to the agent by its operator. Its + /// `provenance_root` is read as the slash reason, so the slash can only cite + /// the agent's own verifiable on-chain record. + #[account( + seeds = [b"credits", agent.operator.as_ref()], + bump = credits.bump, + )] + pub credits: Box>, + #[account( + mut, + constraint = stake_vault.key() == position.vault @ CovenantError::Unauthorized, + constraint = stake_vault.owner == position.key() @ CovenantError::Unauthorized, + constraint = stake_vault.mint == config.covnt_mint @ CovenantError::WrongMint, + )] + pub stake_vault: InterfaceAccount<'info, TokenAccount>, + #[account( + mut, + constraint = slash_vault.key() == config.treasury @ CovenantError::Unauthorized, + constraint = slash_vault.mint == config.covnt_mint @ CovenantError::WrongMint, + )] + pub slash_vault: InterfaceAccount<'info, TokenAccount>, + #[account(constraint = covnt_mint.key() == config.covnt_mint @ CovenantError::WrongMint)] + pub covnt_mint: InterfaceAccount<'info, Mint>, + pub token_program: Interface<'info, TokenInterface>, +} + +impl<'info> SlashForActions<'info> { + fn slash_transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, TransferChecked<'info>> { + CpiContext::new( + self.token_program.to_account_info(), + TransferChecked { + mint: self.covnt_mint.to_account_info(), + from: self.stake_vault.to_account_info(), + to: self.slash_vault.to_account_info(), + authority: self.position.to_account_info(), + }, + ) + } +} + #[derive(Accounts)] #[instruction(args: CreateTaskArgs)] pub struct CreateTask<'info> { @@ -1115,6 +1363,73 @@ pub struct MigrateConfig<'info> { pub system_program: Program<'info, System>, } +/// Migrate a legacy `CreditAccount` to the current layout. Raw account because +/// the legacy bytes do not fit the new struct until realloc; the owner is +/// validated against the on-chain `owner` field in the handler. +#[derive(Accounts)] +pub struct MigrateCreditAccount<'info> { + /// CHECK: raw bytes (the legacy layout cannot deserialize until realloc); the + /// `[b"credits", owner]` seeds bind it to the signing owner. + #[account(mut, seeds = [b"credits", owner.key().as_ref()], bump)] + pub credits: UncheckedAccount<'info>, + #[account(mut)] + pub owner: Signer<'info>, + pub system_program: Program<'info, System>, +} + +/// Delegate the credit-account PDA to the ER. `#[delegate]` adds the +/// `delegate_pda` helper plus the buffer/record/metadata accounts and the +/// delegation + owner programs. The PDA is passed unchecked because delegation +/// transfers its ownership to the delegation program. +#[cfg(feature = "ephemeral")] +#[delegate] +#[derive(Accounts)] +pub struct DelegateCredits<'info> { + #[account(mut)] + pub payer: Signer<'info>, + /// CHECK: the `[b"credits", payer]` PDA, validated by the seeds passed to + /// `delegate_pda`. + #[account(mut, del)] + pub pda: UncheckedAccount<'info>, +} + +/// Undelegate the delegated credit account back to L1 writability. `#[commit]` +/// injects `magic_context` and `magic_program`. Owner-gated: returning the +/// account to L1 is the owner's (or the recovery keeper's) call. +#[cfg(feature = "ephemeral")] +#[commit] +#[derive(Accounts)] +pub struct CommitCredits<'info> { + #[account(mut)] + pub owner: Signer<'info>, + #[account( + mut, + has_one = owner @ CovenantError::Unauthorized, + seeds = [b"credits", owner.key().as_ref()], + bump = credits.bump, + )] + pub credits: Account<'info, CreditAccount>, +} + +/// Permissionless commit of the delegated credit account. Any `payer` funds +/// the checkpoint; the credit PDA is validated against its own stored `owner`, +/// so no owner signature is needed. A commit only writes the current ER state +/// to L1 (no token movement, no release), so opening it lets a verifier or +/// keeper force an agent's provenance root on-chain. +#[cfg(feature = "ephemeral")] +#[commit] +#[derive(Accounts)] +pub struct CommitCreditsPermissionless<'info> { + #[account(mut)] + pub payer: Signer<'info>, + #[account( + mut, + seeds = [b"credits", credits.owner.as_ref()], + bump = credits.bump, + )] + pub credits: Account<'info, CreditAccount>, +} + #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct InitializeArgs { pub slash_authority: Pubkey, @@ -1185,6 +1500,13 @@ pub struct CreditAccount { pub owner: Pubkey, pub balance: u64, pub bump: u8, + /// Rolling hash-chain root over every consumed `receipt_hash`: + /// `root = sha256(root || receipt_hash)`, genesis = 32 zero bytes. Updated + /// on each `consume_credits` (gaslessly in the ER) and committed to L1 with + /// the balance, so it is a real-time, on-chain provenance record of the + /// metered actions. Appended last so legacy accounts migrate by realloc + /// (see `migrate_credit_account`). + pub provenance_root: [u8; 32], } #[account] @@ -1194,6 +1516,7 @@ pub struct StakePosition { pub owner: Pubkey, pub amount: u64, pub lock_until: u64, + pub vault: Pubkey, pub active: bool, pub bump: u8, } @@ -1271,6 +1594,7 @@ pub struct CreditsConsumed { pub owner: Pubkey, pub amount: u64, pub receipt_hash: [u8; 32], + pub provenance_root: [u8; 32], } #[event] @@ -1421,4 +1745,8 @@ pub enum CovenantError { LockTooShort, #[msg("task escrow is disabled in this build")] TasksDisabled, + #[msg("an explicit ER validator account is required to delegate")] + ValidatorRequired, + #[msg("the agent has no recorded actions to slash for (provenance root is genesis)")] + NoRecordedActions, } diff --git a/agent-os/programs/settlement/tests/accountability.rs b/agent-os/programs/settlement/tests/accountability.rs new file mode 100644 index 000000000..091626bd6 --- /dev/null +++ b/agent-os/programs/settlement/tests/accountability.rs @@ -0,0 +1,64 @@ +mod common; + +use common::*; + +const AGENT: [u8; 32] = [0xACu8; 32]; + +// The agent-accountability loop, end to end. +// +// Covenant's pitch to MagicBlock is "an accountability layer for agents". This +// pins the loop that makes that concrete, tying the economic side (a slashable +// bond on L1) to the verifiable side (a per-action provenance hash-chain that +// rides the ER): +// +// 1. Bond — register the agent and stake a slashable position against it. +// 2. Act — the agent meters its work; every receipt folds into the +// credit account's on-chain provenance root (the same fold that +// runs gaslessly in the ephemeral rollup and commits to L1). +// 3. Slash — on a violation, the bond is burned with a reason that points +// at that provenance root, so the penalty is anchored to the +// verifiable record of what the agent actually did, not an +// arbitrary claim. +#[test] +fn agent_accountability_bond_provenance_slash() { + let mut env = boot(); + + // 1. Bond. + register_agent(&mut env, &AGENT); + let (position, stake_vault, _) = stake_locked(&mut env, &AGENT, 1_000, 0); + assert_eq!(agent_stake(&env, &AGENT), 1_000, "agent is bonded"); + + // 2. Act: two metered actions, each folded into the provenance chain. + let credits = open_credit_account(&mut env); + let owner_covnt = funded_covnt(&mut env, 1_000); + buy_credits(&mut env, &credits, &owner_covnt, 10).expect("buy credits"); + consume_credits_with(&mut env, &credits, 1, [0xA1u8; 32]).expect("action 1"); + consume_credits_with(&mut env, &credits, 1, [0xA2u8; 32]).expect("action 2"); + let provenance = credit_provenance_root(&env, &credits); + assert_ne!(provenance, [0u8; 32], "the agent's actions are recorded on chain"); + + // 3. Slash, referencing the provenance root as the reason. + let slash_vault = env.treasury; + let vault_before = token_balance(&env, &slash_vault); + // The program reads the reason straight from the agent's on-chain + // provenance_root (the `provenance` we just read) via the bound credit + // account, so the penalty is provably anchored to the agent's own record. + // There is no caller-supplied reason to forge. + slash_for_actions( + &mut env, + &AGENT, + &position, + &credits, + &stake_vault, + &slash_vault, + 1_000, + ) + .expect("slash the bond for its on-chain actions"); + + assert_eq!(agent_stake(&env, &AGENT), 0, "bond fully slashed"); + assert_eq!( + token_balance(&env, &slash_vault) - vault_before, + 1_000, + "the slashed bond moved to the slash vault" + ); +} diff --git a/agent-os/programs/settlement/tests/common/mod.rs b/agent-os/programs/settlement/tests/common/mod.rs index ea26f2bc8..6662f29e0 100644 --- a/agent-os/programs/settlement/tests/common/mod.rs +++ b/agent-os/programs/settlement/tests/common/mod.rs @@ -185,6 +185,7 @@ pub fn plant_phantom_credit_account(env: &mut Env, owner: &Pubkey) -> Pubkey { owner: *owner, balance: 0, bump: 255, + provenance_root: [0u8; 32], } .try_serialize(&mut data) .unwrap(); @@ -212,6 +213,14 @@ pub fn credit_balance(env: &Env, credits: &Pubkey) -> u64 { .balance } +pub fn credit_provenance_root(env: &Env, credits: &Pubkey) -> [u8; 32] { + let acc = env.svm.get_account(credits).expect("credit account exists"); + let mut data = acc.data(); + CreditAccount::try_deserialize(&mut data) + .expect("credit account") + .provenance_root +} + pub fn register_agent(env: &mut Env, agent_key: &[u8; 32]) -> Pubkey { let agent = agent_pda(agent_key); let data = ix::RegisterAgent { @@ -323,11 +332,31 @@ pub fn slash_stake( stake_vault: &Pubkey, slash_vault: &Pubkey, amount: u64, +) -> Result<(), TransactionError> { + slash_stake_with( + env, + agent_key, + position, + stake_vault, + slash_vault, + amount, + [0u8; 32], + ) +} + +pub fn slash_stake_with( + env: &mut Env, + agent_key: &[u8; 32], + position: &Pubkey, + stake_vault: &Pubkey, + slash_vault: &Pubkey, + amount: u64, + reason_hash: [u8; 32], ) -> Result<(), TransactionError> { let agent = agent_pda(agent_key); let data = ix::SlashStake { amount, - reason_hash: [0u8; 32], + reason_hash, } .data(); let metas = vec![ @@ -354,6 +383,45 @@ pub fn slash_stake( ) } +// Slash citing the agent's on-chain provenance: the program reads the reason +// from the bound credit account's provenance_root, so there is no caller-supplied +// reason_hash to pass. +pub fn slash_for_actions( + env: &mut Env, + agent_key: &[u8; 32], + position: &Pubkey, + credits: &Pubkey, + stake_vault: &Pubkey, + slash_vault: &Pubkey, + amount: u64, +) -> Result<(), TransactionError> { + let agent = agent_pda(agent_key); + let data = ix::SlashForActions { amount }.data(); + let metas = vec![ + AccountMeta::new_readonly(env.config, false), + AccountMeta::new_readonly(env.slash_authority.pubkey(), true), + AccountMeta::new(agent, false), + AccountMeta::new(*position, false), + AccountMeta::new_readonly(*credits, false), + AccountMeta::new(*stake_vault, false), + AccountMeta::new(*slash_vault, false), + AccountMeta::new_readonly(env.mint, false), + AccountMeta::new_readonly(spl_token::ID, false), + ]; + let payer = env.payer.insecure_clone(); + let slasher = env.slash_authority.insecure_clone(); + send( + &mut env.svm, + &payer, + &[Instruction { + program_id: ID, + accounts: metas, + data, + }], + &[&slasher], + ) +} + pub fn token_balance(env: &Env, account: &Pubkey) -> u64 { let acc = env.svm.get_account(account).expect("token account exists"); spl_token::state::Account::unpack(acc.data()) @@ -647,11 +715,20 @@ pub fn consume_credits( env: &mut Env, credits: &Pubkey, amount: u64, +) -> Result<(), TransactionError> { + consume_credits_with(env, credits, amount, [9u8; 32]) +} + +pub fn consume_credits_with( + env: &mut Env, + credits: &Pubkey, + amount: u64, + receipt_hash: [u8; 32], ) -> Result<(), TransactionError> { let owner = env.payer.pubkey(); let data = ix::ConsumeCredits { amount, - receipt_hash: [9u8; 32], + receipt_hash, } .data(); let metas = vec![ @@ -1054,6 +1131,57 @@ pub fn plant_legacy_config(env: &mut Env) { .unwrap(); } +pub fn migrate_credit_account(env: &mut Env, credits: &Pubkey) -> Result<(), TransactionError> { + let owner = env.payer.pubkey(); + let data = ix::MigrateCreditAccount {}.data(); + let metas = vec![ + AccountMeta::new(*credits, false), + AccountMeta::new(owner, true), + AccountMeta::new_readonly(system_program::ID, false), + ]; + let payer = env.payer.insecure_clone(); + send( + &mut env.svm, + &payer, + &[Instruction { + program_id: ID, + accounts: metas, + data, + }], + &[], + ) +} + +/// Plant a legacy 49-byte CreditAccount (no provenance_root) at the canonical +/// `[b"credits", owner]` PDA to model a pre-migration account. +pub fn plant_legacy_credit_account(env: &mut Env, balance: u64) -> Pubkey { + let owner = env.payer.pubkey(); + let (credits, bump) = Pubkey::find_program_address(&[b"credits", owner.as_ref()], &ID); + let acc = CreditAccount { + owner, + balance, + bump, + provenance_root: [0u8; 32], + }; + let mut full = Vec::new(); + acc.try_serialize(&mut full).unwrap(); + let legacy = full[..full.len() - 32].to_vec(); // drop the trailing provenance_root + let lamports = env.svm.minimum_balance_for_rent_exemption(legacy.len()); + env.svm + .set_account( + credits, + Account { + lamports, + data: legacy, + owner: ID, + executable: false, + rent_epoch: 0, + }, + ) + .unwrap(); + credits +} + pub fn set_credits_per_covnt(env: &mut Env, value: u64) -> Result<(), TransactionError> { let authority = env.payer.pubkey(); let data = ix::SetCreditsPerCovnt { diff --git a/agent-os/programs/settlement/tests/failure_modes.rs b/agent-os/programs/settlement/tests/failure_modes.rs index fe0337186..cecbf0435 100644 --- a/agent-os/programs/settlement/tests/failure_modes.rs +++ b/agent-os/programs/settlement/tests/failure_modes.rs @@ -44,12 +44,7 @@ fn slash_more_than_staked_rejected() { let mut env = boot(); register_agent(&mut env, &AGENT); let (position, stake_vault, _) = stake_locked(&mut env, &AGENT, 500, 0); - let slash_vault = create_token_account( - &mut env.svm, - &env.payer.insecure_clone(), - &env.mint, - &env.payer.pubkey(), - ); + let slash_vault = env.treasury; let err = slash_stake(&mut env, &AGENT, &position, &stake_vault, &slash_vault, 600) .expect_err("cannot slash beyond the position"); assert_eq!(custom_error(&err), Some(E_INSUFFICIENT_STAKE)); diff --git a/agent-os/programs/settlement/tests/lifecycle.rs b/agent-os/programs/settlement/tests/lifecycle.rs index 93aa533ae..287cb1e8f 100644 --- a/agent-os/programs/settlement/tests/lifecycle.rs +++ b/agent-os/programs/settlement/tests/lifecycle.rs @@ -21,6 +21,47 @@ fn credit_buy_then_consume_tracks_balance() { assert_eq!(credit_balance(&env, &credits), 0); } +// Each consume folds its receipt_hash into the credit account's provenance +// root (root = sha256(root || receipt_hash)), so the account carries a +// tamper-evident hash-chain of every metered action, committed to L1 with the +// balance. This is the on-chain half of the audit/receipt-root-in-ER work. +#[test] +fn consume_chains_provenance_root_in_credit_account() { + use solana_sdk::hash::hashv; + let mut env = boot(); + let credits = open_credit_account(&mut env); + let owner_covnt = funded_covnt(&mut env, 1_000); + buy_credits(&mut env, &credits, &owner_covnt, 10).expect("buy"); // 100 credits + + // Genesis: the provenance root is 32 zero bytes. + assert_eq!(credit_provenance_root(&env, &credits), [0u8; 32]); + + let r1 = [1u8; 32]; + let r2 = [2u8; 32]; + consume_credits_with(&mut env, &credits, 5, r1).expect("consume r1"); + consume_credits_with(&mut env, &credits, 5, r2).expect("consume r2"); + + let genesis = [0u8; 32]; + let root1 = hashv(&[&genesis, &r1]).to_bytes(); + let expected = hashv(&[&root1, &r2]).to_bytes(); + assert_eq!(credit_provenance_root(&env, &credits), expected); + assert_eq!(credit_balance(&env, &credits), 90); +} + +// A legacy 49-byte credit account (no provenance_root) grows to the current +// layout on migrate, preserving its balance and starting the root at genesis. +// This is the upgrade path for accounts that predate the provenance field. +#[test] +fn migrate_credit_account_upgrades_legacy_layout() { + let mut env = boot(); + let credits = plant_legacy_credit_account(&mut env, 4_242); + migrate_credit_account(&mut env, &credits).expect("migrate legacy credit account"); + + let acc_balance = credit_balance(&env, &credits); // only deserializes if realloc succeeded + assert_eq!(acc_balance, 4_242); // preserved + assert_eq!(credit_provenance_root(&env, &credits), [0u8; 32]); // genesis +} + // Regression for the re-stake lockout: before `unstake` closed the position, // a second stake to the same (agent, owner) PDA failed on `init`. #[test] @@ -52,12 +93,7 @@ fn full_slash_then_close_then_restake() { let mut env = boot(); register_agent(&mut env, &AGENT); let (position, stake_vault, _) = stake_locked(&mut env, &AGENT, 1_000, 0); - let slash_vault = create_token_account( - &mut env.svm, - &env.payer.insecure_clone(), - &env.mint, - &env.payer.pubkey(), - ); + let slash_vault = env.treasury; slash_stake( &mut env, diff --git a/agent-os/programs/settlement/tests/pause_guards.rs b/agent-os/programs/settlement/tests/pause_guards.rs index d4cd15ef9..bb7d6958b 100644 --- a/agent-os/programs/settlement/tests/pause_guards.rs +++ b/agent-os/programs/settlement/tests/pause_guards.rs @@ -10,12 +10,7 @@ fn slash_stake_succeeds_when_not_paused() { let mut env = boot(); register_agent(&mut env, &AGENT); let (position, stake_vault) = stake(&mut env, &AGENT, 1_000); - let slash_vault = create_token_account( - &mut env.svm, - &env.payer.insecure_clone(), - &env.mint, - &env.payer.pubkey(), - ); + let slash_vault = env.treasury; slash_stake(&mut env, &AGENT, &position, &stake_vault, &slash_vault, 400) .expect("slash should succeed while unpaused"); @@ -29,12 +24,7 @@ fn slash_stake_rejected_when_paused() { let mut env = boot(); register_agent(&mut env, &AGENT); let (position, stake_vault) = stake(&mut env, &AGENT, 1_000); - let slash_vault = create_token_account( - &mut env.svm, - &env.payer.insecure_clone(), - &env.mint, - &env.payer.pubkey(), - ); + let slash_vault = env.treasury; set_pause(&mut env, true); diff --git a/deploy/Dockerfile.facilitator b/deploy/Dockerfile.facilitator new file mode 100644 index 000000000..78e2fddfa --- /dev/null +++ b/deploy/Dockerfile.facilitator @@ -0,0 +1,29 @@ +## Multi-stage Dockerfile for the x402 ER facilitator on Render. +## Build context: repository root (so agent-os/ is visible). +## Verifies ER-settled (consume_credits) payments and gates the /paid endpoint. + +FROM rust:1.90-slim-bookworm AS builder +ENV CARGO_TERM_COLOR=always +RUN apt-get update \ + && apt-get install -y --no-install-recommends pkg-config libssl-dev ca-certificates \ + && rm -rf /var/lib/apt/lists/* +WORKDIR /src +COPY agent-os/ ./agent-os/ +WORKDIR /src/agent-os +RUN cargo build --release -p covenant-x402-facilitator --bin covenant-x402-facilitator + +FROM debian:bookworm-slim AS runtime +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates \ + && rm -rf /var/lib/apt/lists/* +RUN useradd --create-home --shell /bin/bash covenant +COPY --from=builder /src/agent-os/target/release/covenant-x402-facilitator /usr/local/bin/covenant-x402-facilitator + +# Devnet defaults; override PROGRAM / ER / PRICE in render.yaml or the dashboard. +# PORT is injected by the platform and read by the binary. +ENV PROGRAM=cov9UDypG7nsryxdgMcKhKU2spRVWLVjxT2iTv6do5Y +ENV ER=https://devnet-eu.magicblock.app +ENV PRICE=1 +USER covenant +EXPOSE 8402 +ENTRYPOINT ["/usr/local/bin/covenant-x402-facilitator"] diff --git a/render.yaml b/render.yaml index 2e2c71dfd..ead5fc300 100644 --- a/render.yaml +++ b/render.yaml @@ -223,6 +223,33 @@ services: - key: RENDER_API_KEY sync: false # team-scoped Render API key + # x402 ER facilitator: verifies ER-settled (consume_credits) payments and gates + # the /paid endpoint. Public web service so agents/clients can reach it. Devnet + # config; the ER signer key + delegated credit account live with the caller, not + # here, so this service holds no secret. Binds Render's $PORT. + - type: web + name: covenant-x402-er-facilitator + runtime: docker + plan: starter + region: frankfurt + autoDeploy: true + rootDir: . + dockerfilePath: ./deploy/Dockerfile.facilitator + dockerContext: . + healthCheckPath: /health + buildFilter: + paths: + - agent-os/crates/covenant-x402-facilitator/** + - agent-os/crates/covenant-x402/** + - deploy/Dockerfile.facilitator + envVars: + - key: PROGRAM + value: cov9UDypG7nsryxdgMcKhKU2spRVWLVjxT2iTv6do5Y + - key: ER + value: https://devnet-eu.magicblock.app + - key: PRICE + value: "1" + envVarGroups: - name: covenant-sandbox-shared envVars: diff --git a/services/settlement-publisher/src/index.mjs b/services/settlement-publisher/src/index.mjs index bd3ec3791..6d733533c 100644 --- a/services/settlement-publisher/src/index.mjs +++ b/services/settlement-publisher/src/index.mjs @@ -22,6 +22,8 @@ import { Keypair, PublicKey, SystemProgram, + Transaction, + sendAndConfirmTransaction, } from "@solana/web3.js"; import http from "node:http"; import { readFileSync, mkdirSync, appendFileSync, existsSync } from "node:fs"; @@ -38,6 +40,9 @@ const OPERATOR_TOKEN = process.env.COVENANT_OPERATOR_TOKEN ?? ""; const RPC_URL = process.env.COVENANT_SOLANA_RPC_URL ?? "https://api.devnet.solana.com"; const CLUSTER = process.env.COVENANT_SOLANA_CLUSTER ?? "devnet"; +// When set, meter gaslessly in a MagicBlock ER (the credit account must be +// delegated to the pinned validator) instead of paying a per-action L1 fee. +const ER_URL = process.env.COVENANT_ER_URL ?? null; // On-chain addresses are baked in for the current devnet deployment. // Each is overridable via env so this same service can flip to mainnet @@ -116,6 +121,7 @@ function recordSig(row) { const operator = loadOperatorKeypair(); const wallet = new Wallet(operator); const conn = new Connection(RPC_URL, "confirmed"); +const erConn = ER_URL ? new Connection(ER_URL, "confirmed") : null; const provider = new AnchorProvider(conn, wallet, { commitment: "confirmed" }); const idl = JSON.parse( @@ -130,6 +136,7 @@ console.log("[boot] config:", CONFIG.toBase58()); console.log("[boot] credit account:", CREDIT_ACCOUNT.toBase58()); console.log("[boot] daemon:", DAEMON_URL); console.log("[boot] rpc:", RPC_URL); +console.log("[boot] mode:", erConn ? `ER gasless (${ER_URL})` : `L1 (${CLUSTER})`); // ─── publish loop ───────────────────────────────────────────────────── @@ -143,20 +150,36 @@ async function consumeOne(intentId, intentText) { const receiptHash = Array.from( createHash("sha256").update(intentId).digest(), ); - const sig = await program.methods + const builder = program.methods .consumeCredits(SETTLE_AMOUNT, receiptHash) .accounts({ config: CONFIG, credits: CREDIT_ACCOUNT, owner: operator.publicKey, - }) - .rpc(); - // Best-effort confirmation slot — solana's signature subscribe path - // would be more accurate but a single getSignatureStatuses call is - // fine for the sidecar's surfaceable info. + }); + + // ER mode: the credit account is delegated to the pinned validator, so the + // same consume_credits ix runs gaslessly in the rollup (committed back to L1 + // out of band by the session delegate/commit/undelegate). L1 mode: anchor .rpc(). + let sig, settledOn; + if (erConn) { + const ix = await builder.instruction(); + sig = await sendAndConfirmTransaction( + erConn, + new Transaction().add(ix), + [operator], + { commitment: "confirmed", skipPreflight: true }, + ); + settledOn = "er"; + } else { + sig = await builder.rpc(); + settledOn = CLUSTER; + } + + // Best-effort confirmation slot. let slot = null; try { - const st = await conn.getSignatureStatuses([sig]); + const st = await (erConn ?? conn).getSignatureStatuses([sig]); slot = st.value[0]?.slot ?? null; } catch { // best-effort; the sig is what matters @@ -167,7 +190,7 @@ async function consumeOne(intentId, intentText) { slot, settled_at_ms: Date.now(), intent_text: intentText?.slice(0, 200) ?? null, - cluster: CLUSTER, + cluster: settledOn, }); return sig; } @@ -275,8 +298,20 @@ const server = http.createServer((req, res) => { res.writeHead(404); res.end(); }); -server.listen(HTTP_PORT, "0.0.0.0", () => { - console.log(`[http] listening on :${HTTP_PORT}`); -}); - -loop(); +if (process.env.SETTLE_ONCE) { + // One-shot: poll the audit feed once, publish, exit. Used for tests and + // for cron-style operation without a long-lived process. + try { + const n = await pollOnce(); + console.log(`[once] published ${n}`); + } catch (err) { + console.error(`[once] error: ${err instanceof Error ? err.message : String(err)}`); + process.exitCode = 1; + } + process.exit(process.exitCode ?? 0); +} else { + server.listen(HTTP_PORT, "0.0.0.0", () => { + console.log(`[http] listening on :${HTTP_PORT}`); + }); + loop(); +}