diff --git a/client_prover/psy_cli/psy_user_cli/src/main.rs b/client_prover/psy_cli/psy_user_cli/src/main.rs index 44e82c15..14900ddb 100644 --- a/client_prover/psy_cli/psy_user_cli/src/main.rs +++ b/client_prover/psy_cli/psy_user_cli/src/main.rs @@ -208,7 +208,6 @@ fn command_paths(cli: &Cli) -> Vec<&str> { Commands::GetLatestBlockState(args) => paths.push(&args.rpc_config), Commands::GetBlockState(args) => paths.push(&args.rpc_config), Commands::LocalProver(args) => paths.push(&args.rpc_config), - #[cfg(feature = "gnark-wrap")] Commands::ProveProxy(args) => paths.push(&args.rpc_config), Commands::FaucetServer(args) => paths.push(&args.rpc_config), Commands::GetClaimAmount(args) => paths.push(&args.rpc_config), @@ -666,7 +665,6 @@ async fn main() -> anyhow::Result<()> { psy_prover::run_server(prover_args).await?; CommandResult::generic("local-prover") } - #[cfg(feature = "gnark-wrap")] Commands::ProveProxy(prove_proxy_args) => { crate::subcommand::prove_proxy::run(prove_proxy_args).await?; CommandResult::generic("prove-proxy") diff --git a/client_prover/psy_cli/psy_user_cli/src/subcommand/mod.rs b/client_prover/psy_cli/psy_user_cli/src/subcommand/mod.rs index 98836f5c..42215aaa 100644 --- a/client_prover/psy_cli/psy_user_cli/src/subcommand/mod.rs +++ b/client_prover/psy_cli/psy_user_cli/src/subcommand/mod.rs @@ -10,7 +10,6 @@ pub mod contract_abi_upload; pub mod deploy_contract; pub mod faucet_server; pub mod local_prover; -#[cfg(feature = "gnark-wrap")] pub mod prove_proxy; pub mod simulate; pub mod update_contract; @@ -110,7 +109,6 @@ pub enum Commands { // local proving LocalProver(ProverArgs), - #[cfg(feature = "gnark-wrap")] ProveProxy(psy_client_common::args::ProveProxyArgs), FaucetServer(PsyFaucetServerArgs), diff --git a/client_prover/psy_core/psy_common/src/args.rs b/client_prover/psy_core/psy_common/src/args.rs index c4cb3181..9dd52e5c 100644 --- a/client_prover/psy_core/psy_common/src/args.rs +++ b/client_prover/psy_core/psy_common/src/args.rs @@ -192,12 +192,45 @@ pub struct ProverArgs { pub api_key: String, } +/// Which proof families a prove-proxy instance serves. +/// +/// `user` — wallet proofs: UPS session chain, contract calls, signatures, minifiers. +/// `system` — relayer proofs: the three bridge Groth16 methods. +/// `all` — both; intended for single-machine local testnets. +#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)] +pub enum ProveProxyRole { + User, + System, + All, +} + +impl ProveProxyRole { + pub fn serves_user(&self) -> bool { + matches!(self, ProveProxyRole::User | ProveProxyRole::All) + } + + pub fn serves_system(&self) -> bool { + matches!(self, ProveProxyRole::System | ProveProxyRole::All) + } + + pub fn as_str(&self) -> &'static str { + match self { + ProveProxyRole::User => "user", + ProveProxyRole::System => "system", + ProveProxyRole::All => "all", + } + } +} + #[derive(Clone, Debug, Parser)] pub struct ProveProxyArgs { #[clap(env = "PROVE_PROXY_LISTEN_ADDR", long, default_value = "0.0.0.0:9999")] pub listen_addr: String, #[clap(env, long, default_value = "config.json", env)] pub rpc_config: String, + /// Proof families this instance serves. Defaults to user proofs only. + #[clap(env = "PROVE_PROXY_ROLE", long, value_enum, default_value_t = ProveProxyRole::User, ignore_case = true)] + pub role: ProveProxyRole, } #[derive(Clone, Debug, Parser)] @@ -258,3 +291,39 @@ pub struct ExportKeyStoreArgs { #[clap(long, env = "WALLET_PASSWORD")] pub wallet_password: String, } + +#[cfg(test)] +mod prove_proxy_role_tests { + use super::*; + use clap::Parser; + + #[test] + fn role_defaults_to_user() { + let args = ProveProxyArgs::try_parse_from(["prove-proxy"]).unwrap(); + assert_eq!(args.role, ProveProxyRole::User); + assert!(args.role.serves_user()); + assert!(!args.role.serves_system()); + } + + #[test] + fn role_parses_case_insensitively() { + let args = ProveProxyArgs::try_parse_from(["prove-proxy", "--role", "SYSTEM"]).unwrap(); + assert_eq!(args.role, ProveProxyRole::System); + assert!(!args.role.serves_user()); + assert!(args.role.serves_system()); + } + + #[test] + fn role_all_serves_both() { + let args = ProveProxyArgs::try_parse_from(["prove-proxy", "--role", "all"]).unwrap(); + assert_eq!(args.role, ProveProxyRole::All); + assert!(args.role.serves_user()); + assert!(args.role.serves_system()); + assert_eq!(args.role.as_str(), "all"); + } + + #[test] + fn role_rejects_unknown_value() { + assert!(ProveProxyArgs::try_parse_from(["prove-proxy", "--role", "bridge"]).is_err()); + } +} diff --git a/client_prover/psy_core/psy_config/src/lib.rs b/client_prover/psy_core/psy_config/src/lib.rs index 8352705d..be2f485b 100644 --- a/client_prover/psy_core/psy_config/src/lib.rs +++ b/client_prover/psy_core/psy_config/src/lib.rs @@ -55,6 +55,10 @@ pub struct NetworkConfig { pub realm_configs: Vec, pub coordinator_configs: Vec, pub prove_proxy_url: Vec, + /// Prove-proxy pool that serves the bridge Groth16 (relayer) proofs. + /// The relayer refuses to start when this is empty for the current network. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub system_prove_proxy_url: Vec, pub faucet_rpc_url: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub api_services_url: Option>, @@ -780,7 +784,7 @@ mod tests { #[test] fn test_config_loading() { - let config_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../config.json"); + let config_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../psy-genesis/config.json"); let config = PsyConfigGoldilocks::from_file(config_path.to_str().unwrap()).unwrap(); assert_eq!(config.current_network_name(), "localhost"); @@ -795,7 +799,9 @@ mod tests { let json = r#"{ "networks": { "localhost": { - "network": { + "magic": "0x1", + "faucet_rpc_url": [], + "nostr_relay_url": "ws://127.0.0.1:8081", "users_per_realm": 1048576, "global_user_tree_height": 24, "realm_user_tree_height": 20, @@ -803,18 +809,21 @@ mod tests { "realm_configs": [{"id": 0, "rpc_url": ["http://127.0.0.1:8546"]}], "coordinator_configs": [{"id": 0, "rpc_url": ["http://127.0.0.1:8545"]}], "prove_proxy_url": ["http://127.0.0.1:9999"], + "system_prove_proxy_url": ["http://127.0.0.1:9999"], "native_currency": "PSY", "native_currency_decimal": 9, "native_currency_name": "PSY", "fees": { "register_user_fee": 0, "deploy_contract_fee": 0, - "guta_fee": 5000000000 + "guta_fee": 5000000000, + "da_fee": 0 } - } }, "testnet": { - "network": { + "magic": "0x1", + "faucet_rpc_url": [], + "nostr_relay_url": "ws://127.0.0.1:8081", "users_per_realm": 1048576, "global_user_tree_height": 24, "realm_user_tree_height": 20, @@ -822,15 +831,16 @@ mod tests { "realm_configs": [{"id": 0, "rpc_url": ["https://testnet.example.com"]}], "coordinator_configs": [{"id": 0, "rpc_url": ["https://testnet-coord.example.com"]}], "prove_proxy_url": ["https://testnet-prover.example.com"], + "system_prove_proxy_url": ["https://testnet-prover.example.com"], "native_currency": "tPSY", "native_currency_decimal": 9, "native_currency_name": "Test PSY", "fees": { "register_user_fee": 1000, "deploy_contract_fee": 5000, - "guta_fee": 5000000000 + "guta_fee": 5000000000, + "da_fee": 0 } - } } }, "defaultNetwork": "localhost" @@ -846,12 +856,61 @@ mod tests { assert_eq!(testnet.fees.register_user_fee, 1000); } + #[test] + fn system_prove_proxy_url_defaults_to_empty_and_reads_when_present() { + // Template: the "localhost" network object's fields from test_network_switching + // above, plus `magic`, `faucet_rpc_url` and `nostr_relay_url` (required fields + // on NetworkConfig with no serde default) and without the extra "network" + // wrapper key that test_network_switching's literal uses. Config::networks is + // HashMap>, i.e. flat — real config.json (see + // psy-genesis/config.json) has no "network" wrapper either. The + // test_network_switching-family tests are pre-existing failures for this + // structural reason; see task report. + // This base still does not contain system_prove_proxy_url. + let base = r#"{ + "magic": "0x1", + "users_per_realm": 1048576, + "global_user_tree_height": 24, + "realm_user_tree_height": 20, + "group_realm_height": 1, + "realm_configs": [{"id": 0, "rpc_url": ["http://127.0.0.1:8546"]}], + "coordinator_configs": [{"id": 0, "rpc_url": ["http://127.0.0.1:8545"]}], + "prove_proxy_url": ["http://127.0.0.1:9999"], + "faucet_rpc_url": ["http://127.0.0.1:8547"], + "nostr_relay_url": "wss://relay.127.0.0.1.example", + "native_currency": "PSY", + "native_currency_decimal": 9, + "native_currency_name": "PSY", + "fees": { + "register_user_fee": 0, + "deploy_contract_fee": 0, + "guta_fee": 5000000000, + "da_fee": 0 + } + }"#; + let without = format!(r#"{{"networks":{{"localhost":{base}}},"defaultNetwork":"localhost"}}"#); + let with = without.replacen( + r#""prove_proxy_url": ["http://127.0.0.1:9999"],"#, + r#""prove_proxy_url": ["http://127.0.0.1:9999"], "system_prove_proxy_url": ["http://127.0.0.1:9997"],"#, + 1, + ); + assert_ne!(with, without, "replacen must hit the prove_proxy_url line"); + + let cfg = PsyConfigGoldilocks::from_json(&without).unwrap(); + assert!(cfg.get_current_network().unwrap().system_prove_proxy_url.is_empty()); + + let cfg = PsyConfigGoldilocks::from_json(&with).unwrap(); + assert_eq!(cfg.get_current_network().unwrap().system_prove_proxy_url, vec!["http://127.0.0.1:9997"]); + } + #[test] fn test_flexible_config_creation() { let json = r#"{ "networks": { "dev": { - "network": { + "magic": "0x1", + "faucet_rpc_url": [], + "nostr_relay_url": "ws://127.0.0.1:8081", "users_per_realm": 1024, "global_user_tree_height": 20, "realm_user_tree_height": 10, @@ -859,18 +918,21 @@ mod tests { "realm_configs": [{"id": 0, "rpc_url": ["http://dev.local"]}], "coordinator_configs": [{"id": 0, "rpc_url": ["http://coord.local"]}], "prove_proxy_url": ["http://prover.local"], + "system_prove_proxy_url": ["http://prover.local"], "native_currency": "DEV", "native_currency_decimal": 6, "native_currency_name": "Development", "fees": { "register_user_fee": 100, "deploy_contract_fee": 500, - "guta_fee": 1000000000 + "guta_fee": 1000000000, + "da_fee": 0 } - } }, "localhost": { - "network": { + "magic": "0x1", + "faucet_rpc_url": [], + "nostr_relay_url": "ws://127.0.0.1:8081", "users_per_realm": 1024, "global_user_tree_height": 20, "realm_user_tree_height": 10, @@ -878,18 +940,19 @@ mod tests { "realm_configs": [{"id": 0, "rpc_url": ["http://localhost:8546"]}], "coordinator_configs": [{"id": 0, "rpc_url": ["http://localhost:8545"]}], "prove_proxy_url": ["http://localhost:9999"], + "system_prove_proxy_url": ["http://localhost:9999"], "native_currency": "LOCAL", "native_currency_decimal": 8, "native_currency_name": "Local Token", "fees": { "register_user_fee": 50, "deploy_contract_fee": 250, - "guta_fee": 500000000 + "guta_fee": 500000000, + "da_fee": 0 } - } } }, - "defaultNetwork": "dev" + "defaultNetwork": "localhost" }"#; let config1 = PsyConfigGoldilocks::from_json(json).unwrap(); @@ -909,7 +972,9 @@ mod tests { let json = r#"{ "networks": { "dev": { - "network": { + "magic": "0x1", + "faucet_rpc_url": [], + "nostr_relay_url": "ws://127.0.0.1:8081", "users_per_realm": 1024, "global_user_tree_height": 20, "realm_user_tree_height": 10, @@ -917,18 +982,21 @@ mod tests { "realm_configs": [{"id": 0, "rpc_url": ["http://dev.local"]}], "coordinator_configs": [{"id": 0, "rpc_url": ["http://coord.local"]}], "prove_proxy_url": ["http://prover.local"], + "system_prove_proxy_url": ["http://prover.local"], "native_currency": "DEV", "native_currency_decimal": 6, "native_currency_name": "Development", "fees": { "register_user_fee": 100, "deploy_contract_fee": 500, - "guta_fee": 1000000000 + "guta_fee": 1000000000, + "da_fee": 0 } - } }, "localhost": { - "network": { + "magic": "0x1", + "faucet_rpc_url": [], + "nostr_relay_url": "ws://127.0.0.1:8081", "users_per_realm": 512, "global_user_tree_height": 18, "realm_user_tree_height": 9, @@ -936,15 +1004,16 @@ mod tests { "realm_configs": [{"id": 0, "rpc_url": ["http://localhost:8546"]}], "coordinator_configs": [{"id": 0, "rpc_url": ["http://localhost:8545"]}], "prove_proxy_url": ["http://localhost:9999"], + "system_prove_proxy_url": ["http://localhost:9999"], "native_currency": "LOCAL", "native_currency_decimal": 8, "native_currency_name": "Local Token", "fees": { "register_user_fee": 50, "deploy_contract_fee": 250, - "guta_fee": 500000000 + "guta_fee": 500000000, + "da_fee": 0 } - } } }, "defaultNetwork": "localhost" @@ -975,7 +1044,9 @@ mod tests { let json = r#"{ "networks": { "only_network": { - "network": { + "magic": "0x1", + "faucet_rpc_url": [], + "nostr_relay_url": "ws://127.0.0.1:8081", "users_per_realm": 1024, "global_user_tree_height": 20, "realm_user_tree_height": 10, @@ -983,18 +1054,19 @@ mod tests { "realm_configs": [{"id": 0, "rpc_url": ["http://test.local"]}], "coordinator_configs": [{"id": 0, "rpc_url": ["http://coord.local"]}], "prove_proxy_url": ["http://prover.local"], + "system_prove_proxy_url": ["http://prover.local"], "native_currency": "TEST", "native_currency_decimal": 6, "native_currency_name": "Test", "fees": { "register_user_fee": 0, "deploy_contract_fee": 0, - "guta_fee": 1000000000 + "guta_fee": 1000000000, + "da_fee": 0 } - } } }, - "defaultNetwork": "only_network" + "defaultNetwork": "missing_network" }"#; let result = PsyConfigGoldilocks::from_json(json); diff --git a/client_prover/psy_prover/src/lib.rs b/client_prover/psy_prover/src/lib.rs index 4920c36e..b6aae6ca 100644 --- a/client_prover/psy_prover/src/lib.rs +++ b/client_prover/psy_prover/src/lib.rs @@ -4,13 +4,11 @@ pub mod signature; pub mod trace; pub mod wallet; -#[cfg(all(not(target_arch = "wasm32"), feature = "gnark-wrap"))] +#[cfg(not(target_arch = "wasm32"))] use psy_config::PSY_NETWORK_MAGIC; #[cfg(not(target_arch = "wasm32"))] use crate::local::native::faucet::PsyFaucetServerProvider; -#[cfg(all(not(target_arch = "wasm32"), feature = "gnark-wrap"))] -use crate::local::native::prove_proxy::ProveProxyServerProvider; #[cfg(not(target_arch = "wasm32"))] pub async fn run_server(args: psy_client_common::args::ProverArgs) -> anyhow::Result<()> { @@ -64,7 +62,7 @@ pub async fn run_server(args: psy_client_common::args::ProverArgs) -> anyhow::Re Ok(()) } -#[cfg(all(not(target_arch = "wasm32"), feature = "gnark-wrap"))] +#[cfg(not(target_arch = "wasm32"))] pub async fn run_prove_proxy_server(args: psy_client_common::args::ProveProxyArgs) -> anyhow::Result<()> { use std::net::SocketAddr; @@ -73,11 +71,39 @@ pub async fn run_prove_proxy_server(args: psy_client_common::args::ProveProxyArg use psy_client_common::health::HealthLayer; use tower_http::cors::{Any, CorsLayer}; - use crate::local::native::prove_proxy::ProveProxyRpcServer; + use crate::local::native::prove_proxy::assemble_rpc_module; + use crate::local::native::prove_proxy::user::{ProveProxyUserRpcServer, UserProveProvider}; let psy_config = psy_config::PsyConfigGoldilocks::from_file(&args.rpc_config)?; let rpc_config = psy_config.get_current_network()?; - let prove_proxy = ProveProxyServerProvider::new_with_config(rpc_config.clone(), PSY_NETWORK_MAGIC).await?; + + let role = args.role; + tracing::info!(role = role.as_str(), "prove proxy role"); + + // User circuits are built before assembly because the constructor is + // async; assemble_rpc_module only decides whether to *use* it. + let user = if role.serves_user() { + Some(UserProveProvider::new_with_config(rpc_config.clone(), PSY_NETWORK_MAGIC).await?) + } else { + None + }; + + let module = assemble_rpc_module( + role, + move || Ok(user.expect("user provider built when role serves user").into_rpc().into()), + || { + #[cfg(feature = "gnark-wrap")] + { + use crate::local::native::prove_proxy::system::{ProveProxySystemRpcServer, SystemProveProvider}; + Ok(SystemProveProvider::new()?.into_rpc().into()) + } + #[cfg(not(feature = "gnark-wrap"))] + { + anyhow::bail!("role `{}` needs system proofs, but this binary was built without the `gnark-wrap` feature", role.as_str()) + } + }, + )?; + let cors_opts = CorsLayer::new() .allow_methods([Method::POST, Method::OPTIONS]) .allow_origin(Any) @@ -96,8 +122,8 @@ pub async fn run_prove_proxy_server(args: psy_client_common::args::ProveProxyArg .build(server_addr) .await?; - let handle = server.start(prove_proxy.into_rpc()); - println!("\n[CFLI:PSY_PROVE_PROXY_STARTED][{}]\n", server_addr); + let handle = server.start(module); + println!("\n[CFLI:PSY_PROVE_PROXY_STARTED][{}][{}]\n", server_addr, role.as_str()); handle.stopped().await; Ok(()) } diff --git a/client_prover/psy_prover/src/local/native/mod.rs b/client_prover/psy_prover/src/local/native/mod.rs index cb5571b1..da0f08eb 100644 --- a/client_prover/psy_prover/src/local/native/mod.rs +++ b/client_prover/psy_prover/src/local/native/mod.rs @@ -1,5 +1,4 @@ pub mod faucet; -#[cfg(feature = "gnark-wrap")] pub mod prove_proxy; use std::{sync::Arc, time::Duration}; diff --git a/client_prover/psy_prover/src/local/native/prove_proxy/mod.rs b/client_prover/psy_prover/src/local/native/prove_proxy/mod.rs new file mode 100644 index 00000000..73d33a98 --- /dev/null +++ b/client_prover/psy_prover/src/local/native/prove_proxy/mod.rs @@ -0,0 +1,70 @@ +//! prove-proxy: wallet-facing (user) and relayer-facing (system) proving RPCs. +//! +//! `user` and `system` are separate `#[rpc]` traits so a process can register +//! exactly one family; see `assemble_rpc_module`. + +pub mod user; + +#[cfg(feature = "gnark-wrap")] +pub mod system; +#[cfg(feature = "gnark-wrap")] +pub mod types; + +use jsonrpsee::{ + proc_macros::rpc, + server::{Methods, RpcModule}, + types::ErrorObjectOwned, +}; +use plonky2::plonk::config::{GenericConfig, PoseidonGoldilocksConfig}; +use psy_client_common::args::ProveProxyRole; + +pub(crate) type C = PoseidonGoldilocksConfig; +pub(crate) type F = >::F; +pub(crate) const D: usize = 2; + +/// Answer of `psy_get_prove_proxy_role`. Lets deploy scripts and operators +/// confirm which pool an instance belongs to without probing proof methods. +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct ProveProxyRoleInfo { + pub role: String, + pub user_methods: bool, + pub system_methods: bool, +} + +#[rpc(server, client, namespace = "psy")] +pub trait ProveProxyInfoRpc { + #[method(name = "get_prove_proxy_role")] + async fn get_prove_proxy_role(&self) -> Result; +} + +struct RoleInfoProvider(ProveProxyRole); + +#[jsonrpsee::core::async_trait] +impl ProveProxyInfoRpcServer for RoleInfoProvider { + async fn get_prove_proxy_role(&self) -> Result { + Ok(ProveProxyRoleInfo { + role: self.0.as_str().to_string(), + user_methods: self.0.serves_user(), + system_methods: self.0.serves_system(), + }) + } +} + +/// Builds the server module for `role`. Constructors for families the role +/// does not serve are never invoked, so a `user` process never builds bridge +/// circuits and a `system` process never builds the UPS circuit manager. +pub fn assemble_rpc_module(role: ProveProxyRole, make_user: U, make_system: S) -> anyhow::Result> +where + U: FnOnce() -> anyhow::Result, + S: FnOnce() -> anyhow::Result, +{ + let mut module = RpcModule::new(()); + module.merge(RoleInfoProvider(role).into_rpc())?; + if role.serves_user() { + module.merge(make_user()?)?; + } + if role.serves_system() { + module.merge(make_system()?)?; + } + Ok(module) +} diff --git a/client_prover/psy_prover/src/local/native/prove_proxy/system.rs b/client_prover/psy_prover/src/local/native/prove_proxy/system.rs new file mode 100644 index 00000000..f45d33a5 --- /dev/null +++ b/client_prover/psy_prover/src/local/native/prove_proxy/system.rs @@ -0,0 +1,650 @@ +use std::sync::{Arc, OnceLock}; + +use jsonrpsee::{core::async_trait, proc_macros::rpc, types::ErrorObjectOwned}; +use parth_core::{ + crypto::hash::merkle_proof::DeltaMerkleProofCore as ParthDeltaMerkleProofCore, pgoldilocks::QHashOut as ParthQHashOut, + protocol::core_types::QNetworkTreeConstants, +}; +use plonky2::{ + field::types::{Field, PrimeField64}, + plonk::{circuit_data::CommonCircuitData, proof::ProofWithPublicInputs}, +}; +use psy_config::network_constants::{DEPOSIT_TREE_CONTRACT_STATE_TREE_HEIGHT, WITHDRAWAL_TREE_CONTRACT_STATE_TREE_HEIGHT}; +use psy_core::{constants::chain_id::PsyChainNetworkType, job::job_id::ProvingJobCircuitType, network_config::PsyNetworkLocalDevnetConstants}; +use psy_data::v1::qdata::checkpoint::PQEDCheckpointGlobalStateRoots; +use psy_plonky2_basic_helpers::verifier::circuit_library::CircuitInfoLibraryCore; +use psy_plonky2_circuits::{ + bridge::{ + circuits::{ + bridge_agg_final::BridgeAggFinalCircuit, + bridge_wrap::{BridgeWrapCircuit, DepositBatchWrapCircuit, SharedGroth16Wrapper, WithdrawalClaimWrapCircuit}, + }, + gadgets::tree_root_in_contract_state::TreeRootInContractStateWitnessInput, + }, + circuit_library::get_plonky2_circuit_library_and_prover_for_network, + coordinator::coordinator_helper::QEDCoordinatorCircuitManager, +}; +use psy_plonky2_common_circuits::bridge::{ + deposit_batch_append_circuit::{ + compute_batch_append_preimage, BatchAppendInputs as DepositBatchAppendInputs, DepositBatchAppendCircuit, + DepositLeafData as DepositBatchLeafData, MAX_DEPOSIT_BATCH_SIZE, + }, + withdrawal_batch_claim_circuit::{ + WithdrawalBatchClaimCircuit, WithdrawalBatchClaimInputs, WithdrawalBatchClaimSlotInputs, MAX_WITHDRAWAL_CLAIM_BATCH_SIZE, + WITHDRAWAL_BATCH_CLAIM_PUBLIC_INPUTS_WORDS, WITHDRAWAL_BATCH_CLAIM_SLOT_WORDS, + }, +}; + +use super::{ + types::*, + C, D, F, +}; + +#[rpc(server, client, namespace = "psy")] +pub trait ProveProxySystemRpc { + #[method(name = "prove_withdrawal_batch_claim_groth16")] + async fn prove_withdrawal_batch_claim_groth16( + &self, + input: BridgeWithdrawalBatchWitnessInput, + ) -> Result; + + #[method(name = "prove_deposit_batch_append_groth16")] + async fn prove_deposit_batch_append_groth16( + &self, + input: BridgeDepositBatchWitnessInput, + ) -> Result; + + /// Bridge aggregation: checkpoints → BridgeAggCircuit → BridgeWrapCircuit → + /// Groth16 + #[method(name = "prove_bridge_agg_groth16")] + async fn prove_bridge_agg_groth16(&self, deps_network: String, input: BridgeAggWitnessInput) -> Result; +} + +pub struct SystemProveProvider { + pub deposit_batch_wrap_circuit: Arc, + pub withdrawal_claim_wrap_circuit: Arc, + pub bridge_wrap_circuit: Arc, + pub deposit_batch_groth16_wrapper: Arc, + pub withdrawal_claim_groth16_wrapper: Arc, + pub bridge_groth16_wrapper: Arc, +} + +impl SystemProveProvider { + /// Builds the bridge wrap circuits and preloads the three Groth16 keystores. + /// Does not touch the coordinator RPC or the UPS circuit manager. + pub fn new() -> anyhow::Result { + use psy_plonky2_circuits::qstandard::QStandardCircuit; + + // ── Pre-build Groth16 wrapping circuits (shared across all threads) ── + // These depend only on the inner circuit structure, not on runtime data. + // Building once at startup saves ~200ms per request (CircuitBuilder::new + + // builder.build). + + tracing::info!("Pre-building DepositBatchWrapCircuit..."); + let deposit_template = DepositBatchAppendCircuit::::build(MAX_DEPOSIT_BATCH_SIZE, 32); + let deposit_minifier = psy_plonky2_circuits::proof_minifier::pm_chain::QEDProofMinifierChain::::new( + &deposit_template.circuit_data.verifier_only, + &deposit_template.circuit_data.common, + 2, + ); + let deposit_fp = ParthQHashOut(deposit_minifier.get_fingerprint()); + let deposit_batch_wrap_circuit = Arc::new(DepositBatchWrapCircuit::new( + deposit_minifier.get_common_data(), + deposit_fp, + deposit_minifier.get_verifier_data().constants_sigmas_cap.height(), + )); + let deposit_batch_groth16_wrapper = Arc::new( + DepositBatchWrapCircuit::new( + deposit_minifier.get_common_data(), + deposit_fp, + deposit_minifier.get_verifier_data().constants_sigmas_cap.height(), + ) + .into_shared_groth16_wrapper(format!("{}/.psy/keystore/deposit_append/", dirs::home_dir().unwrap().display())), + ); + + tracing::info!("Pre-building WithdrawalClaimWrapCircuit..."); + let withdrawal_template = WithdrawalBatchClaimCircuit::::build(32); + let withdrawal_fp = ParthQHashOut(psy_plonky2_circuits::proof_minifier::pm_core::get_circuit_fingerprint_generic( + &withdrawal_template.circuit_data.verifier_only, + )); + let withdrawal_claim_wrap_circuit = Arc::new(WithdrawalClaimWrapCircuit::new( + &withdrawal_template.circuit_data.common, + withdrawal_fp, + withdrawal_template.circuit_data.verifier_only.constants_sigmas_cap.height(), + )); + let withdrawal_claim_groth16_wrapper = Arc::new( + WithdrawalClaimWrapCircuit::new( + &withdrawal_template.circuit_data.common, + withdrawal_fp, + withdrawal_template.circuit_data.verifier_only.constants_sigmas_cap.height(), + ) + .into_shared_groth16_wrapper(format!("{}/.psy/keystore/withdrawal_claim/", dirs::home_dir().unwrap().display())), + ); + + tracing::info!("Pre-building BridgeWrapCircuit..."); + let coordinator_circuits = cached_bridge_coordinator_circuits()?; + let checkpoint_common_data: &CommonCircuitData = coordinator_circuits.checkpoint_root_transition.get_common_circuit_data_ref(); + let checkpoint_verifier_data = coordinator_circuits.checkpoint_root_transition.get_verifier_config_ref(); + let checkpoint_cap_height = checkpoint_verifier_data.constants_sigmas_cap.height(); + let coordinator_checkpoint_fp = coordinator_circuits.checkpoint_root_transition.get_fingerprint(); + // step_commit must use the cached library fingerprint (same as RCP circuit + // genesis proving), NOT base_fingerprint or minifier get_fingerprint(). + let cached_lib = psy_plonky2_circuits::generated::cached_circuit_library::get_cached_circuit_library::(); + let coordinator_checkpoint_step_commit_fp = cached_lib + .get_fingerprint(ProvingJobCircuitType::GenerateRollupStateTransitionProof) + .expect("GenerateRollupStateTransitionProof not found in cached circuit library"); + + tracing::info!( + "[PROXY] checkpoint minifier_fp={:?} step_commit_fp(cached)={:?}", + coordinator_checkpoint_fp.0.elements, + coordinator_checkpoint_step_commit_fp.0.elements, + ); + + let bridge_agg_template = BridgeAggFinalCircuit::::prebuild_final_circuit( + checkpoint_common_data, + checkpoint_cap_height, + coordinator_checkpoint_fp, + coordinator_checkpoint_step_commit_fp, + 32, + PsyNetworkLocalDevnetConstants::GLOBAL_USER_TREE_HEIGHT_USIZE, + PsyNetworkLocalDevnetConstants::GLOBAL_CONTRACT_TREE_HEIGHT_USIZE, + DEPOSIT_TREE_CONTRACT_STATE_TREE_HEIGHT as usize, + WITHDRAWAL_TREE_CONTRACT_STATE_TREE_HEIGHT as usize, + ); + let bridge_agg_fingerprint = bridge_agg_template.get_fingerprint(); + let bridge_agg_common = bridge_agg_template.get_common_circuit_data_ref(); + let bridge_agg_verifier = bridge_agg_template.get_verifier_config_ref(); + let bridge_wrap_circuit = Arc::new(BridgeWrapCircuit::new( + bridge_agg_common, + bridge_agg_fingerprint, + bridge_agg_verifier.constants_sigmas_cap.height(), + )); + let bridge_groth16_wrapper = Arc::new( + BridgeWrapCircuit::new( + bridge_agg_common, + bridge_agg_fingerprint, + bridge_agg_verifier.constants_sigmas_cap.height(), + ) + .into_shared_groth16_wrapper(format!("{}/.psy/keystore/", dirs::home_dir().unwrap().display())), + ); + + tracing::info!("Groth16 wrapping circuits pre-built successfully."); + + // Preload Groth16 keystores into the gnark Go runtime so the first proof + // request doesn't pay the ~15s cold-start penalty (ReadCircuit + + // ReadProvingKey). Each keystore is ~500MB–800MB on disk; loading + // lazily on first request causes relayer claim-proof-fetch timeouts. + tracing::info!("Preloading Groth16 keystores..."); + for (label, keystore_path) in [ + ("bridge", &bridge_groth16_wrapper.keystore_path), + ("deposit_append", &deposit_batch_groth16_wrapper.keystore_path), + ("withdrawal_claim", &withdrawal_claim_groth16_wrapper.keystore_path), + ] { + let keystore_dir = std::path::Path::new(keystore_path); + if keystore_dir.join("circuit_groth16.bin").exists() + && keystore_dir.join("pk_groth16.bin").exists() + && keystore_dir.join("vk_groth16.bin").exists() + { + tracing::info!(keystore = label, path = keystore_path, "preloading Groth16 setup"); + gnark_plonky2_verifier_ffi::initialize(keystore_path); + tracing::info!(keystore = label, "Groth16 setup preloaded"); + } else { + tracing::warn!(keystore = label, path = keystore_path, "skipping preload — keystore files missing"); + } + } + tracing::info!("All Groth16 keystores preloaded."); + + Ok(Self { + deposit_batch_wrap_circuit, + withdrawal_claim_wrap_circuit, + bridge_wrap_circuit, + deposit_batch_groth16_wrapper, + withdrawal_claim_groth16_wrapper, + bridge_groth16_wrapper, + }) + } +} + +fn cached_bridge_coordinator_circuits() -> anyhow::Result<&'static QEDCoordinatorCircuitManager> { + static CACHE: OnceLock>> = OnceLock::new(); + CACHE + .get_or_init(|| { + tracing::info!("Building QEDCoordinatorCircuitManager for bridge agg..."); + get_plonky2_circuit_library_and_prover_for_network::(PsyChainNetworkType::LocalDevnet).map(|(_, circuits)| circuits) + }) + .as_ref() + .map_err(|e| anyhow::anyhow!("failed to build/retrieve cached bridge circuits: {}", e)) +} + +#[async_trait] +impl ProveProxySystemRpcServer for SystemProveProvider { + async fn prove_withdrawal_batch_claim_groth16( + &self, + input: BridgeWithdrawalBatchWitnessInput, + ) -> Result { + tracing::debug!("prove_withdrawal_batch_claim_groth16 count={}", input.withdrawals.len()); + + let wrap_circuit = self.withdrawal_claim_wrap_circuit.clone(); + let groth16_wrapper = self.withdrawal_claim_groth16_wrapper.clone(); + tokio::task::spawn_blocking(move || { + anyhow::ensure!( + input.withdrawals.len() <= MAX_WITHDRAWAL_CLAIM_BATCH_SIZE, + "withdrawal batch too large: got {}, max {}", + input.withdrawals.len(), + MAX_WITHDRAWAL_CLAIM_BATCH_SIZE + ); + anyhow::ensure!(!input.withdrawals.is_empty(), "withdrawal batch must include at least one withdrawal"); + + let mut slot_data = vec![0u64; MAX_WITHDRAWAL_CLAIM_BATCH_SIZE * WITHDRAWAL_BATCH_CLAIM_SLOT_WORDS]; + let mut root: Option> = None; + let mut withdrawals = Vec::with_capacity(input.withdrawals.len()); + for (i, withdrawal) in input.withdrawals.iter().enumerate() { + anyhow::ensure!( + withdrawal.siblings.len() == 32, + "withdrawal[{}] expected 32 siblings, got {}", + i, + withdrawal.siblings.len() + ); + let parsed_root = parse_hex_qhashout(&withdrawal.withdrawal_root)?; + if let Some(existing) = root { + anyhow::ensure!(existing == parsed_root, "withdrawal[{}] root mismatch within batch", i); + } else { + root = Some(parsed_root); + } + let siblings = withdrawal + .siblings + .iter() + .map(|hex| parse_hex_qhashout(hex)) + .collect::>>()?; + let slot_offset = i * WITHDRAWAL_BATCH_CLAIM_SLOT_WORDS; + slot_data[slot_offset] = withdrawal.sender_user_id as u64; + for (j, word) in withdrawal.recipient.iter().enumerate() { + slot_data[slot_offset + 1 + j] = *word as u64; + } + for (j, word) in withdrawal.token.iter().enumerate() { + slot_data[slot_offset + 9 + j] = *word as u64; + } + for (j, word) in withdrawal.amount.iter().enumerate() { + slot_data[slot_offset + 17 + j] = *word as u64; + } + for (j, word) in withdrawal.nonce.iter().enumerate() { + slot_data[slot_offset + 25 + j] = *word as u64; + } + slot_data[slot_offset + 33] = withdrawal.destination_chain_index as u64; + withdrawals.push(WithdrawalBatchClaimSlotInputs:: { + sender_user_id: withdrawal.sender_user_id, + recipient: withdrawal.recipient, + token: withdrawal.token, + amount: withdrawal.amount, + nonce: withdrawal.nonce, + destination_chain_index: withdrawal.destination_chain_index, + leaf_index: withdrawal.leaf_index, + siblings, + }); + } + + let circuit = WithdrawalBatchClaimCircuit::::build(32); + let proof = circuit.generate_proof(&WithdrawalBatchClaimInputs:: { + withdrawal_root: root.expect("non-empty batch ensured above"), + bridge_user_id: input.bridge_user_id, + withdrawals, + })?; + let groth16 = wrap_circuit.prove_groth16_with_shared_wrapper(&groth16_wrapper, &circuit.circuit_data.verifier_only, &proof)?; + tracing::warn!( + withdrawal_claim_gnark_public_inputs = ?groth16.public_inputs, + "withdrawal claim gnark returned public inputs" + ); + + Ok::<_, anyhow::Error>(BridgeWithdrawalBatchGroth16Proof { + solidity_proof: g16_proof_to_solidity_words(&groth16), + public_inputs: { + let pis = proof.public_inputs.iter().map(|x| x.to_noncanonical_u64()).collect::>(); + anyhow::ensure!( + pis.len() == WITHDRAWAL_BATCH_CLAIM_PUBLIC_INPUTS_WORDS, + "expected {} withdrawal batch public inputs, got {}", + WITHDRAWAL_BATCH_CLAIM_PUBLIC_INPUTS_WORDS, + pis.len() + ); + pis + }, + slot_data, + }) + }) + .await + .map_err(|join_err| { + ErrorObjectOwned::owned( + 1, + "prove_withdrawal_batch_claim_groth16: task schedule failed", + Some(format!("Thread pool task execution failed: {}", join_err)), + ) + })? + .map_err(|err| ErrorObjectOwned::owned(1, "prove_withdrawal_batch_claim_groth16 proving error", Some(err.to_string()))) + } + + async fn prove_deposit_batch_append_groth16( + &self, + input: BridgeDepositBatchWitnessInput, + ) -> Result { + tracing::debug!( + "prove_deposit_batch_append_groth16 from_index={} count={}", + input.from_index, + input.deposits.len() + ); + + let wrap_circuit = self.deposit_batch_wrap_circuit.clone(); + let groth16_wrapper = self.deposit_batch_groth16_wrapper.clone(); + tokio::task::spawn_blocking(move || { + anyhow::ensure!( + input.old_frontier.len() == 32, + "expected 32 frontier nodes, got {}", + input.old_frontier.len() + ); + anyhow::ensure!(!input.deposits.is_empty(), "deposit batch must include at least one deposit"); + + let old_frontier_vec = input + .old_frontier + .iter() + .map(|hex| parse_hex_qhashout(hex)) + .collect::>>()?; + let old_frontier: [ParthQHashOut; 32] = old_frontier_vec + .try_into() + .map_err(|v: Vec>| anyhow::anyhow!("invalid frontier length: {}", v.len()))?; + let deposits = input + .deposits + .into_iter() + .map(|leaf| DepositBatchLeafData { + shield_address: leaf.shield_address, + token: leaf.token, + l2_token_contract_id: leaf.l2_token_contract_id, + amount: leaf.amount, + chain_index: leaf.chain_index, + note_commitment: leaf.note_commitment, + }) + .collect::>(); + let batch_inputs = DepositBatchAppendInputs { + frontier: old_frontier, + from_index: input.from_index, + deposits, + bridge_user_id: input.bridge_user_id, + }; + + let circuit = DepositBatchAppendCircuit::::build( + psy_plonky2_common_circuits::bridge::deposit_batch_append_circuit::MAX_DEPOSIT_BATCH_SIZE, + 32, + ); + let proof = circuit.generate_proof(&batch_inputs)?; + let preimage = compute_batch_append_preimage(&batch_inputs); + let minifier = psy_plonky2_circuits::proof_minifier::pm_chain::QEDProofMinifierChain::::new( + &circuit.circuit_data.verifier_only, + &circuit.circuit_data.common, + 2, + ); + let minified_proof = minifier.prove(&proof)?; + let groth16 = wrap_circuit.prove_groth16_with_shared_wrapper(&groth16_wrapper, minifier.get_verifier_data(), &minified_proof)?; + + Ok::<_, anyhow::Error>(BridgeDepositBatchGroth16Proof { + solidity_proof: g16_proof_to_solidity_words(&groth16), + public_inputs: preimage.to_u32_words().into_iter().map(|x| x as u64).collect(), + }) + }) + .await + .map_err(|join_err| { + ErrorObjectOwned::owned( + 1, + "prove_deposit_batch_append_groth16: task schedule failed", + Some(format!("Thread pool task execution failed: {}", join_err)), + ) + })? + .map_err(|err| ErrorObjectOwned::owned(1, "prove_deposit_batch_append_groth16 proving error", Some(err.to_string()))) + } + + async fn prove_bridge_agg_groth16( + &self, + _deps_network: String, + input: BridgeAggWitnessInput, + ) -> Result { + tracing::debug!("prove_bridge_agg_groth16 from={} to={}", input.from_checkpoint, input.to_checkpoint); + + let from_checkpoint = input.from_checkpoint.max(1); + let to_checkpoint = input.to_checkpoint; + if from_checkpoint > to_checkpoint { + return Err(ErrorObjectOwned::owned( + 1, + "prove_bridge_agg_groth16: from_checkpoint must be <= to_checkpoint", + None::<()>, + )); + } + let num_checkpoints_aggregated = to_checkpoint - from_checkpoint + 1; + + let wrap_circuit = self.bridge_wrap_circuit.clone(); + let groth16_wrapper = self.bridge_groth16_wrapper.clone(); + tokio::task::spawn_blocking(move || -> Result { + use psy_plonky2_circuits::qstandard::QStandardCircuit; + let coordinator_circuits = cached_bridge_coordinator_circuits() + .map_err(|e| ErrorObjectOwned::owned(1, "failed to load bridge circuits", Some(e.to_string())))?; + + let checkpoint_common_data: &CommonCircuitData = + coordinator_circuits.checkpoint_root_transition.get_common_circuit_data_ref(); + let checkpoint_verifier_data = + coordinator_circuits.checkpoint_root_transition.get_verifier_config_ref(); + let cap_height = checkpoint_verifier_data.constants_sigmas_cap.height(); + let coordinator_checkpoint_fp = + coordinator_circuits.checkpoint_root_transition.get_fingerprint(); + let checkpoint_state_transition_fingerprint = parse_hex_qhashout_to_qhash(&input.checkpoint_fp) + .map_err(|e| ErrorObjectOwned::owned(1, "parse checkpoint_fp", Some(e.to_string())))?; + if checkpoint_state_transition_fingerprint != coordinator_checkpoint_fp { + return Err(ErrorObjectOwned::owned( + 1, + "checkpoint_fp mismatch", + Some(format!( + "bridge agg witness checkpoint_fp differs from proxy coordinator fingerprint: input={:?} coordinator={:?}", + checkpoint_state_transition_fingerprint, + coordinator_checkpoint_fp + )), + )); + } + // step_commit must use the cached library fingerprint (same as RCP circuit genesis proving). + let cached_lib = psy_plonky2_circuits::generated::cached_circuit_library::get_cached_circuit_library::(); + let checkpoint_step_commit_fingerprint = cached_lib + .get_fingerprint(ProvingJobCircuitType::GenerateRollupStateTransitionProof) + .expect("GenerateRollupStateTransitionProof not found in cached circuit library"); + + // Deserialize the final (to_checkpoint) checkpoint proof from bincode hex + let final_checkpoint_proof_bytes = hex::decode( + input.final_checkpoint_proof_hex.trim_start_matches("0x"), + ) + .map_err(|e| ErrorObjectOwned::owned(1, "hex decode final checkpoint proof", Some(e.to_string())))?; + let final_checkpoint_proof: ProofWithPublicInputs = + bincode::deserialize(&final_checkpoint_proof_bytes) + .map_err(|e| ErrorObjectOwned::owned(1, "bincode deserialize final checkpoint proof", Some(e.to_string())))?; + + // Parse delta merkle proofs + use plonky2::hash::poseidon::PoseidonHash; + let parse_delta = |dp: &BridgeAggDeltaProof| -> anyhow::Result>> { + let new_value = parse_hex_qhashout_to_qhash(&dp.new_value)?; + let siblings = dp.siblings.iter().map(|s| parse_hex_qhashout_to_qhash(s)).collect::, _>>()?; + Ok(ParthDeltaMerkleProofCore::from_params::( + dp.index, + parth_core::pgoldilocks::QHashOut::default(), + new_value, + siblings, + )) + }; + + let delta_merkle_proofs: Vec>> = input.delta_merkle_proofs + .iter() + .map(parse_delta) + .collect::>>() + .map_err(|e| ErrorObjectOwned::owned(1, "parse delta proofs", Some(e.to_string())))?; + + let pre_delta_merkle_proofs: Vec>> = input.pre_delta_merkle_proofs + .iter() + .map(parse_delta) + .collect::>>() + .map_err(|e| ErrorObjectOwned::owned(1, "parse pre-delta proofs", Some(e.to_string())))?; + + // `chain_start` is the chain hash immediately before the aggregated range + // (chain hash of checkpoint `from_checkpoint - 1`; for `from_checkpoint <= 1` + // this is the genesis checkpoint state transition hash). + let start_chain_hash = parse_hex_qhashout_to_qhash(&input.chain_start) + .map_err(|e| ErrorObjectOwned::owned(1, "parse chain_start", Some(e.to_string())))?; + + let final_leaf = psy_data::v1::qdata::checkpoint::PQEDCheckpointLeafCompact { + global_chain_root: parse_hex_qhashout_to_qhash(&input.final_checkpoint_leaf.global_chain_root) + .map_err(|e| ErrorObjectOwned::owned(1, "parse final leaf chain root", Some(e.to_string())))?, + stats_hash: parse_hex_qhashout_to_qhash(&input.final_checkpoint_leaf.stats_hash) + .map_err(|e| ErrorObjectOwned::owned(1, "parse final leaf stats hash", Some(e.to_string())))?, + }; + + // Parse global state roots (anchors the user_tree_root to the verified checkpoint) + let parse_qhash = |hex: &str| -> anyhow::Result> { + parse_hex_qhashout_to_qhash(hex) + }; + let global_state_roots = PQEDCheckpointGlobalStateRoots { + contract_tree_root: parse_qhash(&input.final_checkpoint_global_state_roots.contract_tree_root) + .map_err(|e| ErrorObjectOwned::owned(1, "parse contract_tree_root", Some(e.to_string())))?, + deposit_tree_root: parse_qhash(&input.final_checkpoint_global_state_roots.deposit_tree_root) + .map_err(|e| ErrorObjectOwned::owned(1, "parse deposit_tree_root", Some(e.to_string())))?, + user_tree_root: parse_qhash(&input.final_checkpoint_global_state_roots.user_tree_root) + .map_err(|e| ErrorObjectOwned::owned(1, "parse user_tree_root", Some(e.to_string())))?, + withdrawal_tree_root: parse_qhash(&input.final_checkpoint_global_state_roots.withdrawal_tree_root) + .map_err(|e| ErrorObjectOwned::owned(1, "parse withdrawal_tree_root", Some(e.to_string())))?, + user_registration_tree_root: parse_qhash(&input.final_checkpoint_global_state_roots.user_registration_tree_root) + .map_err(|e| ErrorObjectOwned::owned(1, "parse user_registration_tree_root", Some(e.to_string())))?, + }; + + // Parse witnesses (slot witnesses are the full TreeRootInContractStateWitnessInput) + let parse_slot_witness = |w: &BridgeAggSlotWitness| -> anyhow::Result> { + let user_leaf = psy_data::v1::qdata::user::PQEDUserLeaf::> { + public_key: parse_hex_qhashout_to_qhash(&w.user_leaf_public_key)?, + user_state_tree_root: parse_hex_qhashout_to_qhash(&w.user_leaf_user_state_tree_root)?, + balance: F::from_canonical_u64(w.user_leaf_balance), + nonce: F::from_canonical_u64(w.user_leaf_nonce), + last_checkpoint_id: F::from_canonical_u64(w.user_leaf_last_checkpoint_id), + event_index: F::from_canonical_u64(w.user_leaf_event_index), + user_id: F::from_canonical_u64(w.user_leaf_user_id), + }; + + let mk_proof = |root: &str, value: &str, index: u64, sibs: &[String]| -> anyhow::Result>> { + Ok(parth_core::crypto::hash::merkle_proof::MerkleProofCore { + root: parse_hex_qhashout_to_qhash(root)?, + value: parse_hex_qhashout_to_qhash(value)?, + index, + siblings: sibs.iter().map(|s| parse_hex_qhashout_to_qhash(s)).collect::, _>>()?, + }) + }; + + Ok(TreeRootInContractStateWitnessInput { + owner_user_id: w.owner_user_id, + contract_id: w.contract_id, + user_leaf, + slot0_proof: mk_proof(&w.slot0_root, &w.slot0_value, w.slot0_index, &w.slot0_siblings)?, + slot1_proof: mk_proof(&w.slot1_root, &w.slot1_value, w.slot1_index, &w.slot1_siblings)?, + contract_proof: mk_proof(&w.contract_root, &w.contract_value, w.contract_index, &w.contract_siblings)?, + user_tree_proof: mk_proof(&w.user_tree_root, &w.user_tree_value, w.user_tree_index, &w.user_tree_siblings)?, + }) + }; + + let deposit_witness = parse_slot_witness(&input.deposit_witness) + .map_err(|e| ErrorObjectOwned::owned(1, "parse deposit witness", Some(e.to_string())))?; + let withdrawal_witness = parse_slot_witness(&input.withdrawal_witness) + .map_err(|e| ErrorObjectOwned::owned(1, "parse withdrawal witness", Some(e.to_string())))?; + + tracing::info!( + "Proving bridge aggregation for checkpoints {} to {}...", + from_checkpoint, + to_checkpoint + ); + + let result = BridgeAggFinalCircuit::::prove_range( + from_checkpoint, + to_checkpoint, + start_chain_hash, + checkpoint_common_data, + cap_height, + checkpoint_state_transition_fingerprint, + checkpoint_step_commit_fingerprint, + &final_checkpoint_proof, + &checkpoint_verifier_data, + &delta_merkle_proofs, + &pre_delta_merkle_proofs, + &final_leaf, + &global_state_roots, + &deposit_witness, + &withdrawal_witness, + 32, // CHECKPOINT_TREE_HEIGHT + PsyNetworkLocalDevnetConstants::GLOBAL_USER_TREE_HEIGHT_USIZE, + PsyNetworkLocalDevnetConstants::GLOBAL_CONTRACT_TREE_HEIGHT_USIZE, + DEPOSIT_TREE_CONTRACT_STATE_TREE_HEIGHT as usize, + WITHDRAWAL_TREE_CONTRACT_STATE_TREE_HEIGHT as usize, + ) + .map_err(|e| ErrorObjectOwned::owned(1, "bridge_agg prove_range failed", Some(e.to_string())))?; + + let bridge_agg_proof = result.proof; + let bridge_agg_verifier_data = result.verifier_data; + + tracing::info!("Proving BridgeWrapCircuit (Groth16 wrap)..."); + let groth16_proof = wrap_circuit + .prove_groth16_with_shared_wrapper(&groth16_wrapper, &bridge_agg_verifier_data, &bridge_agg_proof) + .map_err(|e| ErrorObjectOwned::owned(1, "bridge_wrap Groth16 failed", Some(e.to_string())))?; + + // Format outputs + let groth16_pi = &bridge_agg_proof.public_inputs; + let checkpoint_roots = vec![ + felt4_to_bytes32_hex(&groth16_pi[0..4]), + felt4_to_bytes32_hex(&groth16_pi[20..24]), + ]; + let deposit_tree_root = u32x8_to_bytes32_hex(&groth16_pi[4..12]); + let withdrawal_tree_root = u32x8_to_bytes32_hex(&groth16_pi[12..20]); + let end_checkpoint_index = groth16_pi[24].to_canonical_u64(); + if end_checkpoint_index != to_checkpoint { + return Err(ErrorObjectOwned::owned( + 1, + "prove_bridge_agg_groth16: end_checkpoint_index mismatch", + Some(format!("pi={} expected={}", end_checkpoint_index, to_checkpoint)), + )); + } + + let solidity_words = g16_proof_to_solidity_words(&groth16_proof); + let pub_inputs_0 = groth16_proof.public_inputs[0].clone(); + let pub_inputs_1 = groth16_proof.public_inputs[1].clone(); + let public_inputs_str: Vec = groth16_pi.iter().map(|x| x.to_canonical_u64().to_string()).collect(); + let num_pis = groth16_pi.len(); + + Ok(BridgeAggGroth16Output { + from_checkpoint, + to_checkpoint, + num_checkpoints_aggregated, + bridge_agg_public_inputs_count: num_pis, + bridge_agg_public_inputs: public_inputs_str, + groth16_proof, + solidity_proof: [ + solidity_words[0].clone(), + solidity_words[1].clone(), + solidity_words[2].clone(), + solidity_words[3].clone(), + solidity_words[4].clone(), + solidity_words[5].clone(), + solidity_words[6].clone(), + solidity_words[7].clone(), + ], + solidity_public_inputs: [ + pub_inputs_0, + pub_inputs_1, + ], + checkpoint_roots, + deposit_tree_root, + withdrawal_tree_root, + end_checkpoint_index, + }) + }) + .await + .map_err(|join_err| { + ErrorObjectOwned::owned( + 1, + "prove_bridge_agg_groth16: task schedule failed", + Some(format!("Thread pool task execution failed: {}", join_err)), + ) + })? + } +} diff --git a/client_prover/psy_prover/src/local/native/prove_proxy/types.rs b/client_prover/psy_prover/src/local/native/prove_proxy/types.rs new file mode 100644 index 00000000..bd1e263a --- /dev/null +++ b/client_prover/psy_prover/src/local/native/prove_proxy/types.rs @@ -0,0 +1,210 @@ +use parth_core::pgoldilocks::QHashOut as ParthQHashOut; +use plonky2::{ + field::types::{Field, PrimeField64}, + hash::hash_types::HashOut, +}; +use psy_plonky2_circuits::bridge::circuits::bridge_wrap::UncompressedGroth16ProofData; + +use super::F; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct BridgeWithdrawalWitnessInput { + pub withdrawal_root: String, + pub sender_user_id: u32, + pub recipient: [u32; 8], + pub token: [u32; 8], + pub amount: [u32; 8], + pub nonce: [u32; 8], + pub destination_chain_index: u32, + pub leaf_index: u32, + pub bridge_user_id: u32, + pub siblings: Vec, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct BridgeWithdrawalBatchWitnessInput { + pub bridge_user_id: u32, + pub withdrawals: Vec, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct BridgeWithdrawalBatchGroth16Proof { + pub solidity_proof: [String; 8], + pub public_inputs: Vec, + pub slot_data: Vec, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct BridgeDepositLeafInput { + pub shield_address: [u32; 8], + pub token: [u32; 8], + pub l2_token_contract_id: [u32; 8], + pub amount: [u32; 8], + pub chain_index: u32, + pub note_commitment: [u32; 8], +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct BridgeDepositBatchWitnessInput { + pub from_index: u32, + pub bridge_user_id: u32, + pub old_frontier: Vec, + pub deposits: Vec, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct BridgeDepositBatchGroth16Proof { + pub solidity_proof: [String; 8], + pub public_inputs: Vec, +} + +pub fn parse_hex_qhashout(hex: &str) -> anyhow::Result> { + let hex = hex.trim_start_matches("0x"); + anyhow::ensure!(hex.len() == 64, "expected 64 hex chars, got {}", hex.len()); + let bytes = hex::decode(hex)?; + let mut elems = [0u64; 4]; + for i in 0..4 { + let reverse_i = 3 - i; + let hi = u32::from_be_bytes(bytes[reverse_i * 8..reverse_i * 8 + 4].try_into()?); + let lo = u32::from_be_bytes(bytes[reverse_i * 8 + 4..reverse_i * 8 + 8].try_into()?); + elems[i] = ((hi as u64) << 32) | (lo as u64); + } + Ok(ParthQHashOut(HashOut { + elements: elems.map(F::from_canonical_u64), + })) +} + +// ── Bridge Aggregation Types ───────────────────────────────────────────── + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct BridgeAggCheckpointLeaf { + pub global_chain_root: String, + pub stats_hash: String, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct BridgeAggGlobalStateRoots { + pub contract_tree_root: String, + pub deposit_tree_root: String, + pub user_tree_root: String, + pub withdrawal_tree_root: String, + pub user_registration_tree_root: String, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct BridgeAggSlotWitness { + pub owner_user_id: u64, + pub contract_id: u64, + pub user_leaf_public_key: String, + pub user_leaf_user_state_tree_root: String, + pub user_leaf_balance: u64, + pub user_leaf_nonce: u64, + pub user_leaf_last_checkpoint_id: u64, + pub user_leaf_event_index: u64, + pub user_leaf_user_id: u64, + pub slot0_root: String, + pub slot0_value: String, + pub slot0_index: u64, + pub slot0_siblings: Vec, + pub slot1_root: String, + pub slot1_value: String, + pub slot1_index: u64, + pub slot1_siblings: Vec, + pub contract_root: String, + pub contract_value: String, + pub contract_index: u64, + pub contract_siblings: Vec, + pub user_tree_root: String, + pub user_tree_value: String, + pub user_tree_index: u64, + pub user_tree_siblings: Vec, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct BridgeAggDeltaProof { + pub index: u64, + pub new_value: String, + pub siblings: Vec, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct BridgeAggWitnessInput { + pub from_checkpoint: u64, + pub to_checkpoint: u64, + /// Bincode-serialized ProofWithPublicInputs for the final (to_checkpoint) + /// checkpoint state transition proof, hex-encoded. + pub final_checkpoint_proof_hex: String, + pub delta_merkle_proofs: Vec, + pub pre_delta_merkle_proofs: Vec, + /// Chain hash immediately before the aggregated range (chain hash of + /// checkpoint `from_checkpoint - 1`; for `from_checkpoint <= 1` this is the + /// genesis checkpoint state transition hash). + pub chain_start: String, + /// Checkpoint state transition circuit fingerprint (hex). + /// Must match the fingerprint the coordinator used when generating + /// checkpoint proofs. + pub checkpoint_fp: String, + pub final_checkpoint_leaf: BridgeAggCheckpointLeaf, + pub final_checkpoint_global_state_roots: BridgeAggGlobalStateRoots, + pub deposit_witness: BridgeAggSlotWitness, + pub withdrawal_witness: BridgeAggSlotWitness, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct BridgeAggGroth16Output { + pub from_checkpoint: u64, + pub to_checkpoint: u64, + pub num_checkpoints_aggregated: u64, + pub bridge_agg_public_inputs_count: usize, + pub bridge_agg_public_inputs: Vec, + pub groth16_proof: UncompressedGroth16ProofData, + pub solidity_proof: [String; 8], + pub solidity_public_inputs: [String; 2], + pub checkpoint_roots: Vec, + pub deposit_tree_root: String, + pub withdrawal_tree_root: String, + pub end_checkpoint_index: u64, +} + +pub fn g16_proof_to_solidity_words(groth16: &UncompressedGroth16ProofData) -> [String; 8] { + let with_0x = |s: &str| -> String { + if s.starts_with("0x") { + s.to_string() + } else { + format!("0x{}", s) + } + }; + [ + with_0x(&groth16.pi_a[0]), + with_0x(&groth16.pi_a[1]), + with_0x(&groth16.pi_b[0][1]), + with_0x(&groth16.pi_b[0][0]), + with_0x(&groth16.pi_b[1][1]), + with_0x(&groth16.pi_b[1][0]), + with_0x(&groth16.pi_c[0]), + with_0x(&groth16.pi_c[1]), + ] +} + +pub fn parse_hex_qhashout_to_qhash(h: &str) -> anyhow::Result> { + let pq = parse_hex_qhashout(h)?; + Ok(parth_core::pgoldilocks::QHashOut(pq.0)) +} + +pub fn felt4_to_bytes32_hex(felts: &[F]) -> String { + let mut out = [0u8; 32]; + for i in 0..4 { + let v = felts[3 - i].to_canonical_u64(); + out[i * 8..(i + 1) * 8].copy_from_slice(&v.to_be_bytes()); + } + format!("0x{}", hex::encode(out)) +} + +pub fn u32x8_to_bytes32_hex(felts: &[F]) -> String { + let mut out = [0u8; 32]; + for i in 0..8 { + let v = felts[i].to_canonical_u64() as u32; + out[i * 4..(i + 1) * 4].copy_from_slice(&v.to_be_bytes()); + } + format!("0x{}", hex::encode(out)) +} diff --git a/client_prover/psy_prover/src/local/native/prove_proxy.rs b/client_prover/psy_prover/src/local/native/prove_proxy/user.rs similarity index 52% rename from client_prover/psy_prover/src/local/native/prove_proxy.rs rename to client_prover/psy_prover/src/local/native/prove_proxy/user.rs index 143d8412..15326b0b 100644 --- a/client_prover/psy_prover/src/local/native/prove_proxy.rs +++ b/client_prover/psy_prover/src/local/native/prove_proxy/user.rs @@ -1,26 +1,10 @@ -use std::{ - path::PathBuf, - sync::{Arc, OnceLock}, -}; +use std::sync::Arc; use jsonrpsee::{ core::async_trait, proc_macros::rpc, types::{ErrorObject, ErrorObjectOwned}, }; -use parth_core::{ - crypto::hash::merkle_proof::DeltaMerkleProofCore as ParthDeltaMerkleProofCore, pgoldilocks::QHashOut as ParthQHashOut, - protocol::core_types::QNetworkTreeConstants, -}; -use plonky2::{ - field::types::{Field, PrimeField64}, - hash::hash_types::HashOut, - plonk::{ - circuit_data::CommonCircuitData, - config::{GenericConfig, PoseidonGoldilocksConfig}, - proof::ProofWithPublicInputs, - }, -}; use psy_client_common::data::{alt::AltVerifierOnlyCircuitData, qhashout::QHashOut}; use psy_client_data::{ qdata::contract::ContractCodeDefinition, @@ -36,41 +20,11 @@ use psy_client_data::{ }, }; use psy_common_circuit::circuits::traits::qstandard::QStandardCircuit; -use psy_config::network_constants::{ - DEPOSIT_TREE_CONTRACT_STATE_TREE_HEIGHT, - WITHDRAWAL_TREE_CONTRACT_STATE_TREE_HEIGHT, -}; -use psy_core::{constants::chain_id::PsyChainNetworkType, job::job_id::ProvingJobCircuitType, network_config::PsyNetworkLocalDevnetConstants}; use psy_crypto::{ common::witnesses::qrecursion::{header::QRecursionAggStandardHeader, proof_data::QStandardBinaryTreeCircuitType}, hash::merkle::core::{DeltaMerkleProofCore, MerkleProofCore}, signature::secp256k1::core::PsyCompressedSecp256K1Signature, }; -use psy_data::v1::qdata::checkpoint::PQEDCheckpointGlobalStateRoots; -use psy_plonky2_basic_helpers::verifier::circuit_library::CircuitInfoLibraryCore; -use psy_plonky2_circuits::{ - bridge::{ - circuits::{ - bridge_agg_final::BridgeAggFinalCircuit, - bridge_wrap::{ - BridgeWrapCircuit, DepositBatchWrapCircuit, SharedGroth16Wrapper, UncompressedGroth16ProofData, WithdrawalClaimWrapCircuit, - }, - }, - gadgets::tree_root_in_contract_state::TreeRootInContractStateWitnessInput, - }, - circuit_library::get_plonky2_circuit_library_and_prover_for_network, - coordinator::coordinator_helper::QEDCoordinatorCircuitManager, -}; -use psy_plonky2_common_circuits::bridge::{ - deposit_batch_append_circuit::{ - compute_batch_append_preimage, BatchAppendInputs as DepositBatchAppendInputs, DepositBatchAppendCircuit, - DepositLeafData as DepositBatchLeafData, MAX_DEPOSIT_BATCH_SIZE, - }, - withdrawal_batch_claim_circuit::{ - WithdrawalBatchClaimCircuit, WithdrawalBatchClaimInputs, WithdrawalBatchClaimSlotInputs, MAX_WITHDRAWAL_CLAIM_BATCH_SIZE, - WITHDRAWAL_BATCH_CLAIM_PUBLIC_INPUTS_WORDS, WITHDRAWAL_BATCH_CLAIM_SLOT_WORDS, - }, -}; use psy_provider::{ provider::{LocalCommonCircuitsData, QCommonCircuitData, RpcProvider}, request::{DPNSoftwareDefinedSignatureInput, QRegisterDPNSoftwareDefinedCircuitRPCRequest, QRegisterPlonky2SoftwareDefinedCircuitRPCRequest}, @@ -80,212 +34,13 @@ use psy_vm::{ ups::{circuit_manager::UPSCircuitManager, signature::Plonky2SoftwareDefinedSignatureInput}, vm::cfc_input::DapenContractFunctionCircuitInput, }; +use plonky2::plonk::proof::ProofWithPublicInputs; +use super::{C, D, F}; use crate::local::native::DPNFunctionCircuitDefinition; -type C = PoseidonGoldilocksConfig; -type F = >::F; -const D: usize = 2; -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct BridgeWithdrawalWitnessInput { - pub withdrawal_root: String, - pub sender_user_id: u32, - pub recipient: [u32; 8], - pub token: [u32; 8], - pub amount: [u32; 8], - pub nonce: [u32; 8], - pub destination_chain_index: u32, - pub leaf_index: u32, - pub bridge_user_id: u32, - pub siblings: Vec, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct BridgeWithdrawalBatchWitnessInput { - pub bridge_user_id: u32, - pub withdrawals: Vec, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct BridgeWithdrawalBatchGroth16Proof { - pub solidity_proof: [String; 8], - pub public_inputs: Vec, - pub slot_data: Vec, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct BridgeDepositLeafInput { - pub shield_address: [u32; 8], - pub token: [u32; 8], - pub l2_token_contract_id: [u32; 8], - pub amount: [u32; 8], - pub chain_index: u32, - pub note_commitment: [u32; 8], -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct BridgeDepositBatchWitnessInput { - pub from_index: u32, - pub bridge_user_id: u32, - pub old_frontier: Vec, - pub deposits: Vec, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct BridgeDepositBatchGroth16Proof { - pub solidity_proof: [String; 8], - pub public_inputs: Vec, -} - -fn parse_hex_qhashout(hex: &str) -> anyhow::Result> { - let hex = hex.trim_start_matches("0x"); - anyhow::ensure!(hex.len() == 64, "expected 64 hex chars, got {}", hex.len()); - let bytes = hex::decode(hex)?; - let mut elems = [0u64; 4]; - for i in 0..4 { - let reverse_i = 3 - i; - let hi = u32::from_be_bytes(bytes[reverse_i * 8..reverse_i * 8 + 4].try_into()?); - let lo = u32::from_be_bytes(bytes[reverse_i * 8 + 4..reverse_i * 8 + 8].try_into()?); - elems[i] = ((hi as u64) << 32) | (lo as u64); - } - Ok(ParthQHashOut(HashOut { - elements: elems.map(F::from_canonical_u64), - })) -} - -fn parse_internal_u32x8_qhashout(hex: &str) -> anyhow::Result> { - let hex = hex.trim_start_matches("0x"); - anyhow::ensure!(hex.len() == 64, "expected 64 hex chars, got {}", hex.len()); - let bytes = hex::decode(hex)?; - let mut words = [0u32; 8]; - for i in 0..8 { - words[i] = u32::from_be_bytes(bytes[i * 4..i * 4 + 4].try_into()?); - } - let elems = [ - ((words[1] as u64) << 32) | words[0] as u64, - ((words[3] as u64) << 32) | words[2] as u64, - ((words[5] as u64) << 32) | words[4] as u64, - ((words[7] as u64) << 32) | words[6] as u64, - ]; - Ok(ParthQHashOut(HashOut { - elements: elems.map(F::from_canonical_u64), - })) -} - -// ── Bridge Aggregation Types ───────────────────────────────────────────── - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct BridgeAggCheckpointLeaf { - pub global_chain_root: String, - pub stats_hash: String, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct BridgeAggGlobalStateRoots { - pub contract_tree_root: String, - pub deposit_tree_root: String, - pub user_tree_root: String, - pub withdrawal_tree_root: String, - pub user_registration_tree_root: String, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct BridgeAggSlotWitness { - pub owner_user_id: u64, - pub contract_id: u64, - pub user_leaf_public_key: String, - pub user_leaf_user_state_tree_root: String, - pub user_leaf_balance: u64, - pub user_leaf_nonce: u64, - pub user_leaf_last_checkpoint_id: u64, - pub user_leaf_event_index: u64, - pub user_leaf_user_id: u64, - pub slot0_root: String, - pub slot0_value: String, - pub slot0_index: u64, - pub slot0_siblings: Vec, - pub slot1_root: String, - pub slot1_value: String, - pub slot1_index: u64, - pub slot1_siblings: Vec, - pub contract_root: String, - pub contract_value: String, - pub contract_index: u64, - pub contract_siblings: Vec, - pub user_tree_root: String, - pub user_tree_value: String, - pub user_tree_index: u64, - pub user_tree_siblings: Vec, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct BridgeAggDeltaProof { - pub index: u64, - pub new_value: String, - pub siblings: Vec, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct BridgeAggWitnessInput { - pub from_checkpoint: u64, - pub to_checkpoint: u64, - /// Bincode-serialized ProofWithPublicInputs for the final (to_checkpoint) - /// checkpoint state transition proof, hex-encoded. - pub final_checkpoint_proof_hex: String, - pub delta_merkle_proofs: Vec, - pub pre_delta_merkle_proofs: Vec, - /// Chain hash immediately before the aggregated range (chain hash of - /// checkpoint `from_checkpoint - 1`; for `from_checkpoint <= 1` this is the - /// genesis checkpoint state transition hash). - pub chain_start: String, - /// Checkpoint state transition circuit fingerprint (hex). - /// Must match the fingerprint the coordinator used when generating - /// checkpoint proofs. - pub checkpoint_fp: String, - pub final_checkpoint_leaf: BridgeAggCheckpointLeaf, - pub final_checkpoint_global_state_roots: BridgeAggGlobalStateRoots, - pub deposit_witness: BridgeAggSlotWitness, - pub withdrawal_witness: BridgeAggSlotWitness, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct BridgeAggGroth16Output { - pub from_checkpoint: u64, - pub to_checkpoint: u64, - pub num_checkpoints_aggregated: u64, - pub bridge_agg_public_inputs_count: usize, - pub bridge_agg_public_inputs: Vec, - pub groth16_proof: UncompressedGroth16ProofData, - pub solidity_proof: [String; 8], - pub solidity_public_inputs: [String; 2], - pub checkpoint_roots: Vec, - pub deposit_tree_root: String, - pub withdrawal_tree_root: String, - pub end_checkpoint_index: u64, -} - -fn g16_proof_to_solidity_words(groth16: &UncompressedGroth16ProofData) -> [String; 8] { - let with_0x = |s: &str| -> String { - if s.starts_with("0x") { - s.to_string() - } else { - format!("0x{}", s) - } - }; - [ - with_0x(&groth16.pi_a[0]), - with_0x(&groth16.pi_a[1]), - with_0x(&groth16.pi_b[0][1]), - with_0x(&groth16.pi_b[0][0]), - with_0x(&groth16.pi_b[1][1]), - with_0x(&groth16.pi_b[1][0]), - with_0x(&groth16.pi_c[0]), - with_0x(&groth16.pi_c[1]), - ] -} - #[rpc(server, client, namespace = "psy")] -pub trait ProveProxyRpc { +pub trait ProveProxyUserRpc { /// local proving proof generate #[method(name = "prove_ups_start")] async fn prove_ups_start(&self, input: UPSStartStepInput) -> Result, ErrorObjectOwned>; @@ -474,46 +229,17 @@ pub trait ProveProxyRpc { right_verifier_data: AltVerifierOnlyCircuitData, ) -> Result, ErrorObjectOwned>; - #[method(name = "prove_withdrawal_batch_claim_groth16")] - async fn prove_withdrawal_batch_claim_groth16( - &self, - input: BridgeWithdrawalBatchWitnessInput, - ) -> Result; - - #[method(name = "prove_deposit_batch_append_groth16")] - async fn prove_deposit_batch_append_groth16( - &self, - input: BridgeDepositBatchWitnessInput, - ) -> Result; - - /// Bridge aggregation: checkpoints → BridgeAggCircuit → BridgeWrapCircuit → - /// Groth16 - #[method(name = "prove_bridge_agg_groth16")] - async fn prove_bridge_agg_groth16(&self, deps_network: String, input: BridgeAggWitnessInput) -> Result; } -pub struct ProveProxyServerProvider { +pub struct UserProveProvider { pub rpc_provider: RpcProvider, pub circuit_manager: Arc>, pub circuit_info: Arc>, pub circuits_data: LocalCommonCircuitsData, - pub keystore_dir: Option, - pub deployments_network: String, - /// Pre-built wrapping circuits shared across all prove requests. - pub deposit_batch_wrap_circuit: Arc, - pub withdrawal_claim_wrap_circuit: Arc, - pub bridge_wrap_circuit: Arc, - pub deposit_batch_groth16_wrapper: Arc, - pub withdrawal_claim_groth16_wrapper: Arc, - pub bridge_groth16_wrapper: Arc, } -impl ProveProxyServerProvider { +impl UserProveProvider { pub async fn new_with_config(rpc_config: psy_config::NetworkConfigGoldilocks, network_magic: u64) -> anyhow::Result { - use psy_client_data::qstore::controllers::session_info::SessionCircuitInfoStore; - use psy_common_circuit::circuits::traits::qstandard::QStandardCircuit; - use psy_plonky2_circuits::qstandard::QStandardCircuit as PlonkyQStandardCircuit; - let rpc_provider = RpcProvider::new_with_config(&rpc_config)?; let circuit_manager = PsyUPSStepCircuitManager::::new_with_config(network_magic); @@ -637,138 +363,12 @@ impl ProveProxyServerProvider { }), }; - // ── Pre-build Groth16 wrapping circuits (shared across all threads) ── - // These depend only on the inner circuit structure, not on runtime data. - // Building once at startup saves ~200ms per request (CircuitBuilder::new + - // builder.build). - - tracing::info!("Pre-building DepositBatchWrapCircuit..."); - let deposit_template = DepositBatchAppendCircuit::::build(MAX_DEPOSIT_BATCH_SIZE, 32); - let deposit_minifier = psy_plonky2_circuits::proof_minifier::pm_chain::QEDProofMinifierChain::::new( - &deposit_template.circuit_data.verifier_only, - &deposit_template.circuit_data.common, - 2, - ); - let deposit_fp = ParthQHashOut(deposit_minifier.get_fingerprint()); - let deposit_batch_wrap_circuit = Arc::new(DepositBatchWrapCircuit::new( - deposit_minifier.get_common_data(), - deposit_fp, - deposit_minifier.get_verifier_data().constants_sigmas_cap.height(), - )); - let deposit_batch_groth16_wrapper = Arc::new( - DepositBatchWrapCircuit::new( - deposit_minifier.get_common_data(), - deposit_fp, - deposit_minifier.get_verifier_data().constants_sigmas_cap.height(), - ) - .into_shared_groth16_wrapper(format!("{}/.psy/keystore/deposit_append/", dirs::home_dir().unwrap().display())), - ); - - tracing::info!("Pre-building WithdrawalClaimWrapCircuit..."); - let withdrawal_template = WithdrawalBatchClaimCircuit::::build(32); - let withdrawal_fp = ParthQHashOut(psy_plonky2_circuits::proof_minifier::pm_core::get_circuit_fingerprint_generic( - &withdrawal_template.circuit_data.verifier_only, - )); - let withdrawal_claim_wrap_circuit = Arc::new(WithdrawalClaimWrapCircuit::new( - &withdrawal_template.circuit_data.common, - withdrawal_fp, - withdrawal_template.circuit_data.verifier_only.constants_sigmas_cap.height(), - )); - let withdrawal_claim_groth16_wrapper = Arc::new( - WithdrawalClaimWrapCircuit::new( - &withdrawal_template.circuit_data.common, - withdrawal_fp, - withdrawal_template.circuit_data.verifier_only.constants_sigmas_cap.height(), - ) - .into_shared_groth16_wrapper(format!("{}/.psy/keystore/withdrawal_claim/", dirs::home_dir().unwrap().display())), - ); - - tracing::info!("Pre-building BridgeWrapCircuit..."); - let coordinator_circuits = cached_bridge_coordinator_circuits()?; - let checkpoint_common_data: &CommonCircuitData = coordinator_circuits.checkpoint_root_transition.get_common_circuit_data_ref(); - let checkpoint_verifier_data = coordinator_circuits.checkpoint_root_transition.get_verifier_config_ref(); - let checkpoint_cap_height = checkpoint_verifier_data.constants_sigmas_cap.height(); - let coordinator_checkpoint_fp = coordinator_circuits.checkpoint_root_transition.get_fingerprint(); - // step_commit must use the cached library fingerprint (same as RCP circuit - // genesis proving), NOT base_fingerprint or minifier get_fingerprint(). - let cached_lib = psy_plonky2_circuits::generated::cached_circuit_library::get_cached_circuit_library::(); - let coordinator_checkpoint_step_commit_fp = cached_lib - .get_fingerprint(ProvingJobCircuitType::GenerateRollupStateTransitionProof) - .expect("GenerateRollupStateTransitionProof not found in cached circuit library"); - - tracing::info!( - "[PROXY] checkpoint minifier_fp={:?} step_commit_fp(cached)={:?}", - coordinator_checkpoint_fp.0.elements, - coordinator_checkpoint_step_commit_fp.0.elements, - ); - - let bridge_agg_template = BridgeAggFinalCircuit::::prebuild_final_circuit( - checkpoint_common_data, - checkpoint_cap_height, - coordinator_checkpoint_fp, - coordinator_checkpoint_step_commit_fp, - 32, - PsyNetworkLocalDevnetConstants::GLOBAL_USER_TREE_HEIGHT_USIZE, - PsyNetworkLocalDevnetConstants::GLOBAL_CONTRACT_TREE_HEIGHT_USIZE, - DEPOSIT_TREE_CONTRACT_STATE_TREE_HEIGHT as usize, - WITHDRAWAL_TREE_CONTRACT_STATE_TREE_HEIGHT as usize, - ); - let bridge_agg_fingerprint = bridge_agg_template.get_fingerprint(); - let bridge_agg_common = bridge_agg_template.get_common_circuit_data_ref(); - let bridge_agg_verifier = bridge_agg_template.get_verifier_config_ref(); - let bridge_wrap_circuit = Arc::new(BridgeWrapCircuit::new( - bridge_agg_common, - bridge_agg_fingerprint, - bridge_agg_verifier.constants_sigmas_cap.height(), - )); - let bridge_groth16_wrapper = Arc::new( - BridgeWrapCircuit::new( - bridge_agg_common, - bridge_agg_fingerprint, - bridge_agg_verifier.constants_sigmas_cap.height(), - ) - .into_shared_groth16_wrapper(format!("{}/.psy/keystore/", dirs::home_dir().unwrap().display())), - ); - - tracing::info!("Groth16 wrapping circuits pre-built successfully."); - - // Preload Groth16 keystores into the gnark Go runtime so the first proof - // request doesn't pay the ~15s cold-start penalty (ReadCircuit + - // ReadProvingKey). Each keystore is ~500MB–800MB on disk; loading - // lazily on first request causes relayer claim-proof-fetch timeouts. - tracing::info!("Preloading Groth16 keystores..."); - for (label, keystore_path) in [ - ("bridge", &bridge_groth16_wrapper.keystore_path), - ("deposit_append", &deposit_batch_groth16_wrapper.keystore_path), - ("withdrawal_claim", &withdrawal_claim_groth16_wrapper.keystore_path), - ] { - let keystore_dir = std::path::Path::new(keystore_path); - if keystore_dir.join("circuit_groth16.bin").exists() - && keystore_dir.join("pk_groth16.bin").exists() - && keystore_dir.join("vk_groth16.bin").exists() - { - tracing::info!(keystore = label, path = keystore_path, "preloading Groth16 setup"); - gnark_plonky2_verifier_ffi::initialize(keystore_path); - tracing::info!(keystore = label, "Groth16 setup preloaded"); - } else { - tracing::warn!(keystore = label, path = keystore_path, "skipping preload — keystore files missing"); - } - } - tracing::info!("All Groth16 keystores preloaded."); Ok(Self { rpc_provider, circuit_manager: Arc::new(circuit_manager), circuit_info: Arc::new(circuit_info), circuits_data, - keystore_dir: None, - deployments_network: "localhost".to_string(), - deposit_batch_wrap_circuit, - withdrawal_claim_wrap_circuit, - bridge_wrap_circuit, - deposit_batch_groth16_wrapper, - withdrawal_claim_groth16_wrapper, - bridge_groth16_wrapper, }) } @@ -786,478 +386,8 @@ impl ProveProxyServerProvider { } } -fn cached_bridge_coordinator_circuits() -> anyhow::Result<&'static QEDCoordinatorCircuitManager> { - static CACHE: OnceLock>> = OnceLock::new(); - CACHE - .get_or_init(|| { - tracing::info!("Building QEDCoordinatorCircuitManager for bridge agg..."); - get_plonky2_circuit_library_and_prover_for_network::(PsyChainNetworkType::LocalDevnet).map(|(_, circuits)| circuits) - }) - .as_ref() - .map_err(|e| anyhow::anyhow!("failed to build/retrieve cached bridge circuits: {}", e)) -} - -fn parse_hex_qhashout_to_qhash(h: &str) -> anyhow::Result> { - let pq = parse_hex_qhashout(h)?; - Ok(parth_core::pgoldilocks::QHashOut(pq.0)) -} - -fn qhashout_from_felts(elems: &[F]) -> parth_core::pgoldilocks::QHashOut { - parth_core::pgoldilocks::QHashOut(HashOut { - elements: [elems[0], elems[1], elems[2], elems[3]], - }) -} - -fn felt4_to_bytes32_hex(felts: &[F]) -> String { - let mut out = [0u8; 32]; - for i in 0..4 { - let v = felts[3 - i].to_canonical_u64(); - out[i * 8..(i + 1) * 8].copy_from_slice(&v.to_be_bytes()); - } - format!("0x{}", hex::encode(out)) -} - -fn u32x8_to_bytes32_hex(felts: &[F]) -> String { - let mut out = [0u8; 32]; - for i in 0..8 { - let v = felts[i].to_canonical_u64() as u32; - out[i * 4..(i + 1) * 4].copy_from_slice(&v.to_be_bytes()); - } - format!("0x{}", hex::encode(out)) -} - #[async_trait] -impl ProveProxyRpcServer for ProveProxyServerProvider { - async fn prove_withdrawal_batch_claim_groth16( - &self, - input: BridgeWithdrawalBatchWitnessInput, - ) -> Result { - tracing::debug!("prove_withdrawal_batch_claim_groth16 count={}", input.withdrawals.len()); - - let wrap_circuit = self.withdrawal_claim_wrap_circuit.clone(); - let groth16_wrapper = self.withdrawal_claim_groth16_wrapper.clone(); - tokio::task::spawn_blocking(move || { - anyhow::ensure!( - input.withdrawals.len() <= MAX_WITHDRAWAL_CLAIM_BATCH_SIZE, - "withdrawal batch too large: got {}, max {}", - input.withdrawals.len(), - MAX_WITHDRAWAL_CLAIM_BATCH_SIZE - ); - anyhow::ensure!(!input.withdrawals.is_empty(), "withdrawal batch must include at least one withdrawal"); - - let mut slot_data = vec![0u64; MAX_WITHDRAWAL_CLAIM_BATCH_SIZE * WITHDRAWAL_BATCH_CLAIM_SLOT_WORDS]; - let mut root: Option> = None; - let mut withdrawals = Vec::with_capacity(input.withdrawals.len()); - for (i, withdrawal) in input.withdrawals.iter().enumerate() { - anyhow::ensure!( - withdrawal.siblings.len() == 32, - "withdrawal[{}] expected 32 siblings, got {}", - i, - withdrawal.siblings.len() - ); - let parsed_root = parse_hex_qhashout(&withdrawal.withdrawal_root)?; - if let Some(existing) = root { - anyhow::ensure!(existing == parsed_root, "withdrawal[{}] root mismatch within batch", i); - } else { - root = Some(parsed_root); - } - let siblings = withdrawal - .siblings - .iter() - .map(|hex| parse_hex_qhashout(hex)) - .collect::>>()?; - let slot_offset = i * WITHDRAWAL_BATCH_CLAIM_SLOT_WORDS; - slot_data[slot_offset] = withdrawal.sender_user_id as u64; - for (j, word) in withdrawal.recipient.iter().enumerate() { - slot_data[slot_offset + 1 + j] = *word as u64; - } - for (j, word) in withdrawal.token.iter().enumerate() { - slot_data[slot_offset + 9 + j] = *word as u64; - } - for (j, word) in withdrawal.amount.iter().enumerate() { - slot_data[slot_offset + 17 + j] = *word as u64; - } - for (j, word) in withdrawal.nonce.iter().enumerate() { - slot_data[slot_offset + 25 + j] = *word as u64; - } - slot_data[slot_offset + 33] = withdrawal.destination_chain_index as u64; - withdrawals.push(WithdrawalBatchClaimSlotInputs:: { - sender_user_id: withdrawal.sender_user_id, - recipient: withdrawal.recipient, - token: withdrawal.token, - amount: withdrawal.amount, - nonce: withdrawal.nonce, - destination_chain_index: withdrawal.destination_chain_index, - leaf_index: withdrawal.leaf_index, - siblings, - }); - } - - let circuit = WithdrawalBatchClaimCircuit::::build(32); - let proof = circuit.generate_proof(&WithdrawalBatchClaimInputs:: { - withdrawal_root: root.expect("non-empty batch ensured above"), - bridge_user_id: input.bridge_user_id, - withdrawals, - })?; - let groth16 = wrap_circuit.prove_groth16_with_shared_wrapper(&groth16_wrapper, &circuit.circuit_data.verifier_only, &proof)?; - tracing::warn!( - withdrawal_claim_gnark_public_inputs = ?groth16.public_inputs, - "withdrawal claim gnark returned public inputs" - ); - - Ok::<_, anyhow::Error>(BridgeWithdrawalBatchGroth16Proof { - solidity_proof: g16_proof_to_solidity_words(&groth16), - public_inputs: { - let pis = proof.public_inputs.iter().map(|x| x.to_noncanonical_u64()).collect::>(); - anyhow::ensure!( - pis.len() == WITHDRAWAL_BATCH_CLAIM_PUBLIC_INPUTS_WORDS, - "expected {} withdrawal batch public inputs, got {}", - WITHDRAWAL_BATCH_CLAIM_PUBLIC_INPUTS_WORDS, - pis.len() - ); - pis - }, - slot_data, - }) - }) - .await - .map_err(|join_err| { - ErrorObjectOwned::owned( - 1, - "prove_withdrawal_batch_claim_groth16: task schedule failed", - Some(format!("Thread pool task execution failed: {}", join_err)), - ) - })? - .map_err(|err| ErrorObjectOwned::owned(1, "prove_withdrawal_batch_claim_groth16 proving error", Some(err.to_string()))) - } - - async fn prove_deposit_batch_append_groth16( - &self, - input: BridgeDepositBatchWitnessInput, - ) -> Result { - tracing::debug!( - "prove_deposit_batch_append_groth16 from_index={} count={}", - input.from_index, - input.deposits.len() - ); - - let wrap_circuit = self.deposit_batch_wrap_circuit.clone(); - let groth16_wrapper = self.deposit_batch_groth16_wrapper.clone(); - tokio::task::spawn_blocking(move || { - anyhow::ensure!( - input.old_frontier.len() == 32, - "expected 32 frontier nodes, got {}", - input.old_frontier.len() - ); - anyhow::ensure!(!input.deposits.is_empty(), "deposit batch must include at least one deposit"); - - let old_frontier_vec = input - .old_frontier - .iter() - .map(|hex| parse_hex_qhashout(hex)) - .collect::>>()?; - let old_frontier: [ParthQHashOut; 32] = old_frontier_vec - .try_into() - .map_err(|v: Vec>| anyhow::anyhow!("invalid frontier length: {}", v.len()))?; - let deposits = input - .deposits - .into_iter() - .map(|leaf| DepositBatchLeafData { - shield_address: leaf.shield_address, - token: leaf.token, - l2_token_contract_id: leaf.l2_token_contract_id, - amount: leaf.amount, - chain_index: leaf.chain_index, - note_commitment: leaf.note_commitment, - }) - .collect::>(); - let batch_inputs = DepositBatchAppendInputs { - frontier: old_frontier, - from_index: input.from_index, - deposits, - bridge_user_id: input.bridge_user_id, - }; - - let circuit = DepositBatchAppendCircuit::::build( - psy_plonky2_common_circuits::bridge::deposit_batch_append_circuit::MAX_DEPOSIT_BATCH_SIZE, - 32, - ); - let proof = circuit.generate_proof(&batch_inputs)?; - let preimage = compute_batch_append_preimage(&batch_inputs); - let minifier = psy_plonky2_circuits::proof_minifier::pm_chain::QEDProofMinifierChain::::new( - &circuit.circuit_data.verifier_only, - &circuit.circuit_data.common, - 2, - ); - let minified_proof = minifier.prove(&proof)?; - let groth16 = wrap_circuit.prove_groth16_with_shared_wrapper(&groth16_wrapper, minifier.get_verifier_data(), &minified_proof)?; - - Ok::<_, anyhow::Error>(BridgeDepositBatchGroth16Proof { - solidity_proof: g16_proof_to_solidity_words(&groth16), - public_inputs: preimage.to_u32_words().into_iter().map(|x| x as u64).collect(), - }) - }) - .await - .map_err(|join_err| { - ErrorObjectOwned::owned( - 1, - "prove_deposit_batch_append_groth16: task schedule failed", - Some(format!("Thread pool task execution failed: {}", join_err)), - ) - })? - .map_err(|err| ErrorObjectOwned::owned(1, "prove_deposit_batch_append_groth16 proving error", Some(err.to_string()))) - } - - async fn prove_bridge_agg_groth16( - &self, - _deps_network: String, - input: BridgeAggWitnessInput, - ) -> Result { - tracing::debug!("prove_bridge_agg_groth16 from={} to={}", input.from_checkpoint, input.to_checkpoint); - - let from_checkpoint = input.from_checkpoint.max(1); - let to_checkpoint = input.to_checkpoint; - if from_checkpoint > to_checkpoint { - return Err(ErrorObjectOwned::owned( - 1, - "prove_bridge_agg_groth16: from_checkpoint must be <= to_checkpoint", - None::<()>, - )); - } - let num_checkpoints_aggregated = to_checkpoint - from_checkpoint + 1; - - let wrap_circuit = self.bridge_wrap_circuit.clone(); - let groth16_wrapper = self.bridge_groth16_wrapper.clone(); - tokio::task::spawn_blocking(move || -> Result { - use psy_plonky2_circuits::qstandard::QStandardCircuit; - let coordinator_circuits = cached_bridge_coordinator_circuits() - .map_err(|e| ErrorObjectOwned::owned(1, "failed to load bridge circuits", Some(e.to_string())))?; - - let checkpoint_common_data: &CommonCircuitData = - coordinator_circuits.checkpoint_root_transition.get_common_circuit_data_ref(); - let checkpoint_verifier_data = - coordinator_circuits.checkpoint_root_transition.get_verifier_config_ref(); - let cap_height = checkpoint_verifier_data.constants_sigmas_cap.height(); - let coordinator_checkpoint_fp = - coordinator_circuits.checkpoint_root_transition.get_fingerprint(); - let checkpoint_state_transition_fingerprint = parse_hex_qhashout_to_qhash(&input.checkpoint_fp) - .map_err(|e| ErrorObjectOwned::owned(1, "parse checkpoint_fp", Some(e.to_string())))?; - if checkpoint_state_transition_fingerprint != coordinator_checkpoint_fp { - return Err(ErrorObjectOwned::owned( - 1, - "checkpoint_fp mismatch", - Some(format!( - "bridge agg witness checkpoint_fp differs from proxy coordinator fingerprint: input={:?} coordinator={:?}", - checkpoint_state_transition_fingerprint, - coordinator_checkpoint_fp - )), - )); - } - // step_commit must use the cached library fingerprint (same as RCP circuit genesis proving). - let cached_lib = psy_plonky2_circuits::generated::cached_circuit_library::get_cached_circuit_library::(); - let checkpoint_step_commit_fingerprint = cached_lib - .get_fingerprint(ProvingJobCircuitType::GenerateRollupStateTransitionProof) - .expect("GenerateRollupStateTransitionProof not found in cached circuit library"); - - // Deserialize the final (to_checkpoint) checkpoint proof from bincode hex - let final_checkpoint_proof_bytes = hex::decode( - input.final_checkpoint_proof_hex.trim_start_matches("0x"), - ) - .map_err(|e| ErrorObjectOwned::owned(1, "hex decode final checkpoint proof", Some(e.to_string())))?; - let final_checkpoint_proof: ProofWithPublicInputs = - bincode::deserialize(&final_checkpoint_proof_bytes) - .map_err(|e| ErrorObjectOwned::owned(1, "bincode deserialize final checkpoint proof", Some(e.to_string())))?; - - // Parse delta merkle proofs - use plonky2::hash::poseidon::PoseidonHash; - let parse_delta = |dp: &BridgeAggDeltaProof| -> anyhow::Result>> { - let new_value = parse_hex_qhashout_to_qhash(&dp.new_value)?; - let siblings = dp.siblings.iter().map(|s| parse_hex_qhashout_to_qhash(s)).collect::, _>>()?; - Ok(ParthDeltaMerkleProofCore::from_params::( - dp.index, - parth_core::pgoldilocks::QHashOut::default(), - new_value, - siblings, - )) - }; - - let delta_merkle_proofs: Vec>> = input.delta_merkle_proofs - .iter() - .map(parse_delta) - .collect::>>() - .map_err(|e| ErrorObjectOwned::owned(1, "parse delta proofs", Some(e.to_string())))?; - - let pre_delta_merkle_proofs: Vec>> = input.pre_delta_merkle_proofs - .iter() - .map(parse_delta) - .collect::>>() - .map_err(|e| ErrorObjectOwned::owned(1, "parse pre-delta proofs", Some(e.to_string())))?; - - // `chain_start` is the chain hash immediately before the aggregated range - // (chain hash of checkpoint `from_checkpoint - 1`; for `from_checkpoint <= 1` - // this is the genesis checkpoint state transition hash). - let start_chain_hash = parse_hex_qhashout_to_qhash(&input.chain_start) - .map_err(|e| ErrorObjectOwned::owned(1, "parse chain_start", Some(e.to_string())))?; - - let final_leaf = psy_data::v1::qdata::checkpoint::PQEDCheckpointLeafCompact { - global_chain_root: parse_hex_qhashout_to_qhash(&input.final_checkpoint_leaf.global_chain_root) - .map_err(|e| ErrorObjectOwned::owned(1, "parse final leaf chain root", Some(e.to_string())))?, - stats_hash: parse_hex_qhashout_to_qhash(&input.final_checkpoint_leaf.stats_hash) - .map_err(|e| ErrorObjectOwned::owned(1, "parse final leaf stats hash", Some(e.to_string())))?, - }; - - // Parse global state roots (anchors the user_tree_root to the verified checkpoint) - let parse_qhash = |hex: &str| -> anyhow::Result> { - parse_hex_qhashout_to_qhash(hex) - }; - let global_state_roots = PQEDCheckpointGlobalStateRoots { - contract_tree_root: parse_qhash(&input.final_checkpoint_global_state_roots.contract_tree_root) - .map_err(|e| ErrorObjectOwned::owned(1, "parse contract_tree_root", Some(e.to_string())))?, - deposit_tree_root: parse_qhash(&input.final_checkpoint_global_state_roots.deposit_tree_root) - .map_err(|e| ErrorObjectOwned::owned(1, "parse deposit_tree_root", Some(e.to_string())))?, - user_tree_root: parse_qhash(&input.final_checkpoint_global_state_roots.user_tree_root) - .map_err(|e| ErrorObjectOwned::owned(1, "parse user_tree_root", Some(e.to_string())))?, - withdrawal_tree_root: parse_qhash(&input.final_checkpoint_global_state_roots.withdrawal_tree_root) - .map_err(|e| ErrorObjectOwned::owned(1, "parse withdrawal_tree_root", Some(e.to_string())))?, - user_registration_tree_root: parse_qhash(&input.final_checkpoint_global_state_roots.user_registration_tree_root) - .map_err(|e| ErrorObjectOwned::owned(1, "parse user_registration_tree_root", Some(e.to_string())))?, - }; - - // Parse witnesses (slot witnesses are the full TreeRootInContractStateWitnessInput) - let parse_slot_witness = |w: &BridgeAggSlotWitness| -> anyhow::Result> { - let user_leaf = psy_data::v1::qdata::user::PQEDUserLeaf::> { - public_key: parse_hex_qhashout_to_qhash(&w.user_leaf_public_key)?, - user_state_tree_root: parse_hex_qhashout_to_qhash(&w.user_leaf_user_state_tree_root)?, - balance: F::from_canonical_u64(w.user_leaf_balance), - nonce: F::from_canonical_u64(w.user_leaf_nonce), - last_checkpoint_id: F::from_canonical_u64(w.user_leaf_last_checkpoint_id), - event_index: F::from_canonical_u64(w.user_leaf_event_index), - user_id: F::from_canonical_u64(w.user_leaf_user_id), - }; - - let mk_proof = |root: &str, value: &str, index: u64, sibs: &[String]| -> anyhow::Result>> { - Ok(parth_core::crypto::hash::merkle_proof::MerkleProofCore { - root: parse_hex_qhashout_to_qhash(root)?, - value: parse_hex_qhashout_to_qhash(value)?, - index, - siblings: sibs.iter().map(|s| parse_hex_qhashout_to_qhash(s)).collect::, _>>()?, - }) - }; - - Ok(TreeRootInContractStateWitnessInput { - owner_user_id: w.owner_user_id, - contract_id: w.contract_id, - user_leaf, - slot0_proof: mk_proof(&w.slot0_root, &w.slot0_value, w.slot0_index, &w.slot0_siblings)?, - slot1_proof: mk_proof(&w.slot1_root, &w.slot1_value, w.slot1_index, &w.slot1_siblings)?, - contract_proof: mk_proof(&w.contract_root, &w.contract_value, w.contract_index, &w.contract_siblings)?, - user_tree_proof: mk_proof(&w.user_tree_root, &w.user_tree_value, w.user_tree_index, &w.user_tree_siblings)?, - }) - }; - - let deposit_witness = parse_slot_witness(&input.deposit_witness) - .map_err(|e| ErrorObjectOwned::owned(1, "parse deposit witness", Some(e.to_string())))?; - let withdrawal_witness = parse_slot_witness(&input.withdrawal_witness) - .map_err(|e| ErrorObjectOwned::owned(1, "parse withdrawal witness", Some(e.to_string())))?; - - tracing::info!( - "Proving bridge aggregation for checkpoints {} to {}...", - from_checkpoint, - to_checkpoint - ); - - let result = BridgeAggFinalCircuit::::prove_range( - from_checkpoint, - to_checkpoint, - start_chain_hash, - checkpoint_common_data, - cap_height, - checkpoint_state_transition_fingerprint, - checkpoint_step_commit_fingerprint, - &final_checkpoint_proof, - &checkpoint_verifier_data, - &delta_merkle_proofs, - &pre_delta_merkle_proofs, - &final_leaf, - &global_state_roots, - &deposit_witness, - &withdrawal_witness, - 32, // CHECKPOINT_TREE_HEIGHT - PsyNetworkLocalDevnetConstants::GLOBAL_USER_TREE_HEIGHT_USIZE, - PsyNetworkLocalDevnetConstants::GLOBAL_CONTRACT_TREE_HEIGHT_USIZE, - DEPOSIT_TREE_CONTRACT_STATE_TREE_HEIGHT as usize, - WITHDRAWAL_TREE_CONTRACT_STATE_TREE_HEIGHT as usize, - ) - .map_err(|e| ErrorObjectOwned::owned(1, "bridge_agg prove_range failed", Some(e.to_string())))?; - - let bridge_agg_proof = result.proof; - let bridge_agg_verifier_data = result.verifier_data; - - tracing::info!("Proving BridgeWrapCircuit (Groth16 wrap)..."); - let groth16_proof = wrap_circuit - .prove_groth16_with_shared_wrapper(&groth16_wrapper, &bridge_agg_verifier_data, &bridge_agg_proof) - .map_err(|e| ErrorObjectOwned::owned(1, "bridge_wrap Groth16 failed", Some(e.to_string())))?; - - // Format outputs - let groth16_pi = &bridge_agg_proof.public_inputs; - let checkpoint_roots = vec![ - felt4_to_bytes32_hex(&groth16_pi[0..4]), - felt4_to_bytes32_hex(&groth16_pi[20..24]), - ]; - let deposit_tree_root = u32x8_to_bytes32_hex(&groth16_pi[4..12]); - let withdrawal_tree_root = u32x8_to_bytes32_hex(&groth16_pi[12..20]); - let end_checkpoint_index = groth16_pi[24].to_canonical_u64(); - if end_checkpoint_index != to_checkpoint { - return Err(ErrorObjectOwned::owned( - 1, - "prove_bridge_agg_groth16: end_checkpoint_index mismatch", - Some(format!("pi={} expected={}", end_checkpoint_index, to_checkpoint)), - )); - } - - let solidity_words = g16_proof_to_solidity_words(&groth16_proof); - let pub_inputs_0 = groth16_proof.public_inputs[0].clone(); - let pub_inputs_1 = groth16_proof.public_inputs[1].clone(); - let public_inputs_str: Vec = groth16_pi.iter().map(|x| x.to_canonical_u64().to_string()).collect(); - let num_pis = groth16_pi.len(); - - Ok(BridgeAggGroth16Output { - from_checkpoint, - to_checkpoint, - num_checkpoints_aggregated, - bridge_agg_public_inputs_count: num_pis, - bridge_agg_public_inputs: public_inputs_str, - groth16_proof, - solidity_proof: [ - solidity_words[0].clone(), - solidity_words[1].clone(), - solidity_words[2].clone(), - solidity_words[3].clone(), - solidity_words[4].clone(), - solidity_words[5].clone(), - solidity_words[6].clone(), - solidity_words[7].clone(), - ], - solidity_public_inputs: [ - pub_inputs_0, - pub_inputs_1, - ], - checkpoint_roots, - deposit_tree_root, - withdrawal_tree_root, - end_checkpoint_index, - }) - }) - .await - .map_err(|join_err| { - ErrorObjectOwned::owned( - 1, - "prove_bridge_agg_groth16: task schedule failed", - Some(format!("Thread pool task execution failed: {}", join_err)), - ) - })? - } - +impl ProveProxyUserRpcServer for UserProveProvider { async fn prove_ups_start(&self, input: UPSStartStepInput) -> Result, ErrorObjectOwned> { tracing::debug!("prove_ups_start input"); diff --git a/client_prover/psy_prover/tests/prove_proxy_role.rs b/client_prover/psy_prover/tests/prove_proxy_role.rs new file mode 100644 index 00000000..dd1723ca --- /dev/null +++ b/client_prover/psy_prover/tests/prove_proxy_role.rs @@ -0,0 +1,77 @@ +use std::sync::atomic::{AtomicBool, Ordering}; + +use jsonrpsee::server::RpcModule; +use psy_client_common::args::ProveProxyRole; +use psy_prover::local::native::prove_proxy::{assemble_rpc_module, ProveProxyRoleInfo}; + +fn marker(name: &'static str) -> jsonrpsee::server::Methods { + let mut m = RpcModule::new(()); + m.register_method(name, |_, _, _| "ok").unwrap(); + m.into() +} + +fn names(module: &RpcModule<()>) -> Vec<&'static str> { + let mut v: Vec<_> = module.method_names().collect(); + v.sort(); + v +} + +#[test] +fn user_role_registers_only_user_family() { + let user_called = AtomicBool::new(false); + let system_called = AtomicBool::new(false); + let module = assemble_rpc_module( + ProveProxyRole::User, + || { user_called.store(true, Ordering::SeqCst); Ok(marker("psy_user_marker")) }, + || { system_called.store(true, Ordering::SeqCst); Ok(marker("psy_system_marker")) }, + ) + .unwrap(); + assert!(user_called.load(Ordering::SeqCst)); + assert!(!system_called.load(Ordering::SeqCst), "system constructor must not run in user role"); + assert_eq!(names(&module), vec!["psy_get_prove_proxy_role", "psy_user_marker"]); +} + +#[test] +fn system_role_registers_only_system_family() { + let user_called = AtomicBool::new(false); + let module = assemble_rpc_module( + ProveProxyRole::System, + || { user_called.store(true, Ordering::SeqCst); Ok(marker("psy_user_marker")) }, + || Ok(marker("psy_system_marker")), + ) + .unwrap(); + assert!(!user_called.load(Ordering::SeqCst), "user constructor must not run in system role"); + assert_eq!(names(&module), vec!["psy_get_prove_proxy_role", "psy_system_marker"]); +} + +#[test] +fn all_role_registers_both() { + let module = assemble_rpc_module( + ProveProxyRole::All, + || Ok(marker("psy_user_marker")), + || Ok(marker("psy_system_marker")), + ) + .unwrap(); + assert_eq!(names(&module), vec!["psy_get_prove_proxy_role", "psy_system_marker", "psy_user_marker"]); +} + +#[test] +fn constructor_error_propagates() { + let err = assemble_rpc_module( + ProveProxyRole::System, + || Ok(marker("psy_user_marker")), + || Err(anyhow::anyhow!("gnark keystore missing")), + ) + .err() + .expect("must fail"); + assert!(err.to_string().contains("gnark keystore missing")); +} + +#[tokio::test] +async fn role_info_reports_role() { + let module = assemble_rpc_module(ProveProxyRole::System, || Ok(marker("psy_user_marker")), || Ok(marker("psy_system_marker"))).unwrap(); + let info: ProveProxyRoleInfo = module.call("psy_get_prove_proxy_role", jsonrpsee::rpc_params![]).await.unwrap(); + assert_eq!(info.role, "system"); + assert!(!info.user_methods); + assert!(info.system_methods); +} diff --git a/dev/locSetupV4.ts b/dev/locSetupV4.ts index f3b53d85..37b2cdd6 100644 --- a/dev/locSetupV4.ts +++ b/dev/locSetupV4.ts @@ -3806,6 +3806,8 @@ class DevNetProcessManager { `0.0.0.0:${port}`, '--rpc-config', 'psy-genesis/config.json', + '--role', + 'all', ], proveProxyStartedDetector, { cwd, ...getLogPaths(`prove_proxy_${i}`, false), maxRetries: 3, retryDelayMs: 2000, env: this.getEnv() } diff --git a/docs/src/rpc/ProverProxy.md b/docs/src/rpc/ProverProxy.md index a38a8557..6da1a375 100644 --- a/docs/src/rpc/ProverProxy.md +++ b/docs/src/rpc/ProverProxy.md @@ -11,6 +11,7 @@ This document provides comprehensive documentation for the Psy Prover Proxy RPC ## Table of Contents 1. [Overview](#overview) + - [Roles](#roles) 2. [UPS (Unified Proving System) Methods](#ups-unified-proving-system-methods) 3. [Contract Management](#contract-management) 4. [Signature Proving](#signature-proving) @@ -35,6 +36,26 @@ The Prover Proxy is a local proving service that generates zero-knowledge proofs - **Contract Circuit Management**: Dynamic registration and execution of contract circuits - **Proof Tree Aggregation**: Hierarchical proof composition and verification +### Roles + +Every instance runs exactly one role, chosen with `--role` (env `PROVE_PROXY_ROLE`, default `user`): + +| Role | Registers | Built at startup | Intended for | +|---|---|---|---| +| `user` | UPS session chain, proof-tree aggregation, contract circuits, signature and minifier proofs, circuit queries | UPS circuit manager | wallet-facing pool (CLI, web wallet, faucet-server, MCP) | +| `system` | `psy_prove_withdrawal_batch_claim_groth16`, `psy_prove_deposit_batch_append_groth16`, `psy_prove_bridge_agg_groth16` | bridge wrap circuits, three Groth16 keystores | relayer pool | +| `all` | both | both | single-machine local testnets | + +A method outside the instance's role is not registered; calling it returns JSON-RPC `-32601 Method not found`. + +Every role also registers `psy_get_prove_proxy_role`: + +```json +{ "jsonrpc": "2.0", "id": 1, "result": { "role": "user", "user_methods": true, "system_methods": false } } +``` + +Clients pick the pool through `config.json`: wallets use `prove_proxy_url`; the relayer reads `system_prove_proxy_url` and refuses to start when it is missing for the current network. `psy_user_cli claim-withdrawal --prove-proxy-url` must point at a `system` or `all` instance. + --- ## UPS (Unified Proving System) Methods @@ -738,6 +759,7 @@ The prover proxy is configured via `ProveProxyArgs`: pub struct ProveProxyArgs { pub listen_addr: String, // Default: "0.0.0.0:9999" pub rpc_config: String, // Path to network config file + pub role: ProveProxyRole, // user | system | all, default user } ``` @@ -747,7 +769,8 @@ pub struct ProveProxyArgs { # Start prover proxy psy_user_cli prove-proxy \ --listen-addr "127.0.0.1:9999" \ - --rpc-config "config.json" + --rpc-config "config.json" \ + --role user ``` ### Circuit Initialization @@ -755,7 +778,7 @@ psy_user_cli prove-proxy \ The prover proxy initializes with: - Network magic number for proof validation - RPC provider for fetching contract code -- Circuit manager for all proof types +- Circuit manager for the proof families the configured role serves (see Roles) - Session circuit info store for fingerprint tracking --- diff --git a/psy-genesis b/psy-genesis index 9ea96ca1..dd9f3384 160000 --- a/psy-genesis +++ b/psy-genesis @@ -1 +1 @@ -Subproject commit 9ea96ca13f60247c22b2108f086fb9cd1287ba94 +Subproject commit dd9f3384e2b0be8621036c58a3b36248d3138404 diff --git a/psy_cli/psy_relayer_cli/src/bridge/daemon.rs b/psy_cli/psy_relayer_cli/src/bridge/daemon.rs index 6403b441..a6edf529 100644 --- a/psy_cli/psy_relayer_cli/src/bridge/daemon.rs +++ b/psy_cli/psy_relayer_cli/src/bridge/daemon.rs @@ -857,8 +857,9 @@ async fn run_multichain( let max_batch = config.max_checkpoint_batch.unwrap_or(DEFAULT_MAX_CHECKPOINT_BATCH); validate_max_checkpoint_batch(max_batch)?; let state_path = proof_dir.join("daemon_state_multichain.toml"); - let proxy = resolve_prove_proxy_url(&config); - if proxy.is_none() { warmup_bridge_resources()?; } + let proxy = resolve_system_prove_proxy_url(&config)?; + verify_system_prove_proxy(&proxy).await?; + tracing::info!(prove_proxy = %proxy, "system prove proxy configured"); tracing::info!(config=%config_path.display(), chain_count=chains.len(), %identity_namespace, "multichain bridge relayer started"); loop { @@ -948,7 +949,7 @@ async fn run_multichain( let needs_finalize = chains.iter().any(|chain| cursors[&chain.chain_index] < to_checkpoint); if needs_finalize && !shared_path.exists() { if let Err(error) = chains[0].l1.load_or_build_proof( - &chains[0].config, &shared_path, from_checkpoint, to_checkpoint, proxy.as_deref(), + &chains[0].config, &shared_path, from_checkpoint, to_checkpoint, Some(proxy.as_str()), ).await { tracing::error!(%error, "shared bridge proof generation failed"); tokio::time::sleep(poll_interval).await; continue; @@ -1028,12 +1029,9 @@ async fn run_single_chain(config: BridgeProposeDaemonConfig, config_path: &Path) tracing::info!(config = %config_path.display(), proof_dir = %proof_dir.display(), "bridge relayer started"); // Phase 4.4: skip local circuit/Groth16 warmup when remote prove proxy is configured - let proxy_url_at_startup = resolve_prove_proxy_url(&config); - if proxy_url_at_startup.is_some() { - tracing::info!("prove proxy configured; skipping local circuit/Groth16 warmup"); - } else { - warmup_bridge_resources()?; - } + let proxy_url_at_startup = resolve_system_prove_proxy_url(&config)?; + verify_system_prove_proxy(&proxy_url_at_startup).await?; + tracing::info!(prove_proxy = %proxy_url_at_startup, "system prove proxy configured; local Groth16 warmup skipped"); let provider = RpcProvider::new_with_config_path(&config.rpc_config)?; let l1 = L1Client::from_finalize_config(&config.finalize); @@ -1321,7 +1319,7 @@ async fn run_single_chain(config: BridgeProposeDaemonConfig, config_path: &Path) // PHASE 3 ─ Proof Generation (bridge aggregation + Groth16 wrap) // ═══════════════════════════════════════════════════════════════════ - let prove_proxy_url = resolve_prove_proxy_url(&config); + let prove_proxy_url = resolve_system_prove_proxy_url(&config)?; let prove_result = match dispatch_post_l2_phase( &post_l2_phase_permit, l1.load_or_build_proof( @@ -1329,7 +1327,7 @@ async fn run_single_chain(config: BridgeProposeDaemonConfig, config_path: &Path) &proof_path, from_checkpoint, to_checkpoint, - prove_proxy_url.as_deref(), + Some(prove_proxy_url.as_str()), ), ) .await @@ -1651,6 +1649,7 @@ pub(crate) async fn submit_deposit_batch_appends_with_l1_rpc( Ok(()) } +#[allow(dead_code)] // kept for the local-proving CLI path fn warmup_bridge_resources() -> anyhow::Result<()> { tracing::info!("warming bridge relayer resources"); @@ -1682,14 +1681,87 @@ fn warmup_bridge_resources() -> anyhow::Result<()> { Ok(()) } -pub(crate) fn resolve_prove_proxy_url(config: &BridgeProposeDaemonConfig) -> Option { - let rpc_config = psy_config::PsyConfigGoldilocks::from_file(&config.rpc_config).ok()?; - let network = rpc_config.get_current_network().ok()?; +/// The prove-proxy pool that serves bridge Groth16 proofs. +/// +/// Reads `system_prove_proxy_url` for the current network. There is no +/// fallback to `prove_proxy_url`: that pool runs the `user` role and does not +/// register the bridge methods, so routing there would only fail later. +pub(crate) fn resolve_system_prove_proxy_url(config: &BridgeProposeDaemonConfig) -> anyhow::Result { + let rpc_config = psy_config::PsyConfigGoldilocks::from_file(&config.rpc_config) + .with_context(|| format!("failed to load rpc config {}", config.rpc_config))?; + let network_name = rpc_config.current_network_name().to_string(); + let network = rpc_config.get_current_network()?; network - .prove_proxy_url + .system_prove_proxy_url .iter() - .find(|url| !url.trim().is_empty()) - .cloned() + .map(|url| url.trim()) + .find(|url| !url.is_empty()) + .map(str::to_string) + .ok_or_else(|| { + anyhow::anyhow!( + "network `{}` in {} has no `system_prove_proxy_url`; the relayer needs a prove-proxy running role=system or role=all", + network_name, + config.rpc_config + ) + }) +} + +/// Confirms the resolved prove-proxy registers the bridge (system) methods. +/// A pool running role=user answers `psy_get_prove_proxy_role` with +/// `system_methods: false`; treat that like a missing URL and refuse to start. +pub(crate) async fn verify_system_prove_proxy(url: &str) -> anyhow::Result<()> { + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(10)) + .build() + .context("failed to build reqwest client for prove-proxy role verification")?; + + let response = client + .post(url) + .json(&serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "psy_get_prove_proxy_role", + "params": [] + })) + .send() + .await + .with_context(|| format!("failed to reach prove proxy {url} for psy_get_prove_proxy_role"))?; + + let status = response.status(); + if !status.is_success() { + anyhow::bail!("prove proxy {url} returned HTTP {status} for psy_get_prove_proxy_role"); + } + + let body: serde_json::Value = response + .json() + .await + .with_context(|| format!("failed to parse psy_get_prove_proxy_role response from {url}"))?; + + let role = body + .get("result") + .and_then(|result| result.get("role")) + .and_then(|role| role.as_str()) + .ok_or_else(|| { + anyhow::anyhow!("prove proxy {url} psy_get_prove_proxy_role response missing result.role") + })? + .to_string(); + + let system_methods = body + .get("result") + .and_then(|result| result.get("system_methods")) + .and_then(|value| value.as_bool()) + .ok_or_else(|| { + anyhow::anyhow!("prove proxy {url} psy_get_prove_proxy_role response missing result.system_methods") + })?; + + if !system_methods { + anyhow::bail!( + "prove proxy {url} runs role `{role}` and does not serve system proofs; point system_prove_proxy_url at a role=system or role=all instance" + ); + } + + tracing::info!(prove_proxy = %url, role = %role, "system prove proxy verified"); + Ok(()) } fn load_config(path: &Path) -> anyhow::Result { @@ -6003,4 +6075,130 @@ deployments_network = "localhostBase" assert_eq!(config.chains.iter().map(|chain| chain.chain_index).collect::>(), vec![0, 1, 2]); assert!(config.chains.iter().all(|chain| chain.family == "evm")); } + + fn write_temp_rpc_config(system_urls: &str) -> std::path::PathBuf { + let dir = std::env::temp_dir().join(format!( + "relayer-proxy-test-{}-{}", + std::process::id(), + system_urls.len() + )); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("config.json"); + // Template: the "localhost" NetworkConfig object, copied from + // psy_config::tests::system_prove_proxy_url_defaults_to_empty_and_reads_when_present + // (client_prover/psy_core/psy_config/src/lib.rs). That test's `base` is the + // correct, flat NetworkConfig shape (no "network" wrapper key) — unlike + // test_network_switching's literal, which is broken on this branch (extra + // "network" wrapper, missing required fields). + let json = r#"{"networks":{"localhost":{ + "magic": "0x1", + "users_per_realm": 1048576, + "global_user_tree_height": 24, + "realm_user_tree_height": 20, + "group_realm_height": 1, + "realm_configs": [{"id": 0, "rpc_url": ["http://127.0.0.1:8546"]}], + "coordinator_configs": [{"id": 0, "rpc_url": ["http://127.0.0.1:8545"]}], + "prove_proxy_url": ["http://127.0.0.1:9999"], + "system_prove_proxy_url": __SYSTEM_URLS__, + "faucet_rpc_url": ["http://127.0.0.1:8547"], + "nostr_relay_url": "wss://relay.127.0.0.1.example", + "native_currency": "PSY", + "native_currency_decimal": 9, + "native_currency_name": "PSY", + "fees": { + "register_user_fee": 0, + "deploy_contract_fee": 0, + "guta_fee": 5000000000, + "da_fee": 0 + } + }},"defaultNetwork":"localhost"}"# + .replace("__SYSTEM_URLS__", system_urls); + std::fs::write(&path, json).unwrap(); + path + } + + fn daemon_config_with_rpc(path: &std::path::Path) -> BridgeProposeDaemonConfig { + toml::from_str(&format!( + "rpc_config = {:?}\nservices_url = \"http://127.0.0.1:1\"\nwithdraw_method_id = 0\n", + path.display() + )) + .unwrap() + } + + #[test] + fn system_prove_proxy_url_is_required() { + let path = write_temp_rpc_config("[]"); + let err = resolve_system_prove_proxy_url(&daemon_config_with_rpc(&path)).unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("system_prove_proxy_url"), "{msg}"); + assert!(msg.contains("localhost"), "{msg}"); + } + + #[test] + fn system_prove_proxy_url_skips_blank_entries() { + let path = write_temp_rpc_config(r#"[" ", "http://127.0.0.1:9997"]"#); + let url = resolve_system_prove_proxy_url(&daemon_config_with_rpc(&path)).unwrap(); + assert_eq!(url, "http://127.0.0.1:9997"); + } + + #[test] + fn user_pool_url_is_not_a_fallback() { + let path = write_temp_rpc_config(r#"[""]"#); + assert!(resolve_system_prove_proxy_url(&daemon_config_with_rpc(&path)).is_err()); + } + + /// Starts a minimal single-shot HTTP responder on `127.0.0.1:0` that reads + /// one request and replies with `body` as a JSON response, then returns + /// the URL to reach it. + fn spawn_json_responder(body: &'static str) -> String { + let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + let addr = listener.local_addr().unwrap(); + std::thread::spawn(move || { + use std::io::{Read, Write}; + if let Ok((mut stream, _)) = listener.accept() { + let mut buf = [0u8; 4096]; + let _ = stream.read(&mut buf); + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + body.len(), + body + ); + let _ = stream.write_all(response.as_bytes()); + let _ = stream.flush(); + } + }); + format!("http://{addr}") + } + + #[tokio::test] + async fn verify_system_prove_proxy_accepts_role_system() { + let url = spawn_json_responder( + r#"{"jsonrpc":"2.0","id":1,"result":{"role":"system","user_methods":false,"system_methods":true}}"#, + ); + + verify_system_prove_proxy(&url).await.unwrap(); + } + + #[tokio::test] + async fn verify_system_prove_proxy_rejects_role_user() { + let url = spawn_json_responder( + r#"{"jsonrpc":"2.0","id":1,"result":{"role":"user","user_methods":true,"system_methods":false}}"#, + ); + + let err = verify_system_prove_proxy(&url).await.unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("role `user`"), "{msg}"); + assert!(msg.contains("system_prove_proxy_url"), "{msg}"); + } + + #[tokio::test] + async fn verify_system_prove_proxy_errs_on_closed_port() { + // Bind to get a free port, then drop the listener so nothing answers. + let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + let addr = listener.local_addr().unwrap(); + drop(listener); + let url = format!("http://{addr}"); + + assert!(verify_system_prove_proxy(&url).await.is_err()); + } } diff --git a/psy_cli/psy_relayer_cli/src/bridge/l1_client.rs b/psy_cli/psy_relayer_cli/src/bridge/l1_client.rs index 3382d3ee..22d5f505 100644 --- a/psy_cli/psy_relayer_cli/src/bridge/l1_client.rs +++ b/psy_cli/psy_relayer_cli/src/bridge/l1_client.rs @@ -9,7 +9,7 @@ use crate::bridge::{ claim_withdrawals, constants::{DEFAULT_DEPLOYMENTS_NETWORK, DEFAULT_L1_RPC_URL}, daemon::{ - fetch_l1_last_finalized_checkpoint, resolve_bridge_address, resolve_prove_proxy_url, + fetch_l1_last_finalized_checkpoint, resolve_bridge_address, resolve_system_prove_proxy_url, run_l2_bridge_round_with_l1_provider, submit_deposit_batch_appends_with_l1_rpc, BridgeProposeDaemonConfig, DaemonState, DaemonFinalizeConfig, L2RoundResult, }, @@ -218,7 +218,7 @@ impl L1Client { config: &BridgeProposeDaemonConfig, target_deposit_count: u32, ) -> anyhow::Result<()> { - let prove_proxy_url = resolve_prove_proxy_url(config); + let prove_proxy_url = resolve_system_prove_proxy_url(config)?; self.with_retry("deposit_batch_appends", L1_RETRY_MAX_ATTEMPTS, |url| { let owned = url.to_string(); let proxy = prove_proxy_url.clone(); @@ -227,7 +227,7 @@ impl L1Client { config, &owned, target_deposit_count, - proxy.as_deref(), + Some(proxy.as_str()), ) .await } @@ -247,7 +247,7 @@ impl L1Client { to_checkpoint, "claiming current batch withdrawals on L1" ); - let prove_proxy_url = resolve_prove_proxy_url(config); + let prove_proxy_url = resolve_system_prove_proxy_url(config)?; self.with_retry("claim_withdrawals", L1_RETRY_MAX_ATTEMPTS, |url| { let owned = url.to_string(); let bridge_addr = bridge_addr.clone(); @@ -258,7 +258,7 @@ impl L1Client { config, &owned, &bridge_addr, - proxy.as_deref(), + Some(proxy.as_str()), ) .await?; tracing::info!(