diff --git a/near/Cargo.lock b/near/Cargo.lock index dddc2934e..c017558d2 100644 --- a/near/Cargo.lock +++ b/near/Cargo.lock @@ -3158,6 +3158,15 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "polymer-prover" +version = "0.3.2" +dependencies = [ + "borsh", + "near-sdk", + "omni-types", +] + [[package]] name = "potential_utf" version = "0.1.3" diff --git a/near/Cargo.toml b/near/Cargo.toml index a7a7ad2bd..9e9755096 100644 --- a/near/Cargo.toml +++ b/near/Cargo.toml @@ -10,6 +10,7 @@ members = [ "token-deployer", "omni-prover/wormhole-omni-prover-proxy", "omni-prover/evm-prover", + "omni-prover/polymer-prover", "omni-token", "omni-types", "omni-tests", diff --git a/near/omni-prover/polymer-prover/Cargo.toml b/near/omni-prover/polymer-prover/Cargo.toml new file mode 100644 index 000000000..33321194b --- /dev/null +++ b/near/omni-prover/polymer-prover/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "polymer-prover" +version.workspace = true +authors = ["Near One "] +edition = "2021" +repository.workspace = true + +# fields to configure build with WASM reproducibility, according to specs +# in https://github.com/near/NEPs/blob/master/neps/nep-0330.md +[package.metadata.near.reproducible_build] +# docker image, descriptor of build environment +image = "sourcescan/cargo-near:0.16.0-rust-1.86.0" +# tag after colon above serves only descriptive purpose; image is identified by digest +image_digest = "sha256:3220302ebb7036c1942e772810f21edd9381edf9a339983da43487c77fbad488" +# list of environment variables names, whose values, if set, will be used as external build parameters +# in a reproducible manner +# supported by `sourcescan/cargo-near:0.10.1-rust-1.82.0` image or later images +passed_env = [] +# build command inside of docker container +# if docker image from default gallery is used https://hub.docker.com/r/sourcescan/cargo-near/tags, +# the command may be any combination of flags of `cargo-near`, +# supported by respective version of binary inside the container besides `--no-locked` flag +container_build_command = ["cargo", "near", "build", "non-reproducible-wasm", "--locked"] + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +near-sdk.workspace = true +borsh.workspace = true +omni-types.workspace = true diff --git a/near/omni-prover/polymer-prover/src/lib.rs b/near/omni-prover/polymer-prover/src/lib.rs new file mode 100644 index 000000000..5f183e269 --- /dev/null +++ b/near/omni-prover/polymer-prover/src/lib.rs @@ -0,0 +1,130 @@ +use near_sdk::borsh::BorshDeserialize; +use near_sdk::{ + env, ext_contract, near, near_bindgen, AccountId, Gas, PanicOnDefault, Promise, + PromiseError, +}; +use omni_types::polymer::events::parse_polymer_event_by_kind; +use omni_types::prover_args::PolymerVerifyProofArgs; +use omni_types::prover_result::{ProofKind, ProverResult}; + +pub const VERIFY_PROOF_GAS: Gas = Gas::from_tgas(15); +pub const VERIFY_PROOF_CALLBACK_GAS: Gas = Gas::from_tgas(5); + +/// Interface to Polymer's CrossL2ProverV2 contract deployed on NEAR +/// This contract validates IAVL proofs from Polymer Hub +#[ext_contract(ext_polymer_verifier)] +pub trait PolymerVerifier { + /// Validates a Polymer proof and returns event data + /// Returns: (chainId, emittingContract, topics, unindexedData) + fn validate_event(&self, proof: Vec) -> (u32, String, Vec, Vec); +} + +#[near(contract_state)] +#[derive(PanicOnDefault)] +pub struct PolymerProver { + /// Account ID of the deployed Polymer verifier contract on NEAR + pub verifier_account: AccountId, +} + +#[near_bindgen] +impl PolymerProver { + #[init] + #[private] + #[must_use] + pub const fn init(verifier_account: AccountId) -> Self { + Self { verifier_account } + } + + /// Main entry point: accepts proof bytes and delegates to Polymer verifier + #[allow(clippy::needless_pass_by_value)] + pub fn verify_proof(&self, #[serializer(borsh)] input: Vec) -> Promise { + let args = PolymerVerifyProofArgs::try_from_slice(&input) + .unwrap_or_else(|_| env::panic_str("ERR_PARSE_ARGS")); + + env::log_str(&format!( + "Polymer proof verification: chain_id={}, block={}, log_index={}", + args.src_chain_id, args.src_block_number, args.global_log_index + )); + + // Call Polymer verifier contract + ext_polymer_verifier::ext(self.verifier_account.clone()) + .with_static_gas(VERIFY_PROOF_GAS) + .validate_event(args.proof.clone()) + .then( + Self::ext(env::current_account_id()) + .with_static_gas(VERIFY_PROOF_CALLBACK_GAS) + .verify_proof_callback( + args.proof_kind, + args.src_chain_id, + args.src_block_number, + args.global_log_index, + ), + ) + } + + /// Callback after Polymer verifier validates the proof + /// Parses the validated event data into ProverResult + #[private] + #[handle_result] + #[result_serializer(borsh)] + pub fn verify_proof_callback( + &mut self, + #[serializer(borsh)] proof_kind: ProofKind, + #[serializer(borsh)] src_chain_id: u64, + #[serializer(borsh)] _src_block_number: u64, + #[serializer(borsh)] _global_log_index: u64, + #[callback_result] validation_result: &Result<(u32, String, Vec, Vec), PromiseError>, + ) -> Result { + let (chain_id, emitting_contract, topics, unindexed_data) = + validation_result + .as_ref() + .map_err(|_| "Polymer proof validation failed".to_owned())?; + + // Verify the chain_id matches what we requested + if u64::from(*chain_id) != src_chain_id { + return Err(format!( + "Chain ID mismatch: expected {}, got {}", + src_chain_id, chain_id + )); + } + + env::log_str(&format!( + "Proof validated: contract={}, topics_len={}, data_len={}", + emitting_contract, + topics.len(), + unindexed_data.len() + )); + + // Parse event based on proof kind + self.parse_polymer_event( + proof_kind, + emitting_contract, + topics, + unindexed_data, + src_chain_id, + ) + } + + /// Parse Polymer-validated event data into ProverResult + fn parse_polymer_event( + &self, + proof_kind: ProofKind, + emitting_contract: &str, + topics: &[u8], + unindexed_data: &[u8], + chain_id: u64, + ) -> Result { + // Verify minimum topics length (event signature must be present) + if topics.len() < 32 { + return Err("Invalid topics length: must be at least 32 bytes".to_owned()); + } + + parse_polymer_event_by_kind( + proof_kind, + chain_id, + emitting_contract, + topics, + unindexed_data, + ) + } +} diff --git a/near/omni-types/src/lib.rs b/near/omni-types/src/lib.rs index 02acefcba..53c0c8444 100644 --- a/near/omni-types/src/lib.rs +++ b/near/omni-types/src/lib.rs @@ -15,6 +15,7 @@ pub mod evm; pub mod locker_args; pub mod mpc_types; pub mod near_events; +pub mod polymer; pub mod prover_args; pub mod prover_result; pub mod sol_address; diff --git a/near/omni-types/src/polymer/events.rs b/near/omni-types/src/polymer/events.rs new file mode 100644 index 000000000..11276db7e --- /dev/null +++ b/near/omni-types/src/polymer/events.rs @@ -0,0 +1,184 @@ +use alloy::{primitives::{Address, Bytes, FixedBytes, Log, LogData}, sol_types::SolEvent}; + +use crate::{ + evm::events::{DeployToken, FinTransfer, InitTransfer, LogMetadata, TryFromLog}, + prover_result::{ + DeployTokenMessage, FinTransferMessage, InitTransferMessage, LogMetadataMessage, + ProofKind, ProverResult, + }, + stringify, ChainKind, +}; + +/// Generic parser that routes to specific event type based on ProofKind +/// This matches the pattern from evm-prover for consistency +pub fn parse_polymer_event_by_kind( + proof_kind: ProofKind, + chain_id: u64, + emitting_contract: &str, + topics: &[u8], + unindexed_data: &[u8], +) -> Result { + let chain_kind = map_chain_id_to_kind(chain_id)?; + + match proof_kind { + ProofKind::InitTransfer => Ok(ProverResult::InitTransfer( + parse_polymer_event::( + chain_kind, + emitting_contract, + topics, + unindexed_data, + )? + )), + ProofKind::FinTransfer => Ok(ProverResult::FinTransfer( + parse_polymer_event::( + chain_kind, + emitting_contract, + topics, + unindexed_data, + )? + )), + ProofKind::DeployToken => Ok(ProverResult::DeployToken( + parse_polymer_event::( + chain_kind, + emitting_contract, + topics, + unindexed_data, + )? + )), + ProofKind::LogMetadata => Ok(ProverResult::LogMetadata( + parse_polymer_event::( + chain_kind, + emitting_contract, + topics, + unindexed_data, + )? + )), + } +} + +/// Parse Polymer event from raw topics and unindexed data +fn parse_polymer_event>>( + chain_kind: ChainKind, + emitting_contract: &str, + topics_bytes: &[u8], + unindexed_data: &[u8], +) -> Result +where + >>::Error: std::fmt::Display, +{ + // Parse contract address + let address = parse_address(emitting_contract)?; + + // Split topics into 32-byte chunks + let topics: Vec> = topics_bytes + .chunks_exact(32) + .map(|chunk| { + let mut bytes = [0u8; 32]; + bytes.copy_from_slice(chunk); + FixedBytes::from(bytes) + }) + .collect(); + + // Create Log structure + let log = Log { + address, + data: LogData::new_unchecked( + topics, + Bytes::copy_from_slice(unindexed_data), + ), + }; + + // Decode and validate + V::try_from_log( + chain_kind, + T::decode_log(&log).map_err(stringify)?, + ) + .map_err(stringify) +} + +/// Parse InitTransfer event from Polymer-validated data +pub fn parse_init_transfer_event( + chain_id: u64, + emitting_contract: &str, + topics: &[u8], + unindexed_data: &[u8], +) -> Result { + let chain_kind = map_chain_id_to_kind(chain_id)?; + parse_polymer_event::( + chain_kind, + emitting_contract, + topics, + unindexed_data, + ) +} + +/// Parse FinTransfer event from Polymer-validated data +pub fn parse_fin_transfer_event( + chain_id: u64, + emitting_contract: &str, + topics: &[u8], + unindexed_data: &[u8], +) -> Result { + let chain_kind = map_chain_id_to_kind(chain_id)?; + parse_polymer_event::( + chain_kind, + emitting_contract, + topics, + unindexed_data, + ) +} + +/// Parse DeployToken event from Polymer-validated data +pub fn parse_deploy_token_event( + chain_id: u64, + emitting_contract: &str, + topics: &[u8], + unindexed_data: &[u8], +) -> Result { + let chain_kind = map_chain_id_to_kind(chain_id)?; + parse_polymer_event::( + chain_kind, + emitting_contract, + topics, + unindexed_data, + ) +} + +/// Parse LogMetadata event from Polymer-validated data +pub fn parse_log_metadata_event( + chain_id: u64, + emitting_contract: &str, + topics: &[u8], + unindexed_data: &[u8], +) -> Result { + let chain_kind = map_chain_id_to_kind(chain_id)?; + parse_polymer_event::( + chain_kind, + emitting_contract, + topics, + unindexed_data, + ) +} + +/// Map Polymer chain ID to ChainKind +fn map_chain_id_to_kind(chain_id: u64) -> Result { + match chain_id { + 1 => Ok(ChainKind::Eth), + 10 => Ok(ChainKind::Base), // Optimism - using Base as placeholder + 42161 => Ok(ChainKind::Arb), + 8453 => Ok(ChainKind::Base), + 56 => Ok(ChainKind::Bnb), + _ => Err(format!("Unsupported chain ID: {}", chain_id)), + } +} + +/// Parse emitting contract address string to Address +fn parse_address(contract: &str) -> Result { + // Remove "0x" prefix if present + let cleaned = contract.strip_prefix("0x").unwrap_or(contract); + + // Parse hex string to Address + cleaned + .parse::
() + .map_err(|e| format!("Invalid contract address: {}", e)) +} diff --git a/near/omni-types/src/polymer/mod.rs b/near/omni-types/src/polymer/mod.rs new file mode 100644 index 000000000..a9970c28f --- /dev/null +++ b/near/omni-types/src/polymer/mod.rs @@ -0,0 +1 @@ +pub mod events; diff --git a/near/omni-types/src/prover_args.rs b/near/omni-types/src/prover_args.rs index b5c203d52..c13b5f529 100644 --- a/near/omni-types/src/prover_args.rs +++ b/near/omni-types/src/prover_args.rs @@ -16,6 +16,16 @@ pub struct WormholeVerifyProofArgs { pub vaa: String, } +#[near(serializers=[borsh])] +#[derive(Debug, Clone)] +pub struct PolymerVerifyProofArgs { + pub proof_kind: ProofKind, + pub proof: Vec, + pub src_chain_id: u64, + pub src_block_number: u64, + pub global_log_index: u64, +} + #[near(serializers=[borsh, json])] #[derive(Default, Debug, Clone)] pub struct EvmProof { diff --git a/omni-relayer/Cargo.toml b/omni-relayer/Cargo.toml index 9de1324da..d2a372b7c 100644 --- a/omni-relayer/Cargo.toml +++ b/omni-relayer/Cargo.toml @@ -51,18 +51,19 @@ mongodb = "3.2.2" redis = { version = "0.32.5", features = ["aio", "tokio-comp", "connection-manager"] } reqwest = "0.12" -bridge-connector-common = { git = "https://github.com/Near-One/bridge-sdk-rs", package = "bridge-connector-common", rev = "da152725e6a267b7213a755e1ad069f563f23288" } -near-rpc-client = { git = "https://github.com/Near-One/bridge-sdk-rs", package = "near-rpc-client", rev = "da152725e6a267b7213a755e1ad069f563f23288" } +bridge-connector-common = { path = "../bridge-sdk-rs/bridge-sdk/connectors/bridge-connector-common" } +near-rpc-client = { path = "../bridge-sdk-rs/bridge-sdk/near-rpc-client" } -near-bridge-client = { git = "https://github.com/Near-One/bridge-sdk-rs", package = "near-bridge-client", rev = "da152725e6a267b7213a755e1ad069f563f23288" } -evm-bridge-client = { git = "https://github.com/Near-One/bridge-sdk-rs", package = "evm-bridge-client", rev = "da152725e6a267b7213a755e1ad069f563f23288" } -solana-bridge-client = { git = "https://github.com/Near-One/bridge-sdk-rs", package = "solana-bridge-client", rev = "da152725e6a267b7213a755e1ad069f563f23288" } -utxo-bridge-client = { git = "https://github.com/Near-One/bridge-sdk-rs", package = "utxo-bridge-client", rev = "da152725e6a267b7213a755e1ad069f563f23288" } -utxo-utils = { git = "https://github.com/Near-One/bridge-sdk-rs", package = "utxo-utils", rev = "da152725e6a267b7213a755e1ad069f563f23288" } -wormhole-bridge-client = { git = "https://github.com/Near-One/bridge-sdk-rs", package = "wormhole-bridge-client", rev = "da152725e6a267b7213a755e1ad069f563f23288" } -light-client = { git = "https://github.com/Near-One/bridge-sdk-rs", package = "light-client", rev = "da152725e6a267b7213a755e1ad069f563f23288" } +near-bridge-client = { path = "../bridge-sdk-rs/bridge-sdk/bridge-clients/near-bridge-client" } +evm-bridge-client = { path = "../bridge-sdk-rs/bridge-sdk/bridge-clients/evm-bridge-client" } +solana-bridge-client = { path = "../bridge-sdk-rs/bridge-sdk/bridge-clients/solana-bridge-client" } +utxo-bridge-client = { path = "../bridge-sdk-rs/bridge-sdk/bridge-clients/utxo-bridge-client" } +utxo-utils = { path = "../bridge-sdk-rs/bridge-sdk/utxo-utils" } +wormhole-bridge-client = { path = "../bridge-sdk-rs/bridge-sdk/bridge-clients/wormhole-bridge-client" } +polymer-bridge-client = { path = "../bridge-sdk-rs/bridge-sdk/bridge-clients/polymer-bridge-client" } +light-client = { path = "../bridge-sdk-rs/bridge-sdk/light-client" } -omni-connector = { git = "https://github.com/Near-One/bridge-sdk-rs", package = "omni-connector", rev = "da152725e6a267b7213a755e1ad069f563f23288" } +omni-connector = { path = "../bridge-sdk-rs/bridge-sdk/connectors/omni-connector" } # The profile that 'dist' will build with [profile.dist] diff --git a/omni-relayer/src/config.rs b/omni-relayer/src/config.rs index 31a81145f..283945a26 100644 --- a/omni-relayer/src/config.rs +++ b/omni-relayer/src/config.rs @@ -105,6 +105,7 @@ pub struct Config { pub btc: Option, pub zcash: Option, pub wormhole: Wormhole, + pub polymer: Option, } impl Config { @@ -277,3 +278,8 @@ pub struct Wormhole { pub api_url: String, pub solana_chain_id: u64, } + +#[derive(Debug, Clone, Deserialize)] +pub struct Polymer { + pub api_url: String, +} diff --git a/omni-relayer/src/startup/mod.rs b/omni-relayer/src/startup/mod.rs index 87c3b1d2d..a33d94faf 100644 --- a/omni-relayer/src/startup/mod.rs +++ b/omni-relayer/src/startup/mod.rs @@ -12,6 +12,7 @@ use solana_client::nonblocking::rpc_client::RpcClient; use tracing::info; use utxo_bridge_client::{AuthOptions, UTXOBridgeClient}; use wormhole_bridge_client::{WormholeBridgeClient, WormholeBridgeClientBuilder}; +use polymer_bridge_client::{PolymerBridgeClient, PolymerBridgeClientBuilder}; use crate::{ config::{self}, @@ -163,6 +164,19 @@ fn build_wormhole_bridge_client(config: &config::Config) -> Result Result> { + config + .polymer + .as_ref() + .map(|polymer| { + PolymerBridgeClientBuilder::default() + .endpoint(Some(polymer.api_url.clone())) + .build() + .context("Failed to build PolymerBridgeClient") + }) + .transpose() +} + fn build_light_client(config: &config::Config, chain: ChainKind) -> Result> { let light_client = match chain { ChainKind::Eth => config.eth.as_ref().and_then(|eth| eth.light_client.clone()), @@ -204,6 +218,7 @@ pub fn build_omni_connector( let btc_bridge_client = build_utxo_bridge_client(config, ChainKind::Btc)?; let zcash_bridge_client = build_utxo_bridge_client(config, ChainKind::Zcash)?; let wormhole_bridge_client = build_wormhole_bridge_client(config)?; + let polymer_bridge_client = build_polymer_bridge_client(config)?; let eth_light_client = build_light_client(config, ChainKind::Eth)?; let btc_light_client = build_light_client(config, ChainKind::Btc)?; let zcash_light_client = build_light_client(config, ChainKind::Zcash)?; @@ -217,6 +232,7 @@ pub fn build_omni_connector( .bnb_bridge_client(bnb_bridge_client) .solana_bridge_client(solana_bridge_client) .wormhole_bridge_client(Some(wormhole_bridge_client)) + .polymer_bridge_client(polymer_bridge_client) .btc_bridge_client(Some(btc_bridge_client)) .zcash_bridge_client(Some(zcash_bridge_client)) .eth_light_client(eth_light_client) diff --git a/omni-relayer/src/utils/evm.rs b/omni-relayer/src/utils/evm.rs index 087000efa..76159fba7 100644 --- a/omni-relayer/src/utils/evm.rs +++ b/omni-relayer/src/utils/evm.rs @@ -7,7 +7,7 @@ use anyhow::Result; use near_sdk::json_types::U128; use omni_types::{ ChainKind, H160, OmniAddress, - prover_args::{EvmVerifyProofArgs, WormholeVerifyProofArgs}, + prover_args::{EvmVerifyProofArgs, PolymerVerifyProofArgs, WormholeVerifyProofArgs}, prover_result::ProofKind, }; @@ -100,6 +100,36 @@ pub async fn construct_prover_args( borsh::to_vec(&evm_proof_args).ok() } +pub async fn construct_polymer_prover_args( + omni_connector: Arc, + tx_hash: H256, + proof_kind: ProofKind, + chain_id: u64, + log_index: u64, + block_number: u64, +) -> Option> { + let proof = match omni_connector + .polymer_get_proof(chain_id, format!("{tx_hash:?}"), log_index) + .await + { + Ok(proof) => proof, + Err(err) => { + warn!("Failed to get Polymer proof: {err:?}"); + return None; + } + }; + + let polymer_proof_args = PolymerVerifyProofArgs { + proof_kind, + proof, + src_chain_id: chain_id, + src_block_number: block_number, + global_log_index: log_index, + }; + + borsh::to_vec(&polymer_proof_args).ok() +} + pub fn string_to_evm_omniaddress(chain_kind: ChainKind, address: &str) -> Result { OmniAddress::new_from_evm_address( chain_kind,