Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions near/omni-prover/polymer-prover/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[package]
name = "polymer-prover"
version.workspace = true
authors = ["Near One <info@nearone.org>"]
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
144 changes: 144 additions & 0 deletions near/omni-prover/polymer-prover/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
use near_sdk::borsh::BorshDeserialize;
use near_sdk::{
env, ext_contract, near, near_bindgen, require, AccountId, Gas, PanicOnDefault, Promise,
PromiseError,
};
use omni_types::polymer::events::{
parse_deploy_token_event, parse_fin_transfer_event, parse_init_transfer_event,
parse_log_metadata_event,
};
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<u8>) -> (u32, String, Vec<u8>, Vec<u8>);
}

#[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<u8>) -> 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<u8>, Vec<u8>), PromiseError>,
) -> Result<ProverResult, String> {
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<ProverResult, String> {
// 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());
}

match proof_kind {
ProofKind::InitTransfer => {
let event = parse_init_transfer_event(chain_id, emitting_contract, topics, unindexed_data)?;
Ok(ProverResult::InitTransfer(event))
}
ProofKind::FinTransfer => {
let event = parse_fin_transfer_event(chain_id, emitting_contract, topics, unindexed_data)?;
Ok(ProverResult::FinTransfer(event))
}
ProofKind::DeployToken => {
let event = parse_deploy_token_event(chain_id, emitting_contract, topics, unindexed_data)?;
Ok(ProverResult::DeployToken(event))
}
ProofKind::LogMetadata => {
let event = parse_log_metadata_event(chain_id, emitting_contract, topics, unindexed_data)?;
Ok(ProverResult::LogMetadata(event))
}
}
}
}
1 change: 1 addition & 0 deletions near/omni-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
128 changes: 128 additions & 0 deletions near/omni-types/src/polymer/decoder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/// Utilities for decoding Polymer proof data
/// Includes helpers for parsing topics and ABI-decoding unindexed event data

/// Split concatenated topics bytes into individual 32-byte chunks
pub fn decode_topics(topics: &[u8]) -> Vec<[u8; 32]> {
topics
.chunks_exact(32)
.map(|chunk| {
let mut arr = [0u8; 32];
arr.copy_from_slice(chunk);
arr
})
.collect()
}

/// Extract Ethereum address from bytes32 (last 20 bytes)
pub fn bytes32_to_address(bytes: &[u8; 32]) -> [u8; 20] {
let mut addr = [0u8; 20];
addr.copy_from_slice(&bytes[12..32]);
addr
}

/// Extract u128 from bytes32
pub fn bytes32_to_u128(bytes: &[u8; 32]) -> u128 {
u128::from_be_bytes([
bytes[16], bytes[17], bytes[18], bytes[19], bytes[20], bytes[21], bytes[22], bytes[23],
bytes[24], bytes[25], bytes[26], bytes[27], bytes[28], bytes[29], bytes[30], bytes[31],
])
}

/// Extract u64 from bytes32
pub fn bytes32_to_u64(bytes: &[u8; 32]) -> u64 {
u64::from_be_bytes([
bytes[24], bytes[25], bytes[26], bytes[27], bytes[28], bytes[29], bytes[30], bytes[31],
])
}

/// Parse string from ABI-encoded bytes
/// Format: offset (32 bytes) + length (32 bytes) + data (padded to 32-byte chunks)
pub fn decode_string_from_abi(data: &[u8], offset: usize) -> Result<String, String> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this low level decoder? Why not just to use alloy like here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing work, thanks!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we going to support other non-EVM chains?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we going to support other non-EVM chains?

shouldn't be hard to implement if we already have evm examples

@karim-en What chains do you want to support through polymer?

Copy link
Collaborator

@karim-en karim-en Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't be hard to implement if we already have evm examples

The EVM uses ABI message format, so I'm curious what is the non-evm format

if data.len() < offset + 32 {
return Err("Data too short for string length".to_string());
}

// Read length at offset
let length = u64::from_be_bytes([
data[offset + 24],
data[offset + 25],
data[offset + 26],
data[offset + 27],
data[offset + 28],
data[offset + 29],
data[offset + 30],
data[offset + 31],
]) as usize;

if data.len() < offset + 32 + length {
return Err("Data too short for string content".to_string());
}

// Read string data
let string_bytes = &data[offset + 32..offset + 32 + length];
String::from_utf8(string_bytes.to_vec()).map_err(|e| format!("Invalid UTF-8: {}", e))
}

/// Parse address from ABI-encoded bytes at given offset
pub fn decode_address_from_abi(data: &[u8], offset: usize) -> Result<[u8; 20], String> {
if data.len() < offset + 32 {
return Err("Data too short for address".to_string());
}

let mut addr = [0u8; 20];
addr.copy_from_slice(&data[offset + 12..offset + 32]);
Ok(addr)
}

/// Parse u128 from ABI-encoded bytes at given offset
pub fn decode_u128_from_abi(data: &[u8], offset: usize) -> Result<u128, String> {
if data.len() < offset + 32 {
return Err("Data too short for u128".to_string());
}

Ok(u128::from_be_bytes([
data[offset + 16],
data[offset + 17],
data[offset + 18],
data[offset + 19],
data[offset + 20],
data[offset + 21],
data[offset + 22],
data[offset + 23],
data[offset + 24],
data[offset + 25],
data[offset + 26],
data[offset + 27],
data[offset + 28],
data[offset + 29],
data[offset + 30],
data[offset + 31],
]))
}

/// Parse u64 from ABI-encoded bytes at given offset
pub fn decode_u64_from_abi(data: &[u8], offset: usize) -> Result<u64, String> {
if data.len() < offset + 32 {
return Err("Data too short for u64".to_string());
}

Ok(u64::from_be_bytes([
data[offset + 24],
data[offset + 25],
data[offset + 26],
data[offset + 27],
data[offset + 28],
data[offset + 29],
data[offset + 30],
data[offset + 31],
]))
}

/// Parse u8 from ABI-encoded bytes at given offset
pub fn decode_u8_from_abi(data: &[u8], offset: usize) -> Result<u8, String> {
if data.len() < offset + 32 {
return Err("Data too short for u8".to_string());
}

Ok(data[offset + 31])
}
Loading