-
Notifications
You must be signed in to change notification settings - Fork 17
[DRAFT] Add Polymer Prover Support #436
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
notbdu
wants to merge
4
commits into
Near-One:main
Choose a base branch
from
polymerdao:bo/polymer
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 2 commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)) | ||
| } | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> { | ||
| 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]) | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Amazing work, thanks!
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
@karim-en What chains do you want to support through polymer?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The EVM uses ABI message format, so I'm curious what is the non-evm format