From b22bf8f0869fbae0e66a4c73c7cbca7c7841e8e4 Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Mon, 25 Aug 2025 13:24:39 -0700 Subject: [PATCH 01/34] move contract definitions to primitives --- bedrock/src/primitives/contracts.rs | 361 ++++++++++++++++++ bedrock/src/primitives/mod.rs | 3 + bedrock/src/smart_account/mod.rs | 6 +- bedrock/src/smart_account/transaction_4337.rs | 320 ++-------------- bedrock/src/transaction/mod.rs | 2 +- 5 files changed, 391 insertions(+), 301 deletions(-) create mode 100644 bedrock/src/primitives/contracts.rs diff --git a/bedrock/src/primitives/contracts.rs b/bedrock/src/primitives/contracts.rs new file mode 100644 index 00000000..ee14642c --- /dev/null +++ b/bedrock/src/primitives/contracts.rs @@ -0,0 +1,361 @@ +use alloy::sol; +use alloy::primitives::{Address, Bytes, FixedBytes, keccak256, aliases::U48}; +use alloy::sol_types::SolValue; +use ruint::aliases::U256; +use std::{str::FromStr, sync::LazyLock}; +use crate::primitives::{PrimitiveError, HttpError}; +use crate::transaction::rpc::SponsorUserOperationResponse; +use alloy::hex::FromHex; + +/// +static SAFE_OP_TYPEHASH: LazyLock> = LazyLock::new(|| { + FixedBytes::from_hex( + "0xc03dfc11d8b10bf9cf703d558958c8c42777f785d998c62060d85a4f0ef6ea7f", + ) + .expect("error initializing `SAFE_OP_TYPEHASH`") +}); + +/// v0.7 `EntryPoint` +pub static ENTRYPOINT_4337: LazyLock
= LazyLock::new(|| { + Address::from_str("0x0000000071727De22E5E9d8BAf0edAc6f37da032") + .expect("failed to decode ENTRYPOINT_4337") +}); + +/// Multichain address for the v0.3.0 `Safe4337Module` +pub static GNOSIS_SAFE_4337_MODULE: LazyLock
= LazyLock::new(|| { + Address::from_str("0x75cf11467937ce3f2f357ce24ffc3dbf8fd5c226") + .expect("failed to decode GNOSIS_SAFE_4337_MODULE") +}); + +/// The length of a 4337 `UserOperation` signature. +/// +/// This is the length of a regular ECDSA signature with r,s,v (32 + 32 + 1 = 65 bytes) + 12 bytes for the validity timestamps. +const USER_OPERATION_SIGNATURE_LENGTH: usize = 77; + +// --- JSON serialization helpers for ERC-4337 --- + +fn serialize_u128_as_hex( + value: &u128, + serializer: S, +) -> Result { + // Always hex with 0x prefix per spec + let s = format!("0x{value:x}"); + serializer.serialize_str(&s) +} + +fn serialize_u256_as_hex( + value: &U256, + serializer: S, +) -> Result { + let s = format!("0x{value:x}"); + serializer.serialize_str(&s) +} + +sol! { + + /// Interface for the `Safe4337Module` contract. + /// + /// Reference: + #[sol(all_derives)] + interface ISafe4337Module { + function executeUserOp(address to, uint256 value, bytes calldata data, uint8 operation) external; + } + + /// The structure of a generic 4337 `UserOperation`. + /// + /// `UserOperation`s are not used on-chain, they are used by RPCs to bundle transactions as `PackedUserOperation`s. + /// + /// For the flow of World App: + /// - A `UserOperation` is created by the user and passed to the World App RPC to request sponsorship through the `wa_sponsorUserOperation` method. + /// - The final signed `UserOperation` is then passed to the World App RPC to be executed through the standard `eth_sendUserOperation` method. + /// + /// Reference: + #[sol(rename_all = "camelcase")] + #[derive(Debug, Default, serde::Serialize, serde::Deserialize)] + #[serde(rename_all = "camelCase")] + struct UserOperation { + /// The Account making the `UserOperation` + address sender; + /// Anti-replay protection + #[serde(serialize_with = "serialize_u256_as_hex")] + uint256 nonce; + /// Account Factory for new Accounts OR `0x7702` flag for EIP-7702 Accounts, otherwise address(0) + address factory; + /// Data for the Account Factory if factory is provided OR EIP-7702 initialization data, or empty array + bytes factory_data; + /// The data to pass to the sender during the main execution call + bytes call_data; + /// Gas limit for the main execution call. + /// Even though the type is `uint256`, in the `Safe4337Module` (see `EncodedSafeOpStruct`), it is `uint128`. We enforce `uint128` to avoid overflows. + #[serde(serialize_with = "serialize_u128_as_hex")] + uint128 call_gas_limit; + /// Gas limit for the verification call + /// Even though the type is `uint256`, in the `Safe4337Module` (see `EncodedSafeOpStruct`), it is `uint128`. We enforce `uint128` to avoid overflows. + #[serde(serialize_with = "serialize_u128_as_hex")] + uint128 verification_gas_limit; + /// Extra gas to pay the bundler + #[serde(serialize_with = "serialize_u256_as_hex")] + uint256 pre_verification_gas; + /// Maximum fee per gas (similar to [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) `max_fee_per_gas`) + /// Even though the type is `uint256`, in the `Safe4337Module` (see `EncodedSafeOpStruct`), it is `uint128`. We enforce `uint128` to avoid overflows. + #[serde(serialize_with = "serialize_u128_as_hex")] + uint128 max_fee_per_gas; + /// Maximum priority fee per gas (similar to [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) `max_priority_fee_per_gas`) + /// Even though the type is `uint256`, in the `Safe4337Module` (see `EncodedSafeOpStruct`), it is `uint128`. We enforce `uint128` to avoid overflows. + #[serde(serialize_with = "serialize_u128_as_hex")] + uint128 max_priority_fee_per_gas; + /// Address of paymaster contract, (or empty, if the sender pays for gas by itself) + address paymaster; + /// The amount of gas to allocate for the paymaster validation code (only if paymaster exists) + /// Even though the type is `uint256`, in the `Safe4337Module` (see `EncodedSafeOpStruct`), it is expected as `uint128` for `paymasterAndData` validation. + #[serde(serialize_with = "serialize_u128_as_hex")] + uint128 paymaster_verification_gas_limit; + /// The amount of gas to allocate for the paymaster post-operation code (only if paymaster exists) + /// Even though the type is `uint256`, in the `Safe4337Module` (see `EncodedSafeOpStruct`), it is expected as `uint128` for `paymasterAndData` validation. + #[serde(serialize_with = "serialize_u128_as_hex")] + uint128 paymaster_post_op_gas_limit; + /// Data for paymaster (only if paymaster exists) + bytes paymaster_data; + /// Data passed into the sender to verify authorization + bytes signature; + } + + /// The EIP-712 type-hash for a `SafeOp`, representing the structure of a User Operation for the Safe. + /// + /// Reference: + #[sol(rename_all = "camelcase")] + struct EncodedSafeOpStruct { + bytes32 type_hash; + address safe; + uint256 nonce; + bytes32 init_code_hash; + bytes32 call_data_hash; + uint128 verification_gas_limit; + uint128 call_gas_limit; + uint256 pre_verification_gas; + uint128 max_priority_fee_per_gas; + uint128 max_fee_per_gas; + bytes32 paymaster_and_data_hash; + uint48 valid_after; + uint48 valid_until; + address entry_point; + } +} + +impl UserOperation { + /// Initializes a new `UserOperation` with default values. + /// + /// In particular, it sets default values for gas limits & fees, paymaster and sets a dummy signature. + pub fn new_with_defaults(sender: Address, nonce: U256, call_data: Bytes) -> Self { + Self { + sender, + nonce, + call_data, + signature: vec![0xff; USER_OPERATION_SIGNATURE_LENGTH].into(), + ..Default::default() + } + } + + /// Gathers the factory+factoryData as `initCode`. + pub fn get_init_code(&self) -> Bytes { + // Check if `factory` is present + if self.factory.is_zero() { + return Bytes::new(); + } + + let mut out = Vec::new(); + out.extend_from_slice(self.factory.as_slice()); + out.extend_from_slice(&self.factory_data); + out.into() + } + + /// Extract `validAfter` and `validUntil` from a signature as `U256` values. + /// + /// Expects at least 12 bytes additional bytes in the signature. + /// + /// # Errors + /// - Returns an error if the signature is too short. + pub fn extract_validity_timestamps(&self) -> Result<(U48, U48), PrimitiveError> { + // timestamp validity (12 bytes) + regular ECDSA signature (65 bytes) + if self.signature.len() != 77 { + return Err(PrimitiveError::InvalidInput { + attribute: "signature", + message: "signature does not have the correct length (77 bytes)" + .to_string(), + }); + } + + let mut valid_after = [0u8; 6]; + let mut valid_until = [0u8; 6]; + + valid_after.copy_from_slice(&self.signature[0..6]); + valid_until.copy_from_slice(&self.signature[6..12]); + + // Extract 6-byte validAfter and validUntil slices and convert them to U256 + let valid_after = U48::from_be_bytes(valid_after); + let valid_until = U48::from_be_bytes(valid_until); + + Ok((valid_after, valid_until)) + } + + /// Merges all paymaster related data into a single `paymasterAndData` attribute. + pub fn get_paymaster_and_data(&self) -> Bytes { + if self.paymaster.is_zero() { + return Bytes::new(); + } + + let mut out = Vec::new(); + // Append paymaster address (20 bytes) + out.extend_from_slice(self.paymaster.as_slice()); + + // Append paymasterVerificationGasLimit (16 bytes) + out.extend_from_slice(&self.paymaster_verification_gas_limit.to_be_bytes()); + + // Append paymasterPostOpGasLimit (16 bytes) + out.extend_from_slice(&self.paymaster_post_op_gas_limit.to_be_bytes()); + + // Append paymasterData if it exists + if !self.paymaster_data.is_empty() { + out.extend_from_slice(&self.paymaster_data); + } + + out.into() + } + + /// Merges paymaster data from sponsorship response into the `UserOperation` + /// + /// # Errors + /// Returns an error if any U128 to u128 conversion fails + pub fn with_paymaster_data( + mut self, + sponsor_response: SponsorUserOperationResponse, + ) -> Result { + self.paymaster = sponsor_response.paymaster; + self.paymaster_data = sponsor_response.paymaster_data; + self.paymaster_verification_gas_limit = sponsor_response + .paymaster_verification_gas_limit + .try_into() + .unwrap_or(0); + self.paymaster_post_op_gas_limit = sponsor_response + .paymaster_post_op_gas_limit + .try_into() + .unwrap_or(0); + + // Update gas fields if they were estimated by the RPC + if self.pre_verification_gas.is_zero() { + self.pre_verification_gas = sponsor_response.pre_verification_gas; + } + if self.verification_gas_limit == 0 { + self.verification_gas_limit = sponsor_response + .verification_gas_limit + .try_into() + .unwrap_or(0); + } + if self.call_gas_limit == 0 { + self.call_gas_limit = + sponsor_response.call_gas_limit.try_into().unwrap_or(0); + } + if self.max_fee_per_gas == 0 { + self.max_fee_per_gas = + sponsor_response.max_fee_per_gas.try_into().unwrap_or(0); + } + if self.max_priority_fee_per_gas == 0 { + self.max_priority_fee_per_gas = sponsor_response + .max_priority_fee_per_gas + .try_into() + .unwrap_or(0); + } + + Ok(self) + } +} + +impl EncodedSafeOpStruct { + /// Builds an `EncodedSafeOpStruct` from a `UserOperation`, injecting explicit validity timestamps. + /// + /// # Errors + /// Returns `PrimitiveError` if hashing or conversions fail when deriving fields + /// from the provided `user_op`. Currently this can occur if internal helpers + /// on `user_op` return invalid data for hashing. + pub fn from_user_op_with_validity( + user_op: &UserOperation, + valid_after: U48, + valid_until: U48, + ) -> Result { + Ok(Self { + type_hash: *SAFE_OP_TYPEHASH, + safe: user_op.sender, + nonce: user_op.nonce, + init_code_hash: keccak256(user_op.get_init_code()), + call_data_hash: keccak256(&user_op.call_data), + verification_gas_limit: user_op.verification_gas_limit, + call_gas_limit: user_op.call_gas_limit, + pre_verification_gas: user_op.pre_verification_gas, + max_priority_fee_per_gas: user_op.max_priority_fee_per_gas, + max_fee_per_gas: user_op.max_fee_per_gas, + paymaster_and_data_hash: keccak256(user_op.get_paymaster_and_data()), + valid_after, + valid_until, + entry_point: *ENTRYPOINT_4337, + }) + } + + /// computes the hash of the userOp + #[must_use] + pub fn into_transaction_hash(self) -> FixedBytes<32> { + keccak256(self.abi_encode()) + } +} + +sol! { + contract IMulticall3 { + #[derive(Default)] + struct Call3 { + address target; + bool allowFailure; + bytes callData; + } + } + + contract IEntryPoint { + #[derive(Default, serde::Serialize, serde::Deserialize, Debug)] + struct PackedUserOperation { + address sender; + uint256 nonce; + bytes initCode; + bytes callData; + bytes32 accountGasLimits; + uint256 preVerificationGas; + bytes32 gasFees; + bytes paymasterAndData; + bytes signature; + } + + #[derive(Default)] + struct UserOpsPerAggregator { + PackedUserOperation[] userOps; + address aggregator; + bytes signature; + } + } + + contract IPBHEntryPoint { + #[derive(Default)] + struct PBHPayload { + uint256 root; + uint256 pbhExternalNullifier; + uint256 nullifierHash; + uint256[8] proof; + } + + function handleAggregatedOps( + IEntryPoint.UserOpsPerAggregator[] calldata, + address payable + ) external; + + function pbhMulticall( + IMulticall3.Call3[] calls, + PBHPayload payload, + ) external; + } +} diff --git a/bedrock/src/primitives/mod.rs b/bedrock/src/primitives/mod.rs index d3de747b..00821997 100644 --- a/bedrock/src/primitives/mod.rs +++ b/bedrock/src/primitives/mod.rs @@ -59,6 +59,9 @@ pub mod http_client; #[cfg(feature = "tooling_tests")] pub mod tooling_tests; +/// Contract interfaces and data structures for ERC-4337 account abstraction +pub mod contracts; + /// Supported blockchain networks for Bedrock operations #[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)] #[repr(u32)] diff --git a/bedrock/src/smart_account/mod.rs b/bedrock/src/smart_account/mod.rs index 93fa4f05..459c5e03 100644 --- a/bedrock/src/smart_account/mod.rs +++ b/bedrock/src/smart_account/mod.rs @@ -6,7 +6,7 @@ use alloy::{ signers::{k256::ecdsa::SigningKey, local::LocalSigner}, }; pub use signer::SafeSmartAccountSigner; -pub use transaction_4337::{ISafe4337Module, Is4337Encodable}; +pub use transaction_4337::Is4337Encodable; #[cfg(any(test, doc))] use crate::primitives::Network; @@ -32,8 +32,8 @@ mod transaction; /// Reference: mod permit2; -pub use transaction_4337::{ - EncodedSafeOpStruct, UserOperation, ENTRYPOINT_4337, GNOSIS_SAFE_4337_MODULE, +pub use crate::primitives::contracts::{ + EncodedSafeOpStruct, UserOperation, ENTRYPOINT_4337, GNOSIS_SAFE_4337_MODULE, ISafe4337Module, }; pub use nonce::{InstructionFlag, NonceKeyV1, TransactionTypeId}; diff --git a/bedrock/src/smart_account/transaction_4337.rs b/bedrock/src/smart_account/transaction_4337.rs index be1490d6..264014e9 100644 --- a/bedrock/src/smart_account/transaction_4337.rs +++ b/bedrock/src/smart_account/transaction_4337.rs @@ -3,46 +3,17 @@ //! A transaction can be initialized through a `UserOperation` struct. //! -use crate::primitives::{HttpError, Network, PrimitiveError}; +use crate::primitives::{Network, PrimitiveError}; +use crate::primitives::contracts::{UserOperation, EncodedSafeOpStruct}; use crate::smart_account::SafeSmartAccountSigner; use crate::transaction::rpc::{RpcError, SponsorUserOperationResponse}; -use alloy::hex::FromHex; use alloy::{ - primitives::{aliases::U48, keccak256, Address, Bytes, FixedBytes}, - sol, - sol_types::SolValue, + primitives::{aliases::U48, Address, Bytes, FixedBytes}, }; use chrono::{Duration, Utc}; -use ruint::aliases::U256; -use std::{str::FromStr, sync::LazyLock}; - -use serde::Serializer; - -/// -static SAFE_OP_TYPEHASH: LazyLock> = LazyLock::new(|| { - FixedBytes::from_hex( - "0xc03dfc11d8b10bf9cf703d558958c8c42777f785d998c62060d85a4f0ef6ea7f", - ) - .expect("error initializing `SAFE_OP_TYPEHASH`") -}); - -/// v0.7 `EntryPoint` -pub static ENTRYPOINT_4337: LazyLock
= LazyLock::new(|| { - Address::from_str("0x0000000071727De22E5E9d8BAf0edAc6f37da032") - .expect("failed to decode ENTRYPOINT_4337") -}); - -/// Multichain address for the v0.3.0 `Safe4337Module` -pub static GNOSIS_SAFE_4337_MODULE: LazyLock
= LazyLock::new(|| { - Address::from_str("0x75cf11467937ce3f2f357ce24ffc3dbf8fd5c226") - .expect("failed to decode GNOSIS_SAFE_4337_MODULE") -}); - -/// The length of a 4337 `UserOperation` signature. -/// -/// This is the length of a regular ECDSA signature with r,s,v (32 + 32 + 1 = 65 bytes) + 12 bytes for the validity timestamps. -const USER_OPERATION_SIGNATURE_LENGTH: usize = 77; + +use crate::primitives::contracts::{ENTRYPOINT_4337, GNOSIS_SAFE_4337_MODULE}; /// The default validity duration for 4337 `UserOperation` signatures. /// @@ -103,6 +74,7 @@ pub trait Is4337Encodable { safe_account: &crate::smart_account::SafeSmartAccount, self_sponsor_token: Option
, metadata: Option, + _pbh: bool ) -> Result, RpcError> { // Get the global RPC client let rpc_client = crate::transaction::rpc::get_rpc_client()?; @@ -166,285 +138,39 @@ pub trait Is4337Encodable { Ok(user_op_hash) } -} - -sol! { - /// Interface for the `Safe4337Module` contract. - /// - /// Reference: - interface ISafe4337Module { - function executeUserOp(address to, uint256 value, bytes calldata data, uint8 operation) external; - } - /// The structure of a generic 4337 `UserOperation`. + /// Generates a Privacy-Preserving Biometric Hash (PBH) proof for account abstraction transactions. /// - /// `UserOperation`s are not used on-chain, they are used by RPCs to bundle transactions as `PackedUserOperation`s. + /// This function is intended to create cryptographic proofs that verify human uniqueness + /// without revealing biometric data, as part of the Person Bound Transactions (PBT) system. /// - /// For the flow of World App: - /// - A `UserOperation` is created by the user and passed to the World App RPC to request sponsorship through the `wa_sponsorUserOperation` method. - /// - The final signed `UserOperation` is then passed to the World App RPC to be executed through the standard `eth_sendUserOperation` method. - /// - /// Reference: - #[sol(rename_all = "camelcase")] - #[derive(Debug, Default, serde::Serialize, serde::Deserialize)] - #[serde(rename_all = "camelCase")] - struct UserOperation { - /// The Account making the `UserOperation` - address sender; - /// Anti-replay protection - #[serde(serialize_with = "serialize_u256_as_hex")] - uint256 nonce; - /// Account Factory for new Accounts OR `0x7702` flag for EIP-7702 Accounts, otherwise address(0) - address factory; - /// Data for the Account Factory if factory is provided OR EIP-7702 initialization data, or empty array - bytes factory_data; - /// The data to pass to the sender during the main execution call - bytes call_data; - /// Gas limit for the main execution call. - /// Even though the type is `uint256`, in the `Safe4337Module` (see `EncodedSafeOpStruct`), it is `uint128`. We enforce `uint128` to avoid overflows. - #[serde(serialize_with = "serialize_u128_as_hex")] - uint128 call_gas_limit; - /// Gas limit for the verification call - /// Even though the type is `uint256`, in the `Safe4337Module` (see `EncodedSafeOpStruct`), it is `uint128`. We enforce `uint128` to avoid overflows. - #[serde(serialize_with = "serialize_u128_as_hex")] - uint128 verification_gas_limit; - /// Extra gas to pay the bundler - #[serde(serialize_with = "serialize_u256_as_hex")] - uint256 pre_verification_gas; - /// Maximum fee per gas (similar to [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) `max_fee_per_gas`) - /// Even though the type is `uint256`, in the `Safe4337Module` (see `EncodedSafeOpStruct`), it is `uint128`. We enforce `uint128` to avoid overflows. - #[serde(serialize_with = "serialize_u128_as_hex")] - uint128 max_fee_per_gas; - /// Maximum priority fee per gas (similar to [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) `max_priority_fee_per_gas`) - /// Even though the type is `uint256`, in the `Safe4337Module` (see `EncodedSafeOpStruct`), it is `uint128`. We enforce `uint128` to avoid overflows. - #[serde(serialize_with = "serialize_u128_as_hex")] - uint128 max_priority_fee_per_gas; - /// Address of paymaster contract, (or empty, if the sender pays for gas by itself) - address paymaster; - /// The amount of gas to allocate for the paymaster validation code (only if paymaster exists) - /// Even though the type is `uint256`, in the `Safe4337Module` (see `EncodedSafeOpStruct`), it is expected as `uint128` for `paymasterAndData` validation. - #[serde(serialize_with = "serialize_u128_as_hex")] - uint128 paymaster_verification_gas_limit; - /// The amount of gas to allocate for the paymaster post-operation code (only if paymaster exists) - /// Even though the type is `uint256`, in the `Safe4337Module` (see `EncodedSafeOpStruct`), it is expected as `uint128` for `paymasterAndData` validation. - #[serde(serialize_with = "serialize_u128_as_hex")] - uint128 paymaster_post_op_gas_limit; - /// Data for paymaster (only if paymaster exists) - bytes paymaster_data; - /// Data passed into the sender to verify authorization - bytes signature; - } - - /// The EIP-712 type-hash for a `SafeOp`, representing the structure of a User Operation for the Safe. - /// - /// Reference: - #[sol(rename_all = "camelcase")] - struct EncodedSafeOpStruct { - bytes32 type_hash; - address safe; - uint256 nonce; - bytes32 init_code_hash; - bytes32 call_data_hash; - uint128 verification_gas_limit; - uint128 call_gas_limit; - uint256 pre_verification_gas; - uint128 max_priority_fee_per_gas; - uint128 max_fee_per_gas; - bytes32 paymaster_and_data_hash; - uint48 valid_after; - uint48 valid_until; - address entry_point; - } -} - -impl UserOperation { - /// Initializes a new `UserOperation` with default values. - /// - /// In particular, it sets default values for gas limits & fees, paymaster and sets a dummy signature. - pub fn new_with_defaults(sender: Address, nonce: U256, call_data: Bytes) -> Self { - Self { - sender, - nonce, - call_data, - signature: vec![0xff; USER_OPERATION_SIGNATURE_LENGTH].into(), - ..Default::default() - } - } - - /// Gathers the factory+factoryData as `initCode`. - pub fn get_init_code(&self) -> Bytes { - // Check if `factory` is present - if self.factory.is_zero() { - return Bytes::new(); - } - - let mut out = Vec::new(); - out.extend_from_slice(self.factory.as_slice()); - out.extend_from_slice(&self.factory_data); - out.into() - } - - /// Extract `validAfter` and `validUntil` from a signature as `U256` values. - /// - /// Expects at least 12 bytes additional bytes in the signature. - /// - /// # Errors - /// - Returns an error if the signature is too short. - pub fn extract_validity_timestamps(&self) -> Result<(U48, U48), PrimitiveError> { - // timestamp validity (12 bytes) + regular ECDSA signature (65 bytes) - if self.signature.len() != 77 { - return Err(PrimitiveError::InvalidInput { - attribute: "signature", - message: "signature does not have the correct length (77 bytes)" - .to_string(), - }); - } - - let mut valid_after = [0u8; 6]; - let mut valid_until = [0u8; 6]; - - valid_after.copy_from_slice(&self.signature[0..6]); - valid_until.copy_from_slice(&self.signature[6..12]); - - // Extract 6-byte validAfter and validUntil slices and convert them to U256 - let valid_after = U48::from_be_bytes(valid_after); - let valid_until = U48::from_be_bytes(valid_until); - - Ok((valid_after, valid_until)) - } - - /// Merges all paymaster related data into a single `paymasterAndData` attribute. - pub fn get_paymaster_and_data(&self) -> Bytes { - if self.paymaster.is_zero() { - return Bytes::new(); - } - - let mut out = Vec::new(); - // Append paymaster address (20 bytes) - out.extend_from_slice(self.paymaster.as_slice()); - - // Append paymasterVerificationGasLimit (16 bytes) - out.extend_from_slice(&self.paymaster_verification_gas_limit.to_be_bytes()); - - // Append paymasterPostOpGasLimit (16 bytes) - out.extend_from_slice(&self.paymaster_post_op_gas_limit.to_be_bytes()); - - // Append paymasterData if it exists - if !self.paymaster_data.is_empty() { - out.extend_from_slice(&self.paymaster_data); - } - - out.into() - } - - /// Merges paymaster data from sponsorship response into the `UserOperation` + /// # Returns + /// * `Result` - The generated PBH proof bytes on success /// /// # Errors - /// Returns an error if any U128 to u128 conversion fails - pub fn with_paymaster_data( - mut self, - sponsor_response: SponsorUserOperationResponse, - ) -> Result { - self.paymaster = sponsor_response.paymaster; - self.paymaster_data = sponsor_response.paymaster_data; - self.paymaster_verification_gas_limit = sponsor_response - .paymaster_verification_gas_limit - .try_into() - .unwrap_or(0); - self.paymaster_post_op_gas_limit = sponsor_response - .paymaster_post_op_gas_limit - .try_into() - .unwrap_or(0); - - // Update gas fields if they were estimated by the RPC - if self.pre_verification_gas.is_zero() { - self.pre_verification_gas = sponsor_response.pre_verification_gas; - } - if self.verification_gas_limit == 0 { - self.verification_gas_limit = sponsor_response - .verification_gas_limit - .try_into() - .unwrap_or(0); - } - if self.call_gas_limit == 0 { - self.call_gas_limit = - sponsor_response.call_gas_limit.try_into().unwrap_or(0); - } - if self.max_fee_per_gas == 0 { - self.max_fee_per_gas = - sponsor_response.max_fee_per_gas.try_into().unwrap_or(0); - } - if self.max_priority_fee_per_gas == 0 { - self.max_priority_fee_per_gas = sponsor_response - .max_priority_fee_per_gas - .try_into() - .unwrap_or(0); - } - - Ok(self) - } -} - -impl EncodedSafeOpStruct { - /// Builds an `EncodedSafeOpStruct` from a `UserOperation`, injecting explicit validity timestamps. + /// * Returns `PrimitiveError` if proof generation fails /// - /// # Errors - /// Returns `PrimitiveError` if hashing or conversions fail when deriving fields - /// from the provided `user_op`. Currently this can occur if internal helpers - /// on `user_op` return invalid data for hashing. - pub fn from_user_op_with_validity( - user_op: &UserOperation, - valid_after: U48, - valid_until: U48, - ) -> Result { - Ok(Self { - type_hash: *SAFE_OP_TYPEHASH, - safe: user_op.sender, - nonce: user_op.nonce, - init_code_hash: keccak256(user_op.get_init_code()), - call_data_hash: keccak256(&user_op.call_data), - verification_gas_limit: user_op.verification_gas_limit, - call_gas_limit: user_op.call_gas_limit, - pre_verification_gas: user_op.pre_verification_gas, - max_priority_fee_per_gas: user_op.max_priority_fee_per_gas, - max_fee_per_gas: user_op.max_fee_per_gas, - paymaster_and_data_hash: keccak256(user_op.get_paymaster_and_data()), - valid_after, - valid_until, - entry_point: *ENTRYPOINT_4337, - }) + /// # Note + /// This is currently unimplemented and will return a todo!() error. + fn generate_pbh_proof() -> Result { + todo!("PBH proof generation not yet implemented") } + + // pub fn hash_user_op(user_op: &PackedUserOperation) -> Field { + // let hash = SolValue::abi_encode_packed(&(&user_op.sender, &user_op.nonce, &user_op.callData)); - /// computes the hash of the userOp - #[must_use] - pub fn into_transaction_hash(self) -> FixedBytes<32> { - keccak256(self.abi_encode()) - } -} - -// --- JSON serialization helpers for ERC-7769 --- + // hash_to_field(hash.as_slice()) + // } -fn serialize_u128_as_hex( - value: &u128, - serializer: S, -) -> Result { - // Always hex with 0x prefix per spec - let s = format!("0x{value:x}"); - serializer.serialize_str(&s) -} -fn serialize_u256_as_hex( - value: &U256, - serializer: S, -) -> Result { - let s = format!("0x{value:x}"); - serializer.serialize_str(&s) } #[cfg(test)] mod tests { use alloy::primitives::{address, U128}; + use ruint::aliases::U256; + use std::str::FromStr; use super::*; use crate::{ diff --git a/bedrock/src/transaction/mod.rs b/bedrock/src/transaction/mod.rs index ed12a695..e4ad6bb1 100644 --- a/bedrock/src/transaction/mod.rs +++ b/bedrock/src/transaction/mod.rs @@ -75,7 +75,7 @@ impl SafeSmartAccount { // Sign and execute the transaction (uses global RPC client automatically) let user_op_hash = transaction - .sign_and_execute(network, self, None, None) + .sign_and_execute(network, self, None, None, false) .await .map_err(|e| TransactionError::Generic { message: format!("Failed to execute transaction: {e}"), From a69b040d4c344c1dad1c2ea206fda1b016b80972 Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Mon, 25 Aug 2025 13:26:53 -0700 Subject: [PATCH 02/34] remove pbh logic --- bedrock/src/primitives/contracts.rs | 8 ++--- bedrock/src/smart_account/mod.rs | 3 +- bedrock/src/smart_account/transaction_4337.rs | 33 ++----------------- bedrock/src/transaction/mod.rs | 2 +- 4 files changed, 9 insertions(+), 37 deletions(-) diff --git a/bedrock/src/primitives/contracts.rs b/bedrock/src/primitives/contracts.rs index ee14642c..361f6a6a 100644 --- a/bedrock/src/primitives/contracts.rs +++ b/bedrock/src/primitives/contracts.rs @@ -1,11 +1,11 @@ +use crate::primitives::{HttpError, PrimitiveError}; +use crate::transaction::rpc::SponsorUserOperationResponse; +use alloy::hex::FromHex; +use alloy::primitives::{aliases::U48, keccak256, Address, Bytes, FixedBytes}; use alloy::sol; -use alloy::primitives::{Address, Bytes, FixedBytes, keccak256, aliases::U48}; use alloy::sol_types::SolValue; use ruint::aliases::U256; use std::{str::FromStr, sync::LazyLock}; -use crate::primitives::{PrimitiveError, HttpError}; -use crate::transaction::rpc::SponsorUserOperationResponse; -use alloy::hex::FromHex; /// static SAFE_OP_TYPEHASH: LazyLock> = LazyLock::new(|| { diff --git a/bedrock/src/smart_account/mod.rs b/bedrock/src/smart_account/mod.rs index 459c5e03..b840537d 100644 --- a/bedrock/src/smart_account/mod.rs +++ b/bedrock/src/smart_account/mod.rs @@ -33,7 +33,8 @@ mod transaction; mod permit2; pub use crate::primitives::contracts::{ - EncodedSafeOpStruct, UserOperation, ENTRYPOINT_4337, GNOSIS_SAFE_4337_MODULE, ISafe4337Module, + EncodedSafeOpStruct, ISafe4337Module, UserOperation, ENTRYPOINT_4337, + GNOSIS_SAFE_4337_MODULE, }; pub use nonce::{InstructionFlag, NonceKeyV1, TransactionTypeId}; diff --git a/bedrock/src/smart_account/transaction_4337.rs b/bedrock/src/smart_account/transaction_4337.rs index 264014e9..9ebc0522 100644 --- a/bedrock/src/smart_account/transaction_4337.rs +++ b/bedrock/src/smart_account/transaction_4337.rs @@ -3,14 +3,12 @@ //! A transaction can be initialized through a `UserOperation` struct. //! +use crate::primitives::contracts::{EncodedSafeOpStruct, UserOperation}; use crate::primitives::{Network, PrimitiveError}; -use crate::primitives::contracts::{UserOperation, EncodedSafeOpStruct}; use crate::smart_account::SafeSmartAccountSigner; use crate::transaction::rpc::{RpcError, SponsorUserOperationResponse}; -use alloy::{ - primitives::{aliases::U48, Address, Bytes, FixedBytes}, -}; +use alloy::primitives::{aliases::U48, Address, Bytes, FixedBytes}; use chrono::{Duration, Utc}; use crate::primitives::contracts::{ENTRYPOINT_4337, GNOSIS_SAFE_4337_MODULE}; @@ -74,7 +72,6 @@ pub trait Is4337Encodable { safe_account: &crate::smart_account::SafeSmartAccount, self_sponsor_token: Option
, metadata: Option, - _pbh: bool ) -> Result, RpcError> { // Get the global RPC client let rpc_client = crate::transaction::rpc::get_rpc_client()?; @@ -138,32 +135,6 @@ pub trait Is4337Encodable { Ok(user_op_hash) } - - - /// Generates a Privacy-Preserving Biometric Hash (PBH) proof for account abstraction transactions. - /// - /// This function is intended to create cryptographic proofs that verify human uniqueness - /// without revealing biometric data, as part of the Person Bound Transactions (PBT) system. - /// - /// # Returns - /// * `Result` - The generated PBH proof bytes on success - /// - /// # Errors - /// * Returns `PrimitiveError` if proof generation fails - /// - /// # Note - /// This is currently unimplemented and will return a todo!() error. - fn generate_pbh_proof() -> Result { - todo!("PBH proof generation not yet implemented") - } - - // pub fn hash_user_op(user_op: &PackedUserOperation) -> Field { - // let hash = SolValue::abi_encode_packed(&(&user_op.sender, &user_op.nonce, &user_op.callData)); - - // hash_to_field(hash.as_slice()) - // } - - } #[cfg(test)] diff --git a/bedrock/src/transaction/mod.rs b/bedrock/src/transaction/mod.rs index e4ad6bb1..ed12a695 100644 --- a/bedrock/src/transaction/mod.rs +++ b/bedrock/src/transaction/mod.rs @@ -75,7 +75,7 @@ impl SafeSmartAccount { // Sign and execute the transaction (uses global RPC client automatically) let user_op_hash = transaction - .sign_and_execute(network, self, None, None, false) + .sign_and_execute(network, self, None, None) .await .map_err(|e| TransactionError::Generic { message: format!("Failed to execute transaction: {e}"), From 7dcb14b125f07889a6e555e1878779182d11c529 Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Mon, 25 Aug 2025 16:39:50 -0700 Subject: [PATCH 03/34] hardcoded logic --- .gitignore | 4 +- Cargo.lock | 1637 +++++++++++++++-- bedrock/Cargo.toml | 12 +- bedrock/src/primitives/contracts.rs | 133 ++ bedrock/src/smart_account/nonce.rs | 16 +- bedrock/src/smart_account/transaction_4337.rs | 275 ++- bedrock/src/transaction/contracts/erc20.rs | 10 +- bedrock/src/transaction/mod.rs | 4 +- bedrock/tests/test_smart_account_transfer.rs | 3 +- 9 files changed, 1964 insertions(+), 130 deletions(-) diff --git a/.gitignore b/.gitignore index 83b6bca0..cb28eeff 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,6 @@ kotlin/bedrock-tests/bin/ # foundry -out/ \ No newline at end of file +out/ + +load_test_identities.json \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 770a5377..005d82e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,18 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -31,7 +43,7 @@ checksum = "5ecf116474faea3e30ecb03cb14548598ca8243d5316ce50f820e67b3e848473" dependencies = [ "alloy-consensus", "alloy-contract", - "alloy-core", + "alloy-core 1.2.1", "alloy-eips", "alloy-network", "alloy-node-bindings", @@ -52,7 +64,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5848366a4f08dca1caca0a6151294a4799fe2e59ba25df100491d92e0b921b1c" dependencies = [ - "alloy-primitives", + "alloy-primitives 1.3.1", "num_enum", "strum", ] @@ -64,7 +76,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b6093bc69509849435a2d68237a2e9fea79d27390c8e62f1e4012c460aabad8" dependencies = [ "alloy-eips", - "alloy-primitives", + "alloy-primitives 1.3.1", "alloy-rlp", "alloy-serde", "alloy-trie", @@ -90,7 +102,7 @@ checksum = "8d1cfed4fefd13b5620cb81cdb6ba397866ff0de514c1b24806e6e79cdff5570" dependencies = [ "alloy-consensus", "alloy-eips", - "alloy-primitives", + "alloy-primitives 1.3.1", "alloy-rlp", "alloy-serde", "serde", @@ -107,10 +119,10 @@ dependencies = [ "alloy-json-abi", "alloy-network", "alloy-network-primitives", - "alloy-primitives", + "alloy-primitives 1.3.1", "alloy-provider", "alloy-rpc-types-eth", - "alloy-sol-types", + "alloy-sol-types 1.2.1", "alloy-transport", "futures", "futures-util", @@ -118,6 +130,16 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "alloy-core" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d8bcce99ad10fe02640cfaec1c6bc809b837c783c1d52906aa5af66e2a196f6" +dependencies = [ + "alloy-primitives 0.8.25", + "alloy-sol-types 0.8.25", +] + [[package]] name = "alloy-core" version = "1.2.1" @@ -126,8 +148,8 @@ checksum = "ad31216895d27d307369daa1393f5850b50bbbd372478a9fa951c095c210627e" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", - "alloy-primitives", - "alloy-sol-types", + "alloy-primitives 1.3.1", + "alloy-sol-types 1.2.1", ] [[package]] @@ -137,9 +159,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b95b3deca680efc7e9cba781f1a1db352fa1ea50e6384a514944dcf4419e652" dependencies = [ "alloy-json-abi", - "alloy-primitives", + "alloy-primitives 1.3.1", "alloy-sol-type-parser", - "alloy-sol-types", + "alloy-sol-types 1.2.1", "derive_more", "itoa", "serde", @@ -153,7 +175,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "741bdd7499908b3aa0b159bba11e71c8cddd009a2c2eb7a06e825f1ec87900a5" dependencies = [ - "alloy-primitives", + "alloy-primitives 1.3.1", "alloy-rlp", "crc", "serde", @@ -166,7 +188,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b82752a889170df67bbb36d42ca63c531eb16274f0d7299ae2a680facba17bd" dependencies = [ - "alloy-primitives", + "alloy-primitives 1.3.1", "alloy-rlp", "serde", ] @@ -177,7 +199,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d4769c6ffddca380b0070d71c8b7f30bed375543fe76bb2f74ec0acf4b7cd16" dependencies = [ - "alloy-primitives", + "alloy-primitives 1.3.1", "alloy-rlp", "serde", "thiserror 2.0.12", @@ -192,7 +214,7 @@ dependencies = [ "alloy-eip2124", "alloy-eip2930", "alloy-eip7702", - "alloy-primitives", + "alloy-primitives 1.3.1", "alloy-rlp", "alloy-serde", "auto_impl", @@ -210,7 +232,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c51b4c13e02a8104170a4de02ccf006d7c233e6c10ab290ee16e7041e6ac221d" dependencies = [ "alloy-eips", - "alloy-primitives", + "alloy-primitives 1.3.1", "alloy-serde", "alloy-trie", "serde", @@ -224,7 +246,7 @@ checksum = "b40cc82a2283e3ce6317bc1f0134ea50d20e8c1965393045ee952fb28a65ddbd" dependencies = [ "alloy-chains", "alloy-eip2124", - "alloy-primitives", + "alloy-primitives 1.3.1", "auto_impl", "dyn-clone", ] @@ -235,7 +257,7 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15516116086325c157c18261d768a20677f0f699348000ed391d4ad0dcb82530" dependencies = [ - "alloy-primitives", + "alloy-primitives 1.3.1", "alloy-sol-type-parser", "serde", "serde_json", @@ -247,8 +269,8 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b590caa6b6d8bc10e6e7a7696c59b1e550e89f27f50d1ee13071150d3a3e3f66" dependencies = [ - "alloy-primitives", - "alloy-sol-types", + "alloy-primitives 1.3.1", + "alloy-sol-types 1.2.1", "http 1.3.1", "serde", "serde_json", @@ -267,12 +289,12 @@ dependencies = [ "alloy-eips", "alloy-json-rpc", "alloy-network-primitives", - "alloy-primitives", + "alloy-primitives 1.3.1", "alloy-rpc-types-any", "alloy-rpc-types-eth", "alloy-serde", "alloy-signer", - "alloy-sol-types", + "alloy-sol-types 1.2.1", "async-trait", "auto_impl", "derive_more", @@ -290,7 +312,7 @@ checksum = "793df1e3457573877fbde8872e4906638fde565ee2d3bd16d04aad17d43dbf0e" dependencies = [ "alloy-consensus", "alloy-eips", - "alloy-primitives", + "alloy-primitives 1.3.1", "alloy-serde", "serde", ] @@ -304,7 +326,7 @@ dependencies = [ "alloy-genesis", "alloy-hardforks", "alloy-network", - "alloy-primitives", + "alloy-primitives 1.3.1", "alloy-signer", "alloy-signer-local", "k256", @@ -318,9 +340,25 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "1.2.1" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c77490fe91a0ce933a1f219029521f20fc28c2c0ca95d53fa4da9c00b8d9d4e" +dependencies = [ + "bytes", + "cfg-if", + "const-hex", + "derive_more", + "itoa", + "paste", + "ruint", + "tiny-keccak", +] + +[[package]] +name = "alloy-primitives" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6177ed26655d4e84e00b65cb494d4e0b8830e7cae7ef5d63087d445a2600fb55" +checksum = "bc9485c56de23438127a731a6b4c87803d49faf1a7068dcd1d8768aca3a9edb9" dependencies = [ "alloy-rlp", "bytes", @@ -356,12 +394,12 @@ dependencies = [ "alloy-network", "alloy-network-primitives", "alloy-node-bindings", - "alloy-primitives", + "alloy-primitives 1.3.1", "alloy-rpc-client", "alloy-rpc-types-anvil", "alloy-rpc-types-eth", "alloy-signer", - "alloy-sol-types", + "alloy-sol-types 1.2.1", "alloy-transport", "alloy-transport-http", "async-stream", @@ -375,7 +413,7 @@ dependencies = [ "lru", "parking_lot", "pin-project 1.1.10", - "reqwest", + "reqwest 0.12.23", "serde", "serde_json", "thiserror 2.0.12", @@ -414,12 +452,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f060e3bb9f319eb01867a2d6d1ff9e0114e8877f5ca8f5db447724136106cae" dependencies = [ "alloy-json-rpc", - "alloy-primitives", + "alloy-primitives 1.3.1", "alloy-transport", "alloy-transport-http", "futures", "pin-project 1.1.10", - "reqwest", + "reqwest 0.12.23", "serde", "serde_json", "tokio", @@ -436,7 +474,7 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b1f499acb3fc729615147bc113b8b798b17379f19d43058a687edc5792c102" dependencies = [ - "alloy-primitives", + "alloy-primitives 1.3.1", "alloy-rpc-types-eth", "alloy-serde", "serde", @@ -463,10 +501,10 @@ dependencies = [ "alloy-consensus-any", "alloy-eips", "alloy-network-primitives", - "alloy-primitives", + "alloy-primitives 1.3.1", "alloy-rlp", "alloy-serde", - "alloy-sol-types", + "alloy-sol-types 1.2.1", "itertools 0.14.0", "serde", "serde_json", @@ -480,7 +518,7 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e1722bc30feef87cc0fa824e43c9013f9639cc6c037be7be28a31361c788be2" dependencies = [ - "alloy-primitives", + "alloy-primitives 1.3.1", "serde", "serde_json", ] @@ -492,8 +530,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3674beb29e68fbbc7be302b611cf35fe07b736e308012a280861df5a2361395" dependencies = [ "alloy-dyn-abi", - "alloy-primitives", - "alloy-sol-types", + "alloy-primitives 1.3.1", + "alloy-sol-types 1.2.1", "async-trait", "auto_impl", "either", @@ -510,7 +548,7 @@ checksum = "605b1659b320b16708bb84b41038b2f0e2a60d90972c28319c4f5a4866f0efd4" dependencies = [ "alloy-consensus", "alloy-network", - "alloy-primitives", + "alloy-primitives 1.3.1", "alloy-signer", "async-trait", "aws-sdk-kms", @@ -528,7 +566,7 @@ checksum = "0a207671ef0bf6f61e9c80c9ccb6d203439071252fb35886d6a89aae5431cd9c" dependencies = [ "alloy-consensus", "alloy-network", - "alloy-primitives", + "alloy-primitives 1.3.1", "alloy-signer", "async-trait", "gcloud-sdk", @@ -547,9 +585,9 @@ dependencies = [ "alloy-consensus", "alloy-dyn-abi", "alloy-network", - "alloy-primitives", + "alloy-primitives 1.3.1", "alloy-signer", - "alloy-sol-types", + "alloy-sol-types 1.2.1", "async-trait", "coins-ledger", "futures-util", @@ -566,7 +604,7 @@ checksum = "ad7094c39cd41b03ed642145b0bd37251e31a9cf2ed19e1ce761f089867356a6" dependencies = [ "alloy-consensus", "alloy-network", - "alloy-primitives", + "alloy-primitives 1.3.1", "alloy-signer", "async-trait", "k256", @@ -574,18 +612,50 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "alloy-sol-macro" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10ae8e9a91d328ae954c22542415303919aabe976fe7a92eb06db1b68fd59f2" +dependencies = [ + "alloy-sol-macro-expander 0.8.25", + "alloy-sol-macro-input 0.8.25", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "alloy-sol-macro" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a14f21d053aea4c6630687c2f4ad614bed4c81e14737a9b904798b24f30ea849" dependencies = [ - "alloy-sol-macro-expander", - "alloy-sol-macro-input", + "alloy-sol-macro-expander 1.2.1", + "alloy-sol-macro-input 1.2.1", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "alloy-sol-macro-expander" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83ad5da86c127751bc607c174d6c9fe9b85ef0889a9ca0c641735d77d4f98f26" +dependencies = [ + "alloy-sol-macro-input 0.8.25", + "const-hex", + "heck", + "indexmap 2.9.0", "proc-macro-error2", "proc-macro2", "quote", "syn 2.0.104", + "syn-solidity 0.8.25", + "tiny-keccak", ] [[package]] @@ -595,7 +665,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34d99282e7c9ef14eb62727981a985a01869e586d1dec729d3bb33679094c100" dependencies = [ "alloy-json-abi", - "alloy-sol-macro-input", + "alloy-sol-macro-input 1.2.1", "const-hex", "heck", "indexmap 2.9.0", @@ -603,10 +673,26 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.104", - "syn-solidity", + "syn-solidity 1.2.1", "tiny-keccak", ] +[[package]] +name = "alloy-sol-macro-input" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3d30f0d3f9ba3b7686f3ff1de9ee312647aac705604417a2f40c604f409a9e" +dependencies = [ + "const-hex", + "dunce", + "heck", + "macro-string", + "proc-macro2", + "quote", + "syn 2.0.104", + "syn-solidity 0.8.25", +] + [[package]] name = "alloy-sol-macro-input" version = "1.2.1" @@ -622,7 +708,7 @@ dependencies = [ "quote", "serde_json", "syn 2.0.104", - "syn-solidity", + "syn-solidity 1.2.1", ] [[package]] @@ -635,6 +721,17 @@ dependencies = [ "winnow", ] +[[package]] +name = "alloy-sol-types" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43d5e60466a440230c07761aa67671d4719d46f43be8ea6e7ed334d8db4a9ab" +dependencies = [ + "alloy-primitives 0.8.25", + "alloy-sol-macro 0.8.25", + "const-hex", +] + [[package]] name = "alloy-sol-types" version = "1.2.1" @@ -642,8 +739,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58377025a47d8b8426b3e4846a251f2c1991033b27f517aade368146f6ab1dfe" dependencies = [ "alloy-json-abi", - "alloy-primitives", - "alloy-sol-macro", + "alloy-primitives 1.3.1", + "alloy-sol-macro 1.2.1", "serde", ] @@ -654,8 +751,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f89bec2f59a41c0e259b6fe92f78dfc49862c17d10f938db9c33150d5a7f42b6" dependencies = [ "alloy-json-rpc", - "alloy-primitives", - "base64", + "alloy-primitives 1.3.1", + "base64 0.22.1", "derive_more", "futures", "futures-utils-wasm", @@ -678,7 +775,7 @@ checksum = "0d3615ec64d775fec840f4e9d5c8e1f739eb1854d8d28db093fb3d4805e0cb53" dependencies = [ "alloy-json-rpc", "alloy-transport", - "reqwest", + "reqwest 0.12.23", "serde_json", "tower", "tracing", @@ -691,7 +788,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bada1fc392a33665de0dc50d401a3701b62583c655e3522a323490a5da016962" dependencies = [ - "alloy-primitives", + "alloy-primitives 1.3.1", "alloy-rlp", "arrayvec", "derive_more", @@ -707,8 +804,8 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f916ff6d52f219c44a9684aea764ce2c7e1d53bd4a724c9b127863aeacc30bb" dependencies = [ - "alloy-primitives", - "darling", + "alloy-primitives 1.3.1", + "darling 0.20.11", "proc-macro2", "quote", "syn 2.0.104", @@ -741,6 +838,54 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "ark-bn254" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a22f4561524cd949590d78d7d4c5df8f592430d221f7f3c9497bbafd8972120f" +dependencies = [ + "ark-ec", + "ark-ff 0.4.2", + "ark-std 0.4.0", +] + +[[package]] +name = "ark-crypto-primitives" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3a13b34da09176a8baba701233fdffbaa7c1b1192ce031a3da4e55ce1f1a56" +dependencies = [ + "ark-ec", + "ark-ff 0.4.2", + "ark-relations", + "ark-serialize 0.4.2", + "ark-snark", + "ark-std 0.4.0", + "blake2", + "derivative", + "digest 0.10.7", + "rayon", + "sha2", +] + +[[package]] +name = "ark-ec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defd9a439d56ac24968cca0571f598a61bc8c55f71d50a89cda591cb750670ba" +dependencies = [ + "ark-ff 0.4.2", + "ark-poly", + "ark-serialize 0.4.2", + "ark-std 0.4.0", + "derivative", + "hashbrown 0.13.2", + "itertools 0.10.5", + "num-traits", + "rayon", + "zeroize", +] + [[package]] name = "ark-ff" version = "0.3.0" @@ -775,6 +920,7 @@ dependencies = [ "num-bigint", "num-traits", "paste", + "rayon", "rustc_version 0.4.1", "zeroize", ] @@ -824,6 +970,48 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ark-groth16" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20ceafa83848c3e390f1cbf124bc3193b3e639b3f02009e0e290809a501b95fc" +dependencies = [ + "ark-crypto-primitives", + "ark-ec", + "ark-ff 0.4.2", + "ark-poly", + "ark-relations", + "ark-serialize 0.4.2", + "ark-std 0.4.0", + "rayon", +] + +[[package]] +name = "ark-poly" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d320bfc44ee185d899ccbadfa8bc31aab923ce1558716e1997a1e74057fe86bf" +dependencies = [ + "ark-ff 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", + "derivative", + "hashbrown 0.13.2", + "rayon", +] + +[[package]] +name = "ark-relations" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00796b6efc05a3f48225e59cb6a2cda78881e7c390872d5786aaf112f31fb4f0" +dependencies = [ + "ark-ff 0.4.2", + "ark-std 0.4.0", + "tracing", + "tracing-subscriber 0.2.25", +] + [[package]] name = "ark-serialize" version = "0.3.0" @@ -840,11 +1028,35 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" dependencies = [ + "ark-serialize-derive", "ark-std 0.4.0", "digest 0.10.7", "num-bigint", ] +[[package]] +name = "ark-serialize-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae3281bc6d0fd7e549af32b52511e1302185bd688fd3359fa36423346ff682ea" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-snark" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d3cc6833a335bb8a600241889ead68ee89a3cf8448081fb7694c0fe503da63" +dependencies = [ + "ark-ff 0.4.2", + "ark-relations", + "ark-serialize 0.4.2", + "ark-std 0.4.0", +] + [[package]] name = "ark-std" version = "0.3.0" @@ -863,6 +1075,7 @@ checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" dependencies = [ "num-traits", "rand 0.8.5", + "rayon", ] [[package]] @@ -975,6 +1188,15 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -1224,7 +1446,7 @@ dependencies = [ "pin-project-lite", "rustversion", "serde", - "sync_wrapper", + "sync_wrapper 1.0.2", "tower", "tower-layer", "tower-service", @@ -1244,7 +1466,7 @@ dependencies = [ "mime", "pin-project-lite", "rustversion", - "sync_wrapper", + "sync_wrapper 1.0.2", "tower-layer", "tower-service", ] @@ -1270,6 +1492,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -1306,24 +1534,35 @@ name = "bedrock" version = "0.0.6" dependencies = [ "alloy", + "alloy-primitives 1.3.1", + "alloy-rlp", "anyhow", "async-trait", "bedrock-macros", + "bon", "chrono", "dotenvy", + "eyre", + "futures", "hex", "log", "proc-macro2", "quote", "rand 0.9.2", + "reqwest 0.12.23", "ruint", + "semaphore-rs", + "semaphore-rs-proof", "serde", "serde_json", + "strum", "syn 2.0.104", + "test-case", "thiserror 2.0.12", "tokio", "tokio-test", "uniffi", + "world-chain-builder-pbh", ] [[package]] @@ -1335,6 +1574,15 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -1390,6 +1638,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -1411,6 +1668,31 @@ dependencies = [ "zeroize", ] +[[package]] +name = "bon" +version = "3.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537c317ddf588aab15c695bf92cf55dec159b93221c074180ca3e0e5a94da415" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5abbf2d4a4c6896197c9de13d6d7cb7eff438c63dacde1dde980569cb00248" +dependencies = [ + "darling 0.21.3", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.104", +] + [[package]] name = "bumpalo" version = "3.17.0" @@ -1423,6 +1705,12 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" +[[package]] +name = "bytemuck" +version = "1.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" + [[package]] name = "byteorder" version = "1.5.0" @@ -1524,8 +1812,10 @@ checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link", ] @@ -1568,6 +1858,26 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.12", +] + +[[package]] +name = "codespan-reporting" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + [[package]] name = "coins-ledger" version = "0.12.0" @@ -1591,6 +1901,43 @@ dependencies = [ "wasm-bindgen-futures", ] +[[package]] +name = "color-eyre" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "const-hex" version = "1.14.1" @@ -1630,6 +1977,16 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -1680,10 +2037,35 @@ dependencies = [ ] [[package]] -name = "crossbeam-utils" -version = "0.8.21" +name = "critical-section" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" @@ -1713,14 +2095,39 @@ dependencies = [ "typenum", ] +[[package]] +name = "cxx-build" +version = "1.0.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acef58f684c0c9b7bffc111657c9f1364e4fa47af46fd7f03b71b7a4066ac372" +dependencies = [ + "cc", + "codespan-reporting", + "indexmap 2.9.0", + "proc-macro2", + "quote", + "scratch", + "syn 2.0.104", +] + [[package]] name = "darling" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", ] [[package]] @@ -1737,13 +2144,38 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.104", +] + [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core", + "darling_core 0.20.11", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", "quote", "syn 2.0.104", ] @@ -1794,6 +2226,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive-where" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "derive_more" version = "2.0.1" @@ -1910,6 +2353,39 @@ dependencies = [ "zeroize", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1926,6 +2402,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -1998,6 +2484,21 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -2127,12 +2628,12 @@ dependencies = [ "bytes", "chrono", "futures", - "hyper", + "hyper 1.6.0", "jsonwebtoken", "once_cell", "prost", "prost-types", - "reqwest", + "reqwest 0.12.23", "secret-vault-value", "serde", "serde_json", @@ -2217,6 +2718,25 @@ dependencies = [ "subtle", ] +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.9.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "h2" version = "0.4.10" @@ -2236,12 +2756,30 @@ dependencies = [ "tracing", ] +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -2260,6 +2798,20 @@ dependencies = [ "serde", ] +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version 0.4.1", + "serde", + "spin", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.5.0" @@ -2290,6 +2842,12 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + [[package]] name = "hidapi-rusb" version = "1.3.3" @@ -2379,6 +2937,30 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.9", + "tokio", + "tower-service", + "tracing", + "want", +] + [[package]] name = "hyper" version = "1.6.0" @@ -2388,7 +2970,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2", + "h2 0.4.10", "http 1.3.1", "http-body 1.0.1", "httparse", @@ -2407,7 +2989,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d" dependencies = [ "http 1.3.1", - "hyper", + "hyper 1.6.0", "hyper-util", "rustls", "rustls-native-certs", @@ -2415,7 +2997,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.0", + "webpki-roots", ] [[package]] @@ -2424,31 +3006,65 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" dependencies = [ - "hyper", + "hyper 1.6.0", "hyper-util", "pin-project-lite", "tokio", "tower-service", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper 0.14.32", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.6.0", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf9f1e950e0d9d1d3c47184416723cf29c0d1f93bd8cccf37e4beb6b44f31710" dependencies = [ + "base64 0.22.1", "bytes", "futures-channel", "futures-util", "http 1.3.1", "http-body 1.0.1", - "hyper", + "hyper 1.6.0", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2 0.5.9", + "system-configuration 0.6.1", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -2608,6 +3224,12 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + [[package]] name = "indexmap" version = "1.9.3" @@ -2647,6 +3269,16 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "itertools" version = "0.10.5" @@ -2656,6 +3288,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -2687,7 +3328,7 @@ version = "9.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" dependencies = [ - "base64", + "base64 0.22.1", "js-sys", "pem", "ring", @@ -2802,6 +3443,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + [[package]] name = "macro-string" version = "0.1.4" @@ -2825,6 +3475,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memmap2" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.7.1" @@ -2876,6 +3535,40 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "mmap-rs" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86968d85441db75203c34deefd0c88032f275aaa85cee19a1dcfff6ae9df56da" +dependencies = [ + "bitflags 1.3.2", + "combine", + "libc", + "mach2", + "nix", + "sysctl", + "thiserror 1.0.69", + "widestring", + "windows", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework 2.11.1", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nix" version = "0.26.4" @@ -2907,6 +3600,7 @@ checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", + "rand 0.8.5", ] [[package]] @@ -2993,18 +3687,62 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "openssl-probe" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "outref" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" +[[package]] +name = "owo-colors" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" + [[package]] name = "parity-scale-codec" version = "3.7.5" @@ -3068,7 +3806,7 @@ version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" dependencies = [ - "base64", + "base64 0.22.1", "serde", ] @@ -3172,6 +3910,19 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "serde", +] + [[package]] name = "potential_utf" version = "0.1.2" @@ -3196,6 +3947,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" +dependencies = [ + "proc-macro2", + "syn 2.0.104", +] + [[package]] name = "primitive-types" version = "0.12.2" @@ -3452,6 +4213,26 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.12" @@ -3475,50 +4256,92 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.15" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper-tls 0.5.0", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration 0.5.1", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "reqwest" +version = "0.12.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" dependencies = [ "async-compression", - "base64", + "base64 0.22.1", "bytes", + "encoding_rs", "futures-core", "futures-util", + "h2 0.4.10", "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper", + "hyper 1.6.0", "hyper-rustls", + "hyper-tls 0.6.0", "hyper-util", - "ipnet", "js-sys", "log", "mime", "mime_guess", - "once_cell", + "native-tls", "percent-encoding", "pin-project-lite", "quinn", "rustls", "rustls-native-certs", - "rustls-pemfile", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 1.0.2", "tokio", + "tokio-native-tls", "tokio-rustls", "tokio-util", "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 0.26.11", - "windows-registry", + "webpki-roots", ] [[package]] @@ -3564,6 +4387,7 @@ dependencies = [ "alloy-rlp", "ark-ff 0.3.0", "ark-ff 0.4.2", + "bytemuck", "bytes", "fastrlp 0.3.1", "fastrlp 0.4.0", @@ -3671,16 +4495,16 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 3.2.0", ] [[package]] name = "rustls-pemfile" -version = "2.2.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "rustls-pki-types", + "base64 0.21.7", ] [[package]] @@ -3728,6 +4552,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.27" @@ -3743,6 +4576,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scratch" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2" + [[package]] name = "scroll" version = "0.12.0" @@ -3812,27 +4651,268 @@ dependencies = [ "zeroize", ] +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + [[package]] name = "security-framework" version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semaphore-rs" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b47ba971a6088a498d78316854aab2f3833ca031722d369378220cdbbc75ca2" +dependencies = [ + "alloy-core 0.8.25", + "ark-bn254", + "ark-ec", + "ark-ff 0.4.2", + "ark-groth16", + "ark-relations", + "ark-std 0.4.0", + "bincode", + "bytemuck", + "color-eyre", + "hex", + "hex-literal", + "itertools 0.13.0", + "lazy_static", + "mmap-rs", + "num-bigint", + "once_cell", + "rand 0.8.5", + "rayon", + "reqwest 0.11.27", + "ruint", + "semaphore-rs-ark-circom", + "semaphore-rs-ark-zkey", + "semaphore-rs-depth-config", + "semaphore-rs-depth-macros", + "semaphore-rs-hasher", + "semaphore-rs-keccak", + "semaphore-rs-poseidon", + "semaphore-rs-proof", + "semaphore-rs-storage", + "semaphore-rs-trees", + "semaphore-rs-utils", + "semaphore-rs-witness", + "serde", + "sha2", + "thiserror 1.0.69", + "tiny-keccak", + "zeroize", +] + +[[package]] +name = "semaphore-rs-ark-circom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "590c00fa015e2f7f03f4f0f4123bbe38a7cded0f4497cdd67b7874e8a37887f3" +dependencies = [ + "ark-bn254", + "ark-crypto-primitives", + "ark-ff 0.4.2", + "ark-groth16", + "ark-poly", + "ark-relations", + "ark-serialize 0.4.2", + "ark-std 0.4.0", + "byteorder", + "num-bigint", + "num-traits", + "ruint", + "serde_json", +] + +[[package]] +name = "semaphore-rs-ark-zkey" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c5486a113b07b9f4c003a81f4efaca98159498bb7acff34b3b8c6fb022132b" +dependencies = [ + "ark-bn254", + "ark-ec", + "ark-ff 0.4.2", + "ark-groth16", + "ark-relations", + "ark-serialize 0.4.2", + "color-eyre", + "memmap2", + "semaphore-rs-ark-circom", +] + +[[package]] +name = "semaphore-rs-depth-config" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b250534726c9a2785bfb8fcb498540ef47fb0f732824bb640994dcaa40547f6" + +[[package]] +name = "semaphore-rs-depth-macros" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fa8ad8e24071abbc3c554db3c940e2af2dd676259ab702b8b7f516d162986fb" +dependencies = [ + "itertools 0.13.0", + "proc-macro2", + "quote", + "semaphore-rs-depth-config", + "syn 2.0.104", +] + +[[package]] +name = "semaphore-rs-hasher" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11442fee48d178242d0e22715253821fb23ee0a770cf7ce3d13079718f4231e9" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "semaphore-rs-keccak" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9d5efcb970e069bd434c7dc1dad4d715e0ec9208c10d66d81076cc5d57f7b78" +dependencies = [ + "semaphore-rs-hasher", + "tiny-keccak", +] + +[[package]] +name = "semaphore-rs-poseidon" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d26e9b66d2fcf52621d2d86786bca4e09f080c902d6c9f53587bb3c97dd812e" +dependencies = [ + "ark-bn254", + "ark-ff 0.4.2", + "once_cell", + "ruint", + "semaphore-rs-hasher", +] + +[[package]] +name = "semaphore-rs-proof" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bf4c50a78e3ce61761e6c5bdd21f1f640845cf416bc4d9705f0657c3b1a931" +dependencies = [ + "alloy-core 0.8.25", + "ark-bn254", + "ark-ec", + "ark-groth16", + "getrandom 0.2.16", + "hex", + "lazy_static", + "ruint", + "semaphore-rs-ark-circom", + "semaphore-rs-utils", + "serde", + "serde_json", +] + +[[package]] +name = "semaphore-rs-storage" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7843420192065a5a4789285e984df9239a294e9553631717c03cbb66b141ff91" +dependencies = [ + "bytemuck", + "color-eyre", + "mmap-rs", + "tempfile", +] + +[[package]] +name = "semaphore-rs-trees" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a2bc89c277b8761b46ed6953a6995d01ecadb87164f5b499a2c67846ea517" +dependencies = [ + "ark-bn254", + "ark-ec", + "ark-ff 0.4.2", + "ark-groth16", + "ark-relations", + "ark-std 0.4.0", + "bytemuck", + "color-eyre", + "derive-where", + "hex", + "hex-literal", + "itertools 0.13.0", + "mmap-rs", + "once_cell", + "rayon", + "ruint", + "semaphore-rs-ark-circom", + "semaphore-rs-hasher", + "semaphore-rs-storage", + "serde", + "thiserror 1.0.69", + "tiny-keccak", +] + +[[package]] +name = "semaphore-rs-utils" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb67bdd9e07446957225c963228a4be98a60bf80389fbb335eda61e749a15306" dependencies = [ - "bitflags 2.9.1", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", + "hex", + "serde", + "tiny-keccak", ] [[package]] -name = "security-framework-sys" -version = "2.14.0" +name = "semaphore-rs-witness" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "014e1212ea105aa3be44b7c0d318234794dec47e23a926b088e742cfc9f89fa3" dependencies = [ - "core-foundation-sys", - "libc", + "ark-bn254", + "ark-ff 0.4.2", + "ark-serialize 0.4.2", + "byteorder", + "color-eyre", + "cxx-build", + "hex", + "postcard", + "rand 0.8.5", + "ruint", + "serde", + "serde_json", ] [[package]] @@ -3912,7 +4992,7 @@ version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" dependencies = [ - "base64", + "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", @@ -3930,7 +5010,7 @@ version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" dependencies = [ - "darling", + "darling 0.20.11", "proc-macro2", "quote", "syn 2.0.104", @@ -3977,6 +5057,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -4055,6 +5144,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + [[package]] name = "spki" version = "0.7.3" @@ -4085,9 +5183,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ "strum_macros", ] @@ -4133,6 +5231,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn-solidity" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4560533fbd6914b94a8fb5cc803ed6801c3455668db3b810702c57612bac9412" +dependencies = [ + "paste", + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "syn-solidity" version = "1.2.1" @@ -4145,6 +5255,12 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -4165,6 +5281,62 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "sysctl" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7dddc5f0fee506baf8b9fdb989e242f17e4b11c61dfbb0635b705217199eea" +dependencies = [ + "bitflags 2.9.1", + "byteorder", + "enum-as-inner", + "libc", + "thiserror 1.0.69", + "walkdir", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "system-configuration-sys 0.5.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.9.4", + "system-configuration-sys 0.6.0", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tap" version = "1.0.1" @@ -4184,6 +5356,48 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "test-case" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8" +dependencies = [ + "test-case-macros", +] + +[[package]] +name = "test-case-core" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "test-case-macros" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", + "test-case-core", +] + [[package]] name = "textwrap" version = "0.16.2" @@ -4233,6 +5447,15 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "threadpool" version = "1.8.1" @@ -4336,6 +5559,16 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.2" @@ -4418,13 +5651,13 @@ checksum = "7e581ba15a835f4d9ea06c55ab1bd4dce26fc53752c69a04aac00703bfb49ba9" dependencies = [ "async-trait", "axum", - "base64", + "base64 0.22.1", "bytes", - "h2", + "h2 0.4.10", "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper", + "hyper 1.6.0", "hyper-timeout", "hyper-util", "percent-encoding", @@ -4452,7 +5685,7 @@ dependencies = [ "indexmap 2.9.0", "pin-project-lite", "slab", - "sync_wrapper", + "sync_wrapper 1.0.2", "tokio", "tokio-util", "tower-layer", @@ -4460,6 +5693,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.1", + "bytes", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -4513,6 +5764,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" +dependencies = [ + "tracing", + "tracing-subscriber 0.3.19", +] + +[[package]] +name = "tracing-subscriber" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" +dependencies = [ + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", ] [[package]] @@ -4563,6 +5845,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-width" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -4776,6 +6064,16 @@ dependencies = [ "libc", ] +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -4918,15 +6216,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.0", -] - [[package]] name = "webpki-roots" version = "1.0.0" @@ -4945,6 +6234,30 @@ dependencies = [ "nom", ] +[[package]] +name = "widestring" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" + +[[package]] +name = "winapi-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-core" version = "0.61.2" @@ -4994,7 +6307,7 @@ checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" dependencies = [ "windows-result", "windows-strings 0.3.1", - "windows-targets 0.53.0", + "windows-targets 0.53.2", ] [[package]] @@ -5024,6 +6337,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -5042,6 +6364,21 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -5060,9 +6397,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.0" +version = "0.53.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" dependencies = [ "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", @@ -5074,6 +6411,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -5086,6 +6429,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -5098,6 +6447,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -5122,6 +6477,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -5134,6 +6495,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -5146,6 +6513,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -5158,6 +6531,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -5179,6 +6558,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" @@ -5188,6 +6577,22 @@ dependencies = [ "bitflags 2.9.1", ] +[[package]] +name = "world-chain-builder-pbh" +version = "0.1.0" +source = "git+https://github.com/worldcoin/world-chain?rev=4f524c6#4f524c699735a1d30932a22914c8abe3a4b40ac3" +dependencies = [ + "alloy-primitives 1.3.1", + "alloy-rlp", + "bon", + "chrono", + "semaphore-rs", + "semaphore-rs-proof", + "serde", + "strum", + "thiserror 1.0.69", +] + [[package]] name = "writeable" version = "0.6.1" diff --git a/bedrock/Cargo.toml b/bedrock/Cargo.toml index 56594ccf..f63d58e3 100644 --- a/bedrock/Cargo.toml +++ b/bedrock/Cargo.toml @@ -28,26 +28,36 @@ alloy = { version = "1.0.23", default-features = false, features = [ "signer-local", "sol-types", ] } +alloy-primitives = "1.3.1" +alloy-rlp = "0.3.12" anyhow = "1.0" async-trait = "0.1" bedrock-macros = { path = "../bedrock-macros" } +bon = "3.7.1" chrono = { version = "0.4.41", default-features = false, features = [ "now", "std", ] } +eyre = "0.6.12" +futures = "0.3.31" hex = "0.4.3" log = "0.4.22" proc-macro2 = "1.0" quote = "1.0" rand = "0.9.2" +reqwest = { version = "0.12.19", features = ["json"] } ruint = { version = "1.15.0", default-features = false, features = ["serde"] } +semaphore-rs = "0.3.1" +semaphore-rs-proof = "0.3.1" serde = "1.0.219" serde_json = "1.0" +strum = "0.27.1" syn = { version = "2.0", features = ["full"] } +test-case = "3.3.1" thiserror = "2.0.12" tokio = { version = "1.47.1", features = ["time"], optional = true } uniffi = { workspace = true, features = ["build", "tokio"] } - +world-chain-builder-pbh = { git = "https://github.com/worldcoin/world-chain", rev = "4f524c6"} [dev-dependencies] alloy = { version = "1.0.23", default-features = false, features = [ "json", diff --git a/bedrock/src/primitives/contracts.rs b/bedrock/src/primitives/contracts.rs index 361f6a6a..38e5e4c5 100644 --- a/bedrock/src/primitives/contracts.rs +++ b/bedrock/src/primitives/contracts.rs @@ -1,3 +1,4 @@ +use crate::primitives::contracts::IPBHEntryPoint::PBHPayload; use crate::primitives::{HttpError, PrimitiveError}; use crate::transaction::rpc::SponsorUserOperationResponse; use alloy::hex::FromHex; @@ -6,6 +7,8 @@ use alloy::sol; use alloy::sol_types::SolValue; use ruint::aliases::U256; use std::{str::FromStr, sync::LazyLock}; +use world_chain_builder_pbh::external_nullifier::EncodedExternalNullifier; +use world_chain_builder_pbh::payload::{PBHPayload as PbhPayload, TREE_DEPTH}; /// static SAFE_OP_TYPEHASH: LazyLock> = LazyLock::new(|| { @@ -307,6 +310,113 @@ impl EncodedSafeOpStruct { } } +impl From for EncodedSafeOpStruct { + /// Converts a `UserOperation` into an `EncodedSafeOpStruct`. + /// + /// This implementation extracts validity timestamps from the UserOperation's signature. + /// If the signature doesn't contain valid timestamps, it uses zero values as defaults. + /// + /// # Example + /// ```rust + /// use bedrock::primitives::contracts::{UserOperation, EncodedSafeOpStruct}; + /// + /// let user_op = UserOperation::default(); + /// let encoded_safe_op: EncodedSafeOpStruct = user_op.into(); + /// ``` + fn from(user_op: UserOperation) -> Self { + // Extract validity timestamps from the signature, or use defaults + let (valid_after, valid_until) = user_op + .extract_validity_timestamps() + .unwrap_or((U48::ZERO, U48::ZERO)); + + // Use the existing method to create the struct + Self::from_user_op_with_validity(&user_op, valid_after, valid_until) + .expect("Failed to convert UserOperation to EncodedSafeOpStruct") + } +} + +impl From for IEntryPoint::PackedUserOperation { + /// Converts a `UserOperation` into a `PackedUserOperation`. + /// + /// This conversion packs gas limits and fees into bytes32 fields as required by EIP-4337. + /// - `accountGasLimits`: verification_gas_limit (upper 128 bits) + call_gas_limit (lower 128 bits) + /// - `gasFees`: max_priority_fee_per_gas (upper 128 bits) + max_fee_per_gas (lower 128 bits) + /// + /// # Example + /// ```rust + /// use bedrock::primitives::contracts::{UserOperation, IEntryPoint::PackedUserOperation}; + /// + /// let user_op = UserOperation::default(); + /// let packed_user_op: PackedUserOperation = user_op.into(); + /// ``` + fn from(user_op: UserOperation) -> Self { + // Pack verification_gas_limit (upper 128 bits) + call_gas_limit (lower 128 bits) into accountGasLimits + let verification_gas_u256 = U256::from(user_op.verification_gas_limit); + let call_gas_u256 = U256::from(user_op.call_gas_limit); + let account_gas_limits: U256 = (verification_gas_u256 << 128) | call_gas_u256; + + // Pack max_priority_fee_per_gas (upper 128 bits) + max_fee_per_gas (lower 128 bits) into gasFees + let max_priority_fee_u256 = U256::from(user_op.max_priority_fee_per_gas); + let max_fee_u256 = U256::from(user_op.max_fee_per_gas); + let gas_fees: U256 = (max_priority_fee_u256 << 128) | max_fee_u256; + + Self { + sender: user_op.sender, + nonce: user_op.nonce, + initCode: user_op.get_init_code(), + callData: user_op.call_data.clone(), + accountGasLimits: FixedBytes::from_slice( + &account_gas_limits.to_be_bytes::<32>(), + ), + preVerificationGas: user_op.pre_verification_gas, + gasFees: FixedBytes::from_slice(&gas_fees.to_be_bytes::<32>()), + paymasterAndData: user_op.get_paymaster_and_data(), + signature: user_op.signature, + } + } +} + +impl From<&UserOperation> for IEntryPoint::PackedUserOperation { + /// Converts a `&UserOperation` into a `PackedUserOperation`. + /// + /// This conversion packs gas limits and fees into bytes32 fields as required by EIP-4337. + /// This implementation works with borrowed UserOperations to avoid unnecessary moves. + /// + /// # Example + /// ```rust + /// use bedrock::primitives::contracts::{UserOperation, IEntryPoint::PackedUserOperation}; + /// + /// let user_op = UserOperation::default(); + /// let packed_user_op: PackedUserOperation = (&user_op).into(); + /// // user_op is still available for use + /// ``` + fn from(user_op: &UserOperation) -> Self { + // Pack verification_gas_limit (upper 128 bits) + call_gas_limit (lower 128 bits) into accountGasLimits + let verification_gas_u256 = U256::from(user_op.verification_gas_limit); + let call_gas_u256 = U256::from(user_op.call_gas_limit); + let account_gas_limits: U256 = (verification_gas_u256 << 128) | call_gas_u256; + + // Pack max_priority_fee_per_gas (upper 128 bits) + max_fee_per_gas (lower 128 bits) into gasFees + let max_priority_fee_u256 = U256::from(user_op.max_priority_fee_per_gas); + let max_fee_u256 = U256::from(user_op.max_fee_per_gas); + let gas_fees: U256 = (max_priority_fee_u256 << 128) | max_fee_u256; + + Self { + sender: user_op.sender, + nonce: user_op.nonce, + initCode: user_op.get_init_code(), + callData: user_op.call_data.clone(), + accountGasLimits: FixedBytes::from_slice( + &account_gas_limits.to_be_bytes::<32>(), + ), + preVerificationGas: user_op.pre_verification_gas, + gasFees: FixedBytes::from_slice(&gas_fees.to_be_bytes::<32>()), + paymasterAndData: user_op.get_paymaster_and_data(), + signature: user_op.signature.clone(), + } + } +} + sol! { contract IMulticall3 { #[derive(Default)] @@ -359,3 +469,26 @@ sol! { ) external; } } + +impl From for PBHPayload { + fn from(val: PbhPayload) -> Self { + let p0 = val.proof.0 .0 .0; + let p1 = val.proof.0 .0 .1; + let p2 = val.proof.0 .1 .0[0]; + let p3 = val.proof.0 .1 .0[1]; + let p4 = val.proof.0 .1 .1[0]; + let p5 = val.proof.0 .1 .1[1]; + let p6 = val.proof.0 .2 .0; + let p7 = val.proof.0 .2 .1; + + Self { + root: val.root, + pbhExternalNullifier: EncodedExternalNullifier::from( + val.external_nullifier, + ) + .0, + nullifierHash: val.nullifier_hash, + proof: [p0, p1, p2, p3, p4, p5, p6, p7], + } + } +} diff --git a/bedrock/src/smart_account/nonce.rs b/bedrock/src/smart_account/nonce.rs index 0f1df329..cb588175 100644 --- a/bedrock/src/smart_account/nonce.rs +++ b/bedrock/src/smart_account/nonce.rs @@ -10,7 +10,7 @@ use ruint::aliases::U256; -use crate::primitives::BEDROCK_NONCE_PREFIX_CONST; +use crate::primitives::{BEDROCK_NONCE_PREFIX_CONST, PBH_NONCE_PREFIX_CONST}; /// Stable, never-reordered identifiers for transaction classes. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -51,6 +51,8 @@ pub enum InstructionFlag { /// Follows 4337 specs: #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct NonceKeyV1 { + /// Whether the nonce is for a PBH transaction. + pub pbh: bool, /// Stable transaction class id. pub type_id: TransactionTypeId, /// Instruction flags bitfield. @@ -67,6 +69,7 @@ impl NonceKeyV1 { /// Builds a new v1 nonceKey with a random 7-byte tail. #[must_use] pub fn new( + pbh: bool, type_id: TransactionTypeId, instruction: InstructionFlag, metadata: [u8; 10], @@ -77,6 +80,7 @@ impl NonceKeyV1 { let mut tail = [0u8; 7]; tail.copy_from_slice(&bytes[1..8]); Self { + pbh, type_id, instruction, metadata, @@ -88,12 +92,14 @@ impl NonceKeyV1 { /// Test/advanced constructor allowing explicit random tail specification. #[must_use] pub const fn with_random_tail( + pbh: bool, type_id: TransactionTypeId, instruction: InstructionFlag, metadata: [u8; 10], random_tail: [u8; 7], ) -> Self { Self { + pbh, type_id, instruction, metadata, @@ -107,7 +113,11 @@ impl NonceKeyV1 { fn as_bytes(&self) -> [u8; 24] { let mut out: [u8; 24] = [0u8; 24]; // [0..=4] magic - out[0..=4].copy_from_slice(BEDROCK_NONCE_PREFIX_CONST); + if self.pbh { + out[0..=4].copy_from_slice(PBH_NONCE_PREFIX_CONST); + } else { + out[0..=4].copy_from_slice(BEDROCK_NONCE_PREFIX_CONST); + } // [5] typeId out[5] = self.type_id.as_u8(); // [6] instruction flags @@ -139,6 +149,7 @@ mod tests { let metadata = [0x11u8; 10]; let random_tail = [0x22u8; 7]; let key = NonceKeyV1::with_random_tail( + false, TransactionTypeId::Transfer, InstructionFlag::Default, metadata, @@ -161,6 +172,7 @@ mod tests { #[test] fn test_encode_nonce_sequence_is_zero() { let key = NonceKeyV1::with_random_tail( + false, TransactionTypeId::Transfer, InstructionFlag::Default, [0u8; 10], diff --git a/bedrock/src/smart_account/transaction_4337.rs b/bedrock/src/smart_account/transaction_4337.rs index 9ebc0522..995835e3 100644 --- a/bedrock/src/smart_account/transaction_4337.rs +++ b/bedrock/src/smart_account/transaction_4337.rs @@ -3,13 +3,33 @@ //! A transaction can be initialized through a `UserOperation` struct. //! -use crate::primitives::contracts::{EncodedSafeOpStruct, UserOperation}; +use crate::primitives::contracts::IPBHEntryPoint::PBHPayload; +use crate::primitives::contracts::{ + EncodedSafeOpStruct, IEntryPoint::PackedUserOperation, UserOperation, +}; use crate::primitives::{Network, PrimitiveError}; use crate::smart_account::SafeSmartAccountSigner; use crate::transaction::rpc::{RpcError, SponsorUserOperationResponse}; -use alloy::primitives::{aliases::U48, Address, Bytes, FixedBytes}; +use reqwest::Client; +use ruint::{aliases::U256, uint}; +use semaphore_rs::identity::Identity; +use semaphore_rs::poseidon_tree::LazyPoseidonTree; +use semaphore_rs::{hash_to_field, Field}; + +use alloy::primitives::fixed_bytes; +use alloy::primitives::{aliases::U48, keccak256, Address, Bytes, FixedBytes}; +use alloy::sol_types::SolValue; use chrono::{Duration, Utc}; +use eyre; + +use futures::{stream, StreamExt, TryStreamExt}; +use serde::{Deserialize, Serialize}; +use world_chain_builder_pbh::external_nullifier::{ + EncodedExternalNullifier, ExternalNullifier, +}; +use world_chain_builder_pbh::payload::Proof; +use world_chain_builder_pbh::payload::{PBHPayload as PbhPayload, TREE_DEPTH}; use crate::primitives::contracts::{ENTRYPOINT_4337, GNOSIS_SAFE_4337_MODULE}; @@ -18,6 +38,27 @@ use crate::primitives::contracts::{ENTRYPOINT_4337, GNOSIS_SAFE_4337_MODULE}; /// Operations are valid for this duration from the time they are signed. const USER_OPERATION_VALIDITY_DURATION_HOURS: i64 = 12; +#[derive(Debug, Serialize, Deserialize)] +pub struct SerializableIdentity { + pub nullifier: Field, + pub trapdoor: Field, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct InclusionProof { + pub root: Field, + pub proof: semaphore_rs::poseidon_tree::Proof, +} + +impl From<&Identity> for SerializableIdentity { + fn from(identity: &Identity) -> Self { + Self { + nullifier: identity.nullifier, + trapdoor: identity.trapdoor, + } + } +} + /// Identifies a transaction that can be encoded, signed and executed as a 4337 `UserOperation`. #[allow(async_fn_in_trait)] pub trait Is4337Encodable { @@ -43,6 +84,7 @@ pub trait Is4337Encodable { &self, wallet_address: Address, metadata: Option, + pbh: bool, ) -> Result; /// Signs and executes a 4337 `UserOperation` by: @@ -72,13 +114,17 @@ pub trait Is4337Encodable { safe_account: &crate::smart_account::SafeSmartAccount, self_sponsor_token: Option
, metadata: Option, + pbh: bool, ) -> Result, RpcError> { // Get the global RPC client let rpc_client = crate::transaction::rpc::get_rpc_client()?; // 1. Create preflight UserOperation using default metadata for this implementation - let mut user_operation = - self.as_preflight_user_operation(safe_account.wallet_address, metadata)?; + let mut user_operation = self.as_preflight_user_operation( + safe_account.wallet_address, + metadata, + pbh, + )?; // 2. Request sponsorship let sponsor_response = rpc_client @@ -121,11 +167,19 @@ pub trait Is4337Encodable { )?; // Compose the final signature once (timestamps + actual 65-byte signature) - let mut full_signature = Vec::with_capacity(77); + let mut full_signature = Vec::new(); full_signature.extend_from_slice(&valid_after_bytes); full_signature.extend_from_slice(valid_until_bytes); full_signature.extend_from_slice(&signature.as_bytes()[..]); + // // PBH Logic + if pbh { + let pbh_payload = Self::generate_pbh_proof(&user_operation).await; + full_signature.extend_from_slice( + PBHPayload::from(pbh_payload.clone()).abi_encode().as_ref(), + ); + } + user_operation.signature = full_signature.into(); // 5. Submit UserOperation @@ -135,6 +189,116 @@ pub trait Is4337Encodable { Ok(user_op_hash) } + + /// Generates a PBH proof for a given user operation. + async fn generate_pbh_proof(user_op: &UserOperation) -> PbhPayload { + // Convert from UserOperation to PackedUserOperation + // TODO: Fix this + let packed_user_op: PackedUserOperation = PackedUserOperation::from(user_op); + + let signal = Self::hash_user_op(&packed_user_op); + + let external_nullifier = ExternalNullifier::v1(1, 2025, 11); + + // TODO: Autotmatically find an unused one + let encoded_external_nullifier = + EncodedExternalNullifier::from(external_nullifier); + + let identities: Vec = serde_json::from_reader( + std::fs::File::open("load_test_identities.json").unwrap(), + ) + .unwrap(); + let identities: Vec = identities + .into_iter() + .map(|identity| Identity { + nullifier: identity.nullifier, + trapdoor: identity.trapdoor, + }) + .collect(); + + let proofs = + futures::future::try_join_all(identities.iter().map(|identity| async { + Self::fetch_inclusion_proof( + "https://signup-orb-ethereum.stage-crypto.worldcoin.dev", // Staging + identity, + ) + .await + })) + .await + .unwrap(); + + let identity = identities[0].clone(); + let inclusion_proof = proofs[0].clone(); + + let proof: semaphore_rs_proof::Proof = semaphore_rs::protocol::generate_proof( + &identity, + &inclusion_proof.proof, + encoded_external_nullifier.0, + signal, + ) + .expect("Failed to generate semaphore proof"); + let nullifier_hash = semaphore_rs::protocol::generate_nullifier_hash( + &identity, + encoded_external_nullifier.0, + ); + + let proof = Proof(proof); + + PbhPayload { + external_nullifier, + nullifier_hash, + root: inclusion_proof.root, + proof, + } + } + + /// Fetches an inclusion proof for a given identity from the signup sequencer. + /// + /// This function sends a request to the sequencer to fetch the inclusion proof for a given identity. + /// + /// # Arguments + /// * `url` - The URL of the sequencer + /// * `identity` - The identity to fetch the proof for + async fn fetch_inclusion_proof( + url: &str, + identity: &Identity, + ) -> eyre::Result { + let client = Client::new(); + + let commitment = identity.commitment(); + let response = client + .post(format!("{}/inclusionProof", url)) + .json(&serde_json::json! {{ + "identityCommitment": commitment, + }}) + .send() + .await? + .error_for_status()?; + + let proof: InclusionProof = response.json().await?; + + Ok(proof) + } + + /// Computes a ZK-friendly hash of a PackedUserOperation. + /// + /// This function extracts key fields (sender, nonce, callData) from a PackedUserOperation, + /// encodes them using ABI packed encoding, and converts the result to a Field element + /// suitable for use in zero-knowledge proof circuits. + /// + /// # Arguments + /// * `user_op` - The PackedUserOperation to hash + /// + /// # Returns + /// A Field element representing the hash of the user operation + fn hash_user_op(user_op: &PackedUserOperation) -> Field { + let hash = SolValue::abi_encode_packed(&( + &user_op.sender, + &user_op.nonce, + &user_op.callData, + )); + hash_to_field(hash.as_slice()) + } } #[cfg(test)] @@ -456,4 +620,105 @@ mod tests { panic!("Expected InvalidInput error"); } } + + #[test] + fn test_user_operation_to_encoded_safe_op_struct_conversion() { + use alloy::primitives::{address, U256}; + use std::str::FromStr; + + // Create a UserOperation with valid signature containing timestamps + let mut signature = Vec::with_capacity(77); + + // Add validAfter (6 bytes) - timestamp 1704067200 + let valid_after_timestamp: u64 = 1704067200; + let valid_after_bytes = valid_after_timestamp.to_be_bytes(); + signature.extend_from_slice(&valid_after_bytes[2..8]); + + // Add validUntil (6 bytes) - timestamp 1735689600 + let valid_until_timestamp: u64 = 1735689600; + let valid_until_bytes = valid_until_timestamp.to_be_bytes(); + signature.extend_from_slice(&valid_until_bytes[2..8]); + + // Add dummy ECDSA signature (65 bytes) + signature.extend_from_slice(&[0xff; 65]); + + let user_op = UserOperation { + sender: address!("0x1111111111111111111111111111111111111111"), + nonce: U256::from(42), + call_data: Bytes::from_str("0x1234abcd").unwrap(), + signature: signature.into(), + call_gas_limit: 100000, + verification_gas_limit: 50000, + pre_verification_gas: U256::from(30000), + max_fee_per_gas: 2000000000, + max_priority_fee_per_gas: 1000000000, + ..Default::default() + }; + + // Test the From trait conversion + let encoded_safe_op: EncodedSafeOpStruct = user_op.clone().into(); + + // Verify the conversion worked correctly + assert_eq!(encoded_safe_op.safe, user_op.sender); + assert_eq!(encoded_safe_op.nonce, user_op.nonce); + assert_eq!(encoded_safe_op.call_gas_limit, user_op.call_gas_limit); + assert_eq!( + encoded_safe_op.verification_gas_limit, + user_op.verification_gas_limit + ); + assert_eq!( + encoded_safe_op.pre_verification_gas, + user_op.pre_verification_gas + ); + assert_eq!(encoded_safe_op.max_fee_per_gas, user_op.max_fee_per_gas); + assert_eq!( + encoded_safe_op.max_priority_fee_per_gas, + user_op.max_priority_fee_per_gas + ); + assert_eq!( + encoded_safe_op.valid_after, + U48::from(valid_after_timestamp) + ); + assert_eq!( + encoded_safe_op.valid_until, + U48::from(valid_until_timestamp) + ); + assert_eq!(encoded_safe_op.entry_point, *ENTRYPOINT_4337); + assert_eq!( + encoded_safe_op.call_data_hash, + keccak256(&user_op.call_data) + ); + assert_eq!( + encoded_safe_op.init_code_hash, + keccak256(user_op.get_init_code()) + ); + assert_eq!( + encoded_safe_op.paymaster_and_data_hash, + keccak256(user_op.get_paymaster_and_data()) + ); + } + + #[test] + fn test_user_operation_to_encoded_safe_op_struct_with_invalid_signature() { + use alloy::primitives::{address, U256}; + use std::str::FromStr; + + // Create a UserOperation with invalid signature (too short) + let user_op = UserOperation { + sender: address!("0x1111111111111111111111111111111111111111"), + nonce: U256::from(42), + call_data: Bytes::from_str("0x1234abcd").unwrap(), + signature: vec![0xff; 65].into(), // Invalid length + ..Default::default() + }; + + // Test the From trait conversion with invalid signature + let encoded_safe_op: EncodedSafeOpStruct = user_op.clone().into(); + + // Should use default timestamps (zero) when signature is invalid + assert_eq!(encoded_safe_op.valid_after, U48::ZERO); + assert_eq!(encoded_safe_op.valid_until, U48::ZERO); + assert_eq!(encoded_safe_op.safe, user_op.sender); + assert_eq!(encoded_safe_op.nonce, user_op.nonce); + } } diff --git a/bedrock/src/transaction/contracts/erc20.rs b/bedrock/src/transaction/contracts/erc20.rs index afece8d2..94ba05a1 100644 --- a/bedrock/src/transaction/contracts/erc20.rs +++ b/bedrock/src/transaction/contracts/erc20.rs @@ -103,6 +103,7 @@ impl Is4337Encodable for Erc20 { &self, wallet_address: Address, metadata: Option, + pbh: bool, ) -> Result { let call_data = self.as_execute_user_op_call_data(); @@ -118,7 +119,8 @@ impl Is4337Encodable for Erc20 { } } - let key = NonceKeyV1::new( + let key: NonceKeyV1 = NonceKeyV1::new( + pbh, TransactionTypeId::Transfer, InstructionFlag::Default, metadata_bytes, @@ -168,7 +170,9 @@ mod tests { let wallet = Address::from_str("0x4564420674EA68fcc61b463C0494807C759d47e6").unwrap(); - let user_op = erc20.as_preflight_user_operation(wallet, None).unwrap(); + let user_op = erc20 + .as_preflight_user_operation(wallet, None, false) + .unwrap(); // Check nonce layout let be: [u8; 32] = user_op.nonce.to_be_bytes(); @@ -200,7 +204,7 @@ mod tests { }; let user_op = erc20 - .as_preflight_user_operation(wallet, Some(metadata)) + .as_preflight_user_operation(wallet, Some(metadata), false) .unwrap(); // Check nonce layout diff --git a/bedrock/src/transaction/mod.rs b/bedrock/src/transaction/mod.rs index ed12a695..1f8d967e 100644 --- a/bedrock/src/transaction/mod.rs +++ b/bedrock/src/transaction/mod.rs @@ -63,9 +63,11 @@ impl SafeSmartAccount { pub async fn transaction_transfer( &self, network: Network, + // TODO: Use struct for transaction parameters token_address: &str, to_address: &str, amount: &str, + pbh: bool, ) -> Result { let token_address = Address::parse_from_ffi(token_address, "token_address")?; let to_address = Address::parse_from_ffi(to_address, "address")?; @@ -75,7 +77,7 @@ impl SafeSmartAccount { // Sign and execute the transaction (uses global RPC client automatically) let user_op_hash = transaction - .sign_and_execute(network, self, None, None) + .sign_and_execute(network, self, None, None, pbh) .await .map_err(|e| TransactionError::Generic { message: format!("Failed to execute transaction: {e}"), diff --git a/bedrock/tests/test_smart_account_transfer.rs b/bedrock/tests/test_smart_account_transfer.rs index 9799f679..73de2da7 100644 --- a/bedrock/tests/test_smart_account_transfer.rs +++ b/bedrock/tests/test_smart_account_transfer.rs @@ -232,7 +232,7 @@ where // ------------------ The test for the full transaction_transfer flow ------------------ #[tokio::test] -async fn test_transaction_transfer_full_flow_executes_user_operation( +async fn test_transaction_transfer_full_flow_executes_user_operation_non_pbh( ) -> anyhow::Result<()> { // 1) Spin up anvil fork let anvil = setup_anvil(); @@ -295,6 +295,7 @@ async fn test_transaction_transfer_full_flow_executes_user_operation( &wld_token_address.to_string(), &recipient.to_string(), amount, + false, ) .await .expect("transaction_transfer failed"); From 9a2606f912357e8626f54ba3fc50f74127ddec87 Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Mon, 25 Aug 2025 16:48:17 -0700 Subject: [PATCH 04/34] fix after rebase --- bedrock/src/primitives/contracts.rs | 2 +- bedrock/src/smart_account/transaction_4337.rs | 37 ++++++------------- 2 files changed, 12 insertions(+), 27 deletions(-) diff --git a/bedrock/src/primitives/contracts.rs b/bedrock/src/primitives/contracts.rs index b76a7915..38e5e4c5 100644 --- a/bedrock/src/primitives/contracts.rs +++ b/bedrock/src/primitives/contracts.rs @@ -491,4 +491,4 @@ impl From for PBHPayload { proof: [p0, p1, p2, p3, p4, p5, p6, p7], } } -} \ No newline at end of file +} diff --git a/bedrock/src/smart_account/transaction_4337.rs b/bedrock/src/smart_account/transaction_4337.rs index 182a5872..a79d5886 100644 --- a/bedrock/src/smart_account/transaction_4337.rs +++ b/bedrock/src/smart_account/transaction_4337.rs @@ -3,44 +3,29 @@ //! A transaction can be initialized through a `UserOperation` struct. //! -use crate::primitives::contracts::IPBHEntryPoint::PBHPayload; use crate::primitives::contracts::{ - EncodedSafeOpStruct, IEntryPoint::PackedUserOperation, UserOperation, + EncodedSafeOpStruct, IEntryPoint::PackedUserOperation, IPBHEntryPoint::PBHPayload, + UserOperation, ENTRYPOINT_4337, GNOSIS_SAFE_4337_MODULE, }; use crate::primitives::{Network, PrimitiveError}; use crate::smart_account::SafeSmartAccountSigner; -use crate::transaction::rpc::{RpcError, SponsorUserOperationResponse}; - -use reqwest::Client; -use ruint::{aliases::U256, uint}; -use semaphore_rs::identity::Identity; -use semaphore_rs::poseidon_tree::LazyPoseidonTree; -use semaphore_rs::{hash_to_field, Field}; +use crate::transaction::rpc::{ + RpcError, RpcProviderName, SponsorUserOperationResponse, +}; -use alloy::primitives::fixed_bytes; -use alloy::primitives::{aliases::U48, keccak256, Address, Bytes, FixedBytes}; +use alloy::primitives::{aliases::U48, Address, Bytes, FixedBytes}; use alloy::sol_types::SolValue; use chrono::{Duration, Utc}; use eyre; - -use futures::{stream, StreamExt, TryStreamExt}; +use futures::future; +use reqwest::Client; +use semaphore_rs::identity::Identity; +use semaphore_rs::{hash_to_field, Field}; use serde::{Deserialize, Serialize}; use world_chain_builder_pbh::external_nullifier::{ EncodedExternalNullifier, ExternalNullifier, }; -use world_chain_builder_pbh::payload::Proof; -use world_chain_builder_pbh::payload::{PBHPayload as PbhPayload, TREE_DEPTH}; -use crate::primitives::contracts::{EncodedSafeOpStruct, UserOperation}; -use crate::primitives::{Network, PrimitiveError}; -use crate::smart_account::SafeSmartAccountSigner; -use crate::transaction::rpc::{ - RpcError, RpcProviderName, SponsorUserOperationResponse, -}; - -use alloy::primitives::{aliases::U48, Address, Bytes, FixedBytes}; -use chrono::{Duration, Utc}; - -use crate::primitives::contracts::{ENTRYPOINT_4337, GNOSIS_SAFE_4337_MODULE}; +use world_chain_builder_pbh::payload::{PBHPayload as PbhPayload, Proof}; /// The default validity duration for 4337 `UserOperation` signatures. /// From 99f887aea134f6a0873078ceeceb72d830f77103 Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Mon, 25 Aug 2025 16:51:56 -0700 Subject: [PATCH 05/34] fix cargo sort --- bedrock/Cargo.toml | 2 +- .../tests/test_smart_account_transfer_pbh.rs | 316 ++++++++++++++++++ 2 files changed, 317 insertions(+), 1 deletion(-) create mode 100644 bedrock/tests/test_smart_account_transfer_pbh.rs diff --git a/bedrock/Cargo.toml b/bedrock/Cargo.toml index f63d58e3..9bd35db8 100644 --- a/bedrock/Cargo.toml +++ b/bedrock/Cargo.toml @@ -57,7 +57,7 @@ test-case = "3.3.1" thiserror = "2.0.12" tokio = { version = "1.47.1", features = ["time"], optional = true } uniffi = { workspace = true, features = ["build", "tokio"] } -world-chain-builder-pbh = { git = "https://github.com/worldcoin/world-chain", rev = "4f524c6"} +world-chain-builder-pbh = { git = "https://github.com/worldcoin/world-chain", rev = "4f524c6" } [dev-dependencies] alloy = { version = "1.0.23", default-features = false, features = [ "json", diff --git a/bedrock/tests/test_smart_account_transfer_pbh.rs b/bedrock/tests/test_smart_account_transfer_pbh.rs new file mode 100644 index 00000000..52db5d98 --- /dev/null +++ b/bedrock/tests/test_smart_account_transfer_pbh.rs @@ -0,0 +1,316 @@ +use std::{str::FromStr, sync::Arc}; + +use alloy::{ + network::Ethereum, + primitives::{address, keccak256, Address, U256}, + providers::{ext::AnvilApi, Provider, ProviderBuilder}, + signers::local::PrivateKeySigner, + sol_types::SolValue, +}; + +use bedrock::{ + primitives::{ + http_client::{ + set_http_client, AuthenticatedHttpClient, HttpError, HttpHeader, HttpMethod, + }, + Network, + }, + smart_account::{SafeSmartAccount, ENTRYPOINT_4337}, + transaction::{foreign::UnparsedUserOperation, RpcProviderName}, +}; + +use serde::Serialize; +use serde_json::json; + +mod common; +use common::{deploy_safe, setup_anvil, IEntryPoint, PackedUserOperation, IERC20}; + +// ------------------ Mock HTTP client that actually executes the op on Anvil ------------------ +#[derive(Clone)] +struct AnvilBackedHttpClient

+where + P: Provider + Clone + Send + Sync + 'static, +{ + provider: P, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct SponsorUserOperationResponseLite<'a> { + paymaster: &'a str, + paymaster_data: &'a str, + pre_verification_gas: String, + verification_gas_limit: String, + call_gas_limit: String, + paymaster_verification_gas_limit: String, + paymaster_post_op_gas_limit: String, + max_priority_fee_per_gas: String, + max_fee_per_gas: String, +} + +#[async_trait::async_trait] +impl

AuthenticatedHttpClient for AnvilBackedHttpClient

+where + P: Provider + Clone + Send + Sync + 'static, +{ + async fn fetch_from_app_backend( + &self, + _url: String, + method: HttpMethod, + _headers: Vec, + body: Option>, + ) -> Result, HttpError> { + if method != HttpMethod::Post { + return Err(HttpError::Generic { + message: "unsupported method".into(), + }); + } + + let body = body.ok_or(HttpError::Generic { + message: "missing body".into(), + })?; + + let root: serde_json::Value = + serde_json::from_slice(&body).map_err(|_| HttpError::Generic { + message: "invalid json".into(), + })?; + + let method = + root.get("method") + .and_then(|m| m.as_str()) + .ok_or(HttpError::Generic { + message: "invalid json".into(), + })?; + let id = root.get("id").cloned().unwrap_or(serde_json::Value::Null); + let params = root + .get("params") + .cloned() + .unwrap_or(serde_json::Value::Null); + + match method { + // Respond with minimal, sane gas values and no paymaster + "wa_sponsorUserOperation" => { + let result = SponsorUserOperationResponseLite { + paymaster: "0x0000000000000000000000000000000000000000", + paymaster_data: "0x", + pre_verification_gas: "0x20000".into(), + verification_gas_limit: "0x20000".into(), + call_gas_limit: "0x20000".into(), + paymaster_verification_gas_limit: "0x0".into(), + paymaster_post_op_gas_limit: "0x0".into(), + max_priority_fee_per_gas: "0x3b9aca00".into(), // 1 gwei + max_fee_per_gas: "0x3b9aca00".into(), // 1 gwei + }; + let resp = json!({ + "jsonrpc": "2.0", + "id": id, + "result": result, + }); + Ok(serde_json::to_vec(&resp).unwrap()) + } + // Execute the inner call directly through the Safe 4337 Module (no sponsorship path) + "eth_sendUserOperation" => { + let params = params.as_array().ok_or(HttpError::Generic { + message: "invalid params".into(), + })?; + let user_op_val = params.first().ok_or(HttpError::Generic { + message: "missing userOp param".into(), + })?; + let entry_point_str = params.get(1).and_then(|v| v.as_str()).ok_or( + HttpError::Generic { + message: "missing entryPoint param".into(), + }, + )?; + // Build UnparsedUserOperation from JSON (which uses hex strings), then convert + let obj = user_op_val.as_object().ok_or(HttpError::Generic { + message: "userOp param must be an object".into(), + })?; + + let get_opt = |k: &str| -> Option { + obj.get(k).and_then(|v| v.as_str()).map(|s| s.to_string()) + }; + let get_or_zero = |k: &str| -> String { + get_opt(k).unwrap_or_else(|| "0x0".to_string()) + }; + let get_required = |k: &str| -> Result { + get_opt(k).ok_or(HttpError::Generic { + message: format!("missing or invalid {k}"), + }) + }; + + let unparsed = UnparsedUserOperation { + sender: get_required("sender")?, + nonce: get_required("nonce")?, + call_data: get_required("callData")?, + call_gas_limit: get_or_zero("callGasLimit"), + verification_gas_limit: get_or_zero("verificationGasLimit"), + pre_verification_gas: get_or_zero("preVerificationGas"), + max_fee_per_gas: get_or_zero("maxFeePerGas"), + max_priority_fee_per_gas: get_or_zero("maxPriorityFeePerGas"), + paymaster: get_opt("paymaster"), + paymaster_verification_gas_limit: get_or_zero( + "paymasterVerificationGasLimit", + ), + paymaster_post_op_gas_limit: get_or_zero("paymasterPostOpGasLimit"), + paymaster_data: get_opt("paymasterData"), + signature: get_required("signature")?, + factory: get_opt("factory"), + factory_data: get_opt("factoryData"), + }; + + let user_op: bedrock::smart_account::UserOperation = + unparsed.try_into().map_err(|e| HttpError::Generic { + message: format!("invalid userOp: {e}"), + })?; + + // Convert to the packed format expected by EntryPoint + let packed = PackedUserOperation::try_from(&user_op).map_err(|e| { + HttpError::Generic { + message: format!("pack userOp failed: {e}"), + } + })?; + + // Compute the EntryPoint userOpHash per EIP-4337 spec + let packed_for_hash = + PackedUserOperation::try_from(&user_op).map_err(|e| { + HttpError::Generic { + message: format!("pack userOp for hash failed: {e}"), + } + })?; + let chain_id_u64 = self.provider.get_chain_id().await.map_err(|e| { + HttpError::Generic { + message: format!("getChainId failed: {e}"), + } + })?; + let inner_encoded = ( + packed_for_hash.sender, + packed_for_hash.nonce, + keccak256(packed_for_hash.init_code.clone()), + keccak256(packed_for_hash.call_data.clone()), + packed_for_hash.account_gas_limits, + packed_for_hash.pre_verification_gas, + packed_for_hash.gas_fees, + keccak256(packed_for_hash.paymaster_and_data.clone()), + ) + .abi_encode(); + let inner_hash = keccak256(inner_encoded); + + // Execute via EntryPoint.handleOps on-chain + let entry_point_addr = + Address::from_str(entry_point_str).map_err(|_| { + HttpError::Generic { + message: "invalid entryPoint".into(), + } + })?; + let entry_point = IEntryPoint::new(entry_point_addr, &self.provider); + let _tx = entry_point + .handleOps(vec![packed], user_op.sender) + .send() + .await + .map_err(|e| HttpError::Generic { + message: format!("handleOps failed: {e}"), + })?; + + // Return the chain userOpHash (EntryPoint-wrapped) + let enc = (inner_hash, entry_point_addr, U256::from(chain_id_u64)) + .abi_encode(); + let user_op_hash = keccak256(enc); + + let resp = json!({ + "jsonrpc": "2.0", + "id": id, + "result": format!("0x{}", hex::encode(user_op_hash)), + }); + Ok(serde_json::to_vec(&resp).unwrap()) + } + other => Err(HttpError::Generic { + message: format!("unsupported method {other}"), + }), + } + } +} + +// ------------------ The test for the full transaction_transfer flow ------------------ + +#[tokio::test] +async fn test_transaction_transfer_full_flow_executes_user_operation_non_pbh( +) -> anyhow::Result<()> { + // 1) Spin up anvil fork + let anvil = setup_anvil(); + + // 2) Owner signer and provider + let owner_signer = PrivateKeySigner::random(); + let owner_key_hex = hex::encode(owner_signer.to_bytes()); + let owner = owner_signer.address(); + + let provider = ProviderBuilder::new() + .wallet(owner_signer.clone()) + .connect_http(anvil.endpoint_url()); + + provider + .anvil_set_balance(owner, U256::from(1e18 as u64)) + .await?; + + // 3) Deploy Safe with 4337 module enabled + let safe_address = deploy_safe(&provider, owner, U256::ZERO).await?; + + // 4) Fund EntryPoint deposit for Safe + let entry_point = IEntryPoint::new(*ENTRYPOINT_4337, &provider); + let _ = entry_point + .depositTo(safe_address) + .value(U256::from(1e18 as u64)) + .send() + .await?; + + // 5) Give Safe some ERC-20 balance (WLD on World Chain test contract used in other tests) + let wld_token_address = address!("0x2cFc85d8E48F8EAB294be644d9E25C3030863003"); + let wld = IERC20::new(wld_token_address, &provider); + + // Simulate balance by writing storage slot for mapping(address => uint) at slot 0 + let mut padded = [0u8; 64]; + padded[12..32].copy_from_slice(safe_address.as_slice()); + let slot_hash = keccak256(padded); + let slot = U256::from_be_bytes(slot_hash.into()); + let starting_balance = U256::from(10u128.pow(18) * 10); // 10 WLD + provider + .anvil_set_storage_at(wld_token_address, slot, starting_balance.into()) + .await?; + + // 6) Prepare recipient and assert initial balances + let recipient = PrivateKeySigner::random().address(); + let before_recipient = wld.balanceOf(recipient).call().await?; + let before_safe = wld.balanceOf(safe_address).call().await?; + + // 7) Install mocked HTTP client that routes calls to Anvil + let client = AnvilBackedHttpClient { + provider: provider.clone(), + }; + let _ = set_http_client(Arc::new(client)); + + // 8) Execute high-level transfer via transaction_transfer + let safe_account = SafeSmartAccount::new(owner_key_hex, &safe_address.to_string())?; + let amount = "1000000000000000000"; // 1 WLD + let _user_op_hash = safe_account + .transaction_transfer( + Network::WorldChain, + &wld_token_address.to_string(), + &recipient.to_string(), + amount, + false, + RpcProviderName::Alchemy, + ) + .await + .expect("transaction_transfer failed"); + + // 9) Verify balances updated + let after_recipient = wld.balanceOf(recipient).call().await?; + let after_safe = wld.balanceOf(safe_address).call().await?; + + assert_eq!( + after_recipient, + before_recipient + U256::from(10u128.pow(18)) + ); + assert_eq!(after_safe, before_safe - U256::from(10u128.pow(18))); + + Ok(()) +} From b7629e165828d1f80ada4b5e5cd585ad3601937b Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Thu, 28 Aug 2025 18:28:56 -0700 Subject: [PATCH 06/34] pause --- bedrock/tests/{ => sepolia}/test_smart_account_transfer_pbh.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename bedrock/tests/{ => sepolia}/test_smart_account_transfer_pbh.rs (99%) diff --git a/bedrock/tests/test_smart_account_transfer_pbh.rs b/bedrock/tests/sepolia/test_smart_account_transfer_pbh.rs similarity index 99% rename from bedrock/tests/test_smart_account_transfer_pbh.rs rename to bedrock/tests/sepolia/test_smart_account_transfer_pbh.rs index 52db5d98..c2be6b7f 100644 --- a/bedrock/tests/test_smart_account_transfer_pbh.rs +++ b/bedrock/tests/sepolia/test_smart_account_transfer_pbh.rs @@ -108,7 +108,7 @@ where }); Ok(serde_json::to_vec(&resp).unwrap()) } - // Execute the inner call directly through the Safe 4337 Module (no sponsorship path) + // Send the userOperation to alchemy as Rundler will be able to determine this is a PBH userOperation "eth_sendUserOperation" => { let params = params.as_array().ok_or(HttpError::Generic { message: "invalid params".into(), From 8896f5265b1bef877b096932ad4357d5e013dc31 Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Thu, 28 Aug 2025 19:24:37 -0700 Subject: [PATCH 07/34] pause --- .gitignore | 4 +- bedrock/src/primitives/contracts.rs | 18 ++ bedrock/src/primitives/mod.rs | 4 + bedrock/src/smart_account/transaction_4337.rs | 15 +- .../test_smart_account_transfer_pbh.rs | 184 ++---------------- 5 files changed, 52 insertions(+), 173 deletions(-) diff --git a/.gitignore b/.gitignore index cb28eeff..c0ad5e46 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,6 @@ kotlin/bedrock-tests/bin/ # foundry out/ -load_test_identities.json \ No newline at end of file +load_test_identities.json +test_identities.json +sepolia_secrets.json \ No newline at end of file diff --git a/bedrock/src/primitives/contracts.rs b/bedrock/src/primitives/contracts.rs index 71ffcfd6..86bc574d 100644 --- a/bedrock/src/primitives/contracts.rs +++ b/bedrock/src/primitives/contracts.rs @@ -25,12 +25,30 @@ pub static ENTRYPOINT_4337: LazyLock

= LazyLock::new(|| { .expect("failed to decode ENTRYPOINT_4337") }); +/// Multichain address for PBH_ENTRYPOINT_4337 +/// Contract reference: +pub static PBH_ENTRYPOINT_4337: LazyLock
= LazyLock::new(|| { + Address::from_str("0x0000000000A21818Ee9F93BB4f2AAad305b5397C") + .expect("failed to decode PBH_ENTRYPOINT_4337") +}); + /// Multichain address for the v0.3.0 `Safe4337Module` pub static GNOSIS_SAFE_4337_MODULE: LazyLock
= LazyLock::new(|| { Address::from_str("0x75cf11467937ce3f2f357ce24ffc3dbf8fd5c226") .expect("failed to decode GNOSIS_SAFE_4337_MODULE") }); +/// Contract reference: +pub static PBH_SAFE_4337_MODULE_SEPOLIA: LazyLock
= LazyLock::new(|| { + Address::from_str("0xeA5877676caC52d51DCEc80e4Ff33898d5B0E8D9") + .expect("failed to decode GNOSIS_SAFE_4337_MODULE") +}); + +pub static PBH_SAFE_4337_MODULE_MAINNET: LazyLock
= LazyLock::new(|| { + Address::from_str("0xb5b2a890a5ED55B07A27d014AdaAC113A545a96c") + .expect("failed to decode GNOSIS_SAFE_4337_MODULE") +}); + /// The length of a 4337 `UserOperation` signature. /// /// This is the length of a regular ECDSA signature with r,s,v (32 + 32 + 1 = 65 bytes) + 12 bytes for the validity timestamps. diff --git a/bedrock/src/primitives/mod.rs b/bedrock/src/primitives/mod.rs index 00821997..fa7637c4 100644 --- a/bedrock/src/primitives/mod.rs +++ b/bedrock/src/primitives/mod.rs @@ -72,6 +72,8 @@ pub enum Network { Optimism = 10, /// World Chain (chain ID: 480) WorldChain = 480, + /// World Chain Sepolia (chain ID: 481) + WorldChainSepolia = 4801, } impl Network { @@ -82,6 +84,7 @@ impl Network { Self::Ethereum => "ethereum", Self::Optimism => "optimism", Self::WorldChain => "worldchain", + Self::WorldChainSepolia => "worldchain-sepolia", } } @@ -92,6 +95,7 @@ impl Network { Self::Ethereum => "Ethereum", Self::Optimism => "Optimism", Self::WorldChain => "World Chain", + Self::WorldChainSepolia => "World Chain Sepolia", } } } diff --git a/bedrock/src/smart_account/transaction_4337.rs b/bedrock/src/smart_account/transaction_4337.rs index 0129636b..e3a9d90a 100644 --- a/bedrock/src/smart_account/transaction_4337.rs +++ b/bedrock/src/smart_account/transaction_4337.rs @@ -3,6 +3,7 @@ //! A transaction can be initialized through a `UserOperation` struct. //! +use crate::primitives::contracts::{PBH_ENTRYPOINT_4337, PBH_ENTRYPOINT_4337_MAINNET, PBH_ENTRYPOINT_4337_SEPOLIA}; use crate::primitives::contracts::{ EncodedSafeOpStruct, IEntryPoint::PackedUserOperation, IPBHEntryPoint::PBHPayload, UserOperation, ENTRYPOINT_4337, GNOSIS_SAFE_4337_MODULE, @@ -119,12 +120,18 @@ pub trait Is4337Encodable { pbh, )?; + let entrypoint = if pbh { + *PBH_ENTRYPOINT_4337 + } else { + *ENTRYPOINT_4337 + }; + // 2. Request sponsorship let sponsor_response = rpc_client .sponsor_user_operation( network, &user_operation, - *ENTRYPOINT_4337, + entrypoint, self_sponsor_token, provider, ) @@ -178,7 +185,7 @@ pub trait Is4337Encodable { // 5. Submit UserOperation let user_op_hash = rpc_client - .send_user_operation(network, &user_operation, *ENTRYPOINT_4337, provider) + .send_user_operation(network, &user_operation, entrypoint, provider) .await?; Ok(user_op_hash) @@ -192,14 +199,14 @@ pub trait Is4337Encodable { let signal = Self::hash_user_op(&packed_user_op); - let external_nullifier = ExternalNullifier::v1(1, 2025, 11); + let external_nullifier = ExternalNullifier::v1(8, 2025, 0); // TODO: Autotmatically find an unused one let encoded_external_nullifier = EncodedExternalNullifier::from(external_nullifier); let identities: Vec = serde_json::from_reader( - std::fs::File::open("load_test_identities.json").unwrap(), + std::fs::File::open("test_identities.json").unwrap(), ) .unwrap(); let identities: Vec = identities diff --git a/bedrock/tests/sepolia/test_smart_account_transfer_pbh.rs b/bedrock/tests/sepolia/test_smart_account_transfer_pbh.rs index c2be6b7f..59b486af 100644 --- a/bedrock/tests/sepolia/test_smart_account_transfer_pbh.rs +++ b/bedrock/tests/sepolia/test_smart_account_transfer_pbh.rs @@ -108,121 +108,6 @@ where }); Ok(serde_json::to_vec(&resp).unwrap()) } - // Send the userOperation to alchemy as Rundler will be able to determine this is a PBH userOperation - "eth_sendUserOperation" => { - let params = params.as_array().ok_or(HttpError::Generic { - message: "invalid params".into(), - })?; - let user_op_val = params.first().ok_or(HttpError::Generic { - message: "missing userOp param".into(), - })?; - let entry_point_str = params.get(1).and_then(|v| v.as_str()).ok_or( - HttpError::Generic { - message: "missing entryPoint param".into(), - }, - )?; - // Build UnparsedUserOperation from JSON (which uses hex strings), then convert - let obj = user_op_val.as_object().ok_or(HttpError::Generic { - message: "userOp param must be an object".into(), - })?; - - let get_opt = |k: &str| -> Option { - obj.get(k).and_then(|v| v.as_str()).map(|s| s.to_string()) - }; - let get_or_zero = |k: &str| -> String { - get_opt(k).unwrap_or_else(|| "0x0".to_string()) - }; - let get_required = |k: &str| -> Result { - get_opt(k).ok_or(HttpError::Generic { - message: format!("missing or invalid {k}"), - }) - }; - - let unparsed = UnparsedUserOperation { - sender: get_required("sender")?, - nonce: get_required("nonce")?, - call_data: get_required("callData")?, - call_gas_limit: get_or_zero("callGasLimit"), - verification_gas_limit: get_or_zero("verificationGasLimit"), - pre_verification_gas: get_or_zero("preVerificationGas"), - max_fee_per_gas: get_or_zero("maxFeePerGas"), - max_priority_fee_per_gas: get_or_zero("maxPriorityFeePerGas"), - paymaster: get_opt("paymaster"), - paymaster_verification_gas_limit: get_or_zero( - "paymasterVerificationGasLimit", - ), - paymaster_post_op_gas_limit: get_or_zero("paymasterPostOpGasLimit"), - paymaster_data: get_opt("paymasterData"), - signature: get_required("signature")?, - factory: get_opt("factory"), - factory_data: get_opt("factoryData"), - }; - - let user_op: bedrock::smart_account::UserOperation = - unparsed.try_into().map_err(|e| HttpError::Generic { - message: format!("invalid userOp: {e}"), - })?; - - // Convert to the packed format expected by EntryPoint - let packed = PackedUserOperation::try_from(&user_op).map_err(|e| { - HttpError::Generic { - message: format!("pack userOp failed: {e}"), - } - })?; - - // Compute the EntryPoint userOpHash per EIP-4337 spec - let packed_for_hash = - PackedUserOperation::try_from(&user_op).map_err(|e| { - HttpError::Generic { - message: format!("pack userOp for hash failed: {e}"), - } - })?; - let chain_id_u64 = self.provider.get_chain_id().await.map_err(|e| { - HttpError::Generic { - message: format!("getChainId failed: {e}"), - } - })?; - let inner_encoded = ( - packed_for_hash.sender, - packed_for_hash.nonce, - keccak256(packed_for_hash.init_code.clone()), - keccak256(packed_for_hash.call_data.clone()), - packed_for_hash.account_gas_limits, - packed_for_hash.pre_verification_gas, - packed_for_hash.gas_fees, - keccak256(packed_for_hash.paymaster_and_data.clone()), - ) - .abi_encode(); - let inner_hash = keccak256(inner_encoded); - - // Execute via EntryPoint.handleOps on-chain - let entry_point_addr = - Address::from_str(entry_point_str).map_err(|_| { - HttpError::Generic { - message: "invalid entryPoint".into(), - } - })?; - let entry_point = IEntryPoint::new(entry_point_addr, &self.provider); - let _tx = entry_point - .handleOps(vec![packed], user_op.sender) - .send() - .await - .map_err(|e| HttpError::Generic { - message: format!("handleOps failed: {e}"), - })?; - - // Return the chain userOpHash (EntryPoint-wrapped) - let enc = (inner_hash, entry_point_addr, U256::from(chain_id_u64)) - .abi_encode(); - let user_op_hash = keccak256(enc); - - let resp = json!({ - "jsonrpc": "2.0", - "id": id, - "result": format!("0x{}", hex::encode(user_op_hash)), - }); - Ok(serde_json::to_vec(&resp).unwrap()) - } other => Err(HttpError::Generic { message: format!("unsupported method {other}"), }), @@ -233,53 +118,25 @@ where // ------------------ The test for the full transaction_transfer flow ------------------ #[tokio::test] -async fn test_transaction_transfer_full_flow_executes_user_operation_non_pbh( +async fn test_pbh_transaction_transfer_full_flow( ) -> anyhow::Result<()> { - // 1) Spin up anvil fork - let anvil = setup_anvil(); + let secrets = std::fs::read_to_string("sepolia_secrets.json")?; + let secrets: Vec = serde_json::from_str(&secrets)?; + let secret = secrets[0].clone(); + let private_key = secret["private_key"].as_str().unwrap(); + let safe_address = secret["safe_address"].as_str().unwrap(); + let rpc_url = secret["rpc_url"].as_str().unwrap(); + + let owner_signer = PrivateKeySigner::from_slice( + &hex::decode(private_key)?, + )?; - // 2) Owner signer and provider - let owner_signer = PrivateKeySigner::random(); let owner_key_hex = hex::encode(owner_signer.to_bytes()); let owner = owner_signer.address(); let provider = ProviderBuilder::new() .wallet(owner_signer.clone()) - .connect_http(anvil.endpoint_url()); - - provider - .anvil_set_balance(owner, U256::from(1e18 as u64)) - .await?; - - // 3) Deploy Safe with 4337 module enabled - let safe_address = deploy_safe(&provider, owner, U256::ZERO).await?; - - // 4) Fund EntryPoint deposit for Safe - let entry_point = IEntryPoint::new(*ENTRYPOINT_4337, &provider); - let _ = entry_point - .depositTo(safe_address) - .value(U256::from(1e18 as u64)) - .send() - .await?; - - // 5) Give Safe some ERC-20 balance (WLD on World Chain test contract used in other tests) - let wld_token_address = address!("0x2cFc85d8E48F8EAB294be644d9E25C3030863003"); - let wld = IERC20::new(wld_token_address, &provider); - - // Simulate balance by writing storage slot for mapping(address => uint) at slot 0 - let mut padded = [0u8; 64]; - padded[12..32].copy_from_slice(safe_address.as_slice()); - let slot_hash = keccak256(padded); - let slot = U256::from_be_bytes(slot_hash.into()); - let starting_balance = U256::from(10u128.pow(18) * 10); // 10 WLD - provider - .anvil_set_storage_at(wld_token_address, slot, starting_balance.into()) - .await?; - - // 6) Prepare recipient and assert initial balances - let recipient = PrivateKeySigner::random().address(); - let before_recipient = wld.balanceOf(recipient).call().await?; - let before_safe = wld.balanceOf(safe_address).call().await?; + .connect_http(rpc_url); // 7) Install mocked HTTP client that routes calls to Anvil let client = AnvilBackedHttpClient { @@ -289,28 +146,19 @@ async fn test_transaction_transfer_full_flow_executes_user_operation_non_pbh( // 8) Execute high-level transfer via transaction_transfer let safe_account = SafeSmartAccount::new(owner_key_hex, &safe_address.to_string())?; - let amount = "1000000000000000000"; // 1 WLD + let amount = "1"; + let _user_op_hash = safe_account .transaction_transfer( Network::WorldChain, - &wld_token_address.to_string(), + &"0xC82Ea35634BcE95C394B6BC00626f827bB0F4801".to_string(), // WORLD SEPOLIA LINK TOKEN &recipient.to_string(), amount, - false, + true, RpcProviderName::Alchemy, ) .await .expect("transaction_transfer failed"); - // 9) Verify balances updated - let after_recipient = wld.balanceOf(recipient).call().await?; - let after_safe = wld.balanceOf(safe_address).call().await?; - - assert_eq!( - after_recipient, - before_recipient + U256::from(10u128.pow(18)) - ); - assert_eq!(after_safe, before_safe - U256::from(10u128.pow(18))); - Ok(()) } From a459ddbd8fac8f290bc0c293a641f23959e85720 Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Thu, 28 Aug 2025 20:38:53 -0700 Subject: [PATCH 08/34] working --- bedrock/src/primitives/contracts.rs | 8 +++ bedrock/src/smart_account/transaction_4337.rs | 49 ++++++++++----- bedrock/src/transaction/rpc.rs | 31 +++++++++- bedrock/tests/test_smart_account_nonce.rs | 1 + .../test_smart_account_transfer_pbh.rs | 60 ++++++++++++------- 5 files changed, 109 insertions(+), 40 deletions(-) rename bedrock/tests/{sepolia => }/test_smart_account_transfer_pbh.rs (74%) diff --git a/bedrock/src/primitives/contracts.rs b/bedrock/src/primitives/contracts.rs index 86bc574d..5d8eb09a 100644 --- a/bedrock/src/primitives/contracts.rs +++ b/bedrock/src/primitives/contracts.rs @@ -44,6 +44,7 @@ pub static PBH_SAFE_4337_MODULE_SEPOLIA: LazyLock
= LazyLock::new(|| { .expect("failed to decode GNOSIS_SAFE_4337_MODULE") }); +/// Contract reference: pub static PBH_SAFE_4337_MODULE_MAINNET: LazyLock
= LazyLock::new(|| { Address::from_str("0xb5b2a890a5ED55B07A27d014AdaAC113A545a96c") .expect("failed to decode GNOSIS_SAFE_4337_MODULE") @@ -174,6 +175,13 @@ impl UserOperation { nonce, call_data, signature: vec![0xff; USER_OPERATION_SIGNATURE_LENGTH].into(), + factory: Address::ZERO, + factory_data: Bytes::new(), + // call_gas_limit: U128::MAX, + // verification_gas_limit: U128::MAX, + // pre_verification_gas: U256::MAX, + // max_priority_fee_per_gas: U128::MAX, + // max_fee_per_gas: U128::MAX, ..Default::default() } } diff --git a/bedrock/src/smart_account/transaction_4337.rs b/bedrock/src/smart_account/transaction_4337.rs index e3a9d90a..b0fc640d 100644 --- a/bedrock/src/smart_account/transaction_4337.rs +++ b/bedrock/src/smart_account/transaction_4337.rs @@ -3,11 +3,15 @@ //! A transaction can be initialized through a `UserOperation` struct. //! -use crate::primitives::contracts::{PBH_ENTRYPOINT_4337, PBH_ENTRYPOINT_4337_MAINNET, PBH_ENTRYPOINT_4337_SEPOLIA}; use crate::primitives::contracts::{ EncodedSafeOpStruct, IEntryPoint::PackedUserOperation, IPBHEntryPoint::PBHPayload, UserOperation, ENTRYPOINT_4337, GNOSIS_SAFE_4337_MODULE, }; + +use alloy_primitives::keccak256; +use crate::primitives::contracts::{ + PBH_ENTRYPOINT_4337, PBH_SAFE_4337_MODULE_SEPOLIA, +}; use crate::primitives::{Network, PrimitiveError}; use crate::smart_account::SafeSmartAccountSigner; use crate::transaction::rpc::{RpcError, RpcProviderName}; @@ -143,6 +147,7 @@ pub trait Is4337Encodable { // 4. Compute validity timestamps // validAfter = 0 (immediately valid) let valid_after_u48 = U48::from(0u64); + // TODO: Set real value here? let valid_after_bytes: [u8; 6] = [0u8; 6]; // Set validUntil to the configured duration from now @@ -164,7 +169,8 @@ pub trait Is4337Encodable { let signature = safe_account.sign_digest( encoded_safe_op.into_transaction_hash(), network as u32, - Some(*GNOSIS_SAFE_4337_MODULE), + // Some(*GNOSIS_SAFE_4337_MODULE), + Some(*PBH_SAFE_4337_MODULE_SEPOLIA), )?; // Compose the final signature once (timestamps + actual 65-byte signature) @@ -173,7 +179,7 @@ pub trait Is4337Encodable { full_signature.extend_from_slice(valid_until_bytes); full_signature.extend_from_slice(&signature.as_bytes()[..]); - // // PBH Logic + // // // PBH Logic if pbh { let pbh_payload = Self::generate_pbh_proof(&user_operation).await; full_signature.extend_from_slice( @@ -183,9 +189,13 @@ pub trait Is4337Encodable { user_operation.signature = full_signature.into(); + // println!("user_operation: {user_operation:?}"); + // 5. Submit UserOperation - let user_op_hash = rpc_client - .send_user_operation(network, &user_operation, entrypoint, provider) + let user_op_hash: FixedBytes<32> = rpc_client + // Always send to standard 4337 entrypoint even for PBH + // The bundler will route it to the PBH entrypoint if it's a PBH transaction + .send_user_operation(network, &user_operation, *ENTRYPOINT_4337, provider) .await?; Ok(user_op_hash) @@ -205,17 +215,19 @@ pub trait Is4337Encodable { let encoded_external_nullifier = EncodedExternalNullifier::from(external_nullifier); - let identities: Vec = serde_json::from_reader( - std::fs::File::open("test_identities.json").unwrap(), - ) - .unwrap(); - let identities: Vec = identities - .into_iter() - .map(|identity| Identity { - nullifier: identity.nullifier, - trapdoor: identity.trapdoor, - }) - .collect(); + + let secrets: String = std::fs::read_to_string("tests/sepolia_secrets.json").unwrap(); + + let secret: serde_json::Value = serde_json::from_str(&secrets).unwrap(); + let nullifier = secret["nullifier"].as_str().unwrap(); + let trapdoor = secret["trapdoor"].as_str().unwrap(); + + let identity = Identity { + nullifier: nullifier.parse().unwrap(), + trapdoor: trapdoor.parse().unwrap(), + }; + + let identities = vec![identity]; let proofs = futures::future::try_join_all(identities.iter().map(|identity| async { @@ -231,6 +243,8 @@ pub trait Is4337Encodable { let identity = identities[0].clone(); let inclusion_proof = proofs[0].clone(); + // println!("inclusion_proof: {inclusion_proof:?}"); + let proof: semaphore_rs_proof::Proof = semaphore_rs::protocol::generate_proof( &identity, &inclusion_proof.proof, @@ -238,6 +252,9 @@ pub trait Is4337Encodable { signal, ) .expect("Failed to generate semaphore proof"); + + // println!("proof: {proof:?}"); + let nullifier_hash = semaphore_rs::protocol::generate_nullifier_hash( &identity, encoded_external_nullifier.0, diff --git a/bedrock/src/transaction/rpc.rs b/bedrock/src/transaction/rpc.rs index 6f8749ba..dae86226 100644 --- a/bedrock/src/transaction/rpc.rs +++ b/bedrock/src/transaction/rpc.rs @@ -313,8 +313,37 @@ impl RpcClient { entrypoint: Address, provider: RpcProviderName, ) -> Result, RpcError> { + + // TODO: Move this logic to the right place for serialization + + // Serialize user operation, removing factory and factory_data if factory is address(0) + let mut user_op_value = serde_json::to_value(user_operation).map_err(|_| RpcError::JsonError)?; + + if let Some(user_op_obj) = user_op_value.as_object_mut() { + // Remove factory and factory_data if factory is address(0) + if let Some(factory_value) = user_op_obj.get("factory") { + if factory_value == "0x0000000000000000000000000000000000000000" { + user_op_obj.remove("factory"); + user_op_obj.remove("factoryData"); + } + } + + // Remove paymaster fields if paymaster is address(0) or None + if let Some(paymaster_value) = user_op_obj.get("paymaster") { + if paymaster_value == "0x0000000000000000000000000000000000000000" || paymaster_value.is_null() { + user_op_obj.remove("paymaster"); + user_op_obj.remove("paymasterData"); + user_op_obj.remove("paymasterPostOpGasLimit"); + user_op_obj.remove("paymasterVerificationGasLimit"); + } + } + + // Add aggregator field + user_op_obj.insert("aggregator".to_string(), serde_json::Value::String("0x8af27ee9af538c48c7d2a2c8bd6a40ef830e2489".to_string())); + } + let params = vec![ - serde_json::to_value(user_operation).map_err(|_| RpcError::JsonError)?, + user_op_value, serde_json::Value::String(format!("{entrypoint:?}")), ]; diff --git a/bedrock/tests/test_smart_account_nonce.rs b/bedrock/tests/test_smart_account_nonce.rs index 50722b48..65f48b82 100644 --- a/bedrock/tests/test_smart_account_nonce.rs +++ b/bedrock/tests/test_smart_account_nonce.rs @@ -51,6 +51,7 @@ async fn test_rust_nonce_matches_solidity_encoding() -> anyhow::Result<()> { let metadata: [u8; 10] = [0x11; 10]; let random_tail: [u8; 7] = [0x22; 7]; let rust_nonce = NonceKeyV1::with_random_tail( + false, TransactionTypeId::Transfer, InstructionFlag::Default, metadata, diff --git a/bedrock/tests/sepolia/test_smart_account_transfer_pbh.rs b/bedrock/tests/test_smart_account_transfer_pbh.rs similarity index 74% rename from bedrock/tests/sepolia/test_smart_account_transfer_pbh.rs rename to bedrock/tests/test_smart_account_transfer_pbh.rs index 59b486af..c2a1fca7 100644 --- a/bedrock/tests/sepolia/test_smart_account_transfer_pbh.rs +++ b/bedrock/tests/test_smart_account_transfer_pbh.rs @@ -1,12 +1,11 @@ -use std::{str::FromStr, sync::Arc}; +use std::sync::Arc; use alloy::{ network::Ethereum, - primitives::{address, keccak256, Address, U256}, - providers::{ext::AnvilApi, Provider, ProviderBuilder}, + providers::{Provider, ProviderBuilder}, signers::local::PrivateKeySigner, - sol_types::SolValue, }; +use reqwest::Url; use bedrock::{ primitives::{ @@ -15,15 +14,14 @@ use bedrock::{ }, Network, }, - smart_account::{SafeSmartAccount, ENTRYPOINT_4337}, - transaction::{foreign::UnparsedUserOperation, RpcProviderName}, + smart_account::SafeSmartAccount, + transaction::RpcProviderName, }; use serde::Serialize; use serde_json::json; mod common; -use common::{deploy_safe, setup_anvil, IEntryPoint, PackedUserOperation, IERC20}; // ------------------ Mock HTTP client that actually executes the op on Anvil ------------------ #[derive(Clone)] @@ -75,19 +73,19 @@ where message: "invalid json".into(), })?; - let method = - root.get("method") - .and_then(|m| m.as_str()) - .ok_or(HttpError::Generic { - message: "invalid json".into(), - })?; + let method = root.get("method") + .and_then(|m| m.as_str()) + .ok_or(HttpError::Generic { + message: "invalid json".into(), + })? + .to_string(); let id = root.get("id").cloned().unwrap_or(serde_json::Value::Null); let params = root .get("params") .cloned() .unwrap_or(serde_json::Value::Null); - match method { + match method.as_str() { // Respond with minimal, sane gas values and no paymaster "wa_sponsorUserOperation" => { let result = SponsorUserOperationResponseLite { @@ -108,9 +106,25 @@ where }); Ok(serde_json::to_vec(&resp).unwrap()) } - other => Err(HttpError::Generic { - message: format!("unsupported method {other}"), - }), + // Forward all other methods to the actual provider + _ => { + + println!("method: {method}"); + println!("params: {params}"); + + // Forward the JSON-RPC request to the provider + let response = self.provider.raw_request::(method.into(), params).await + .map_err(|e| HttpError::Generic { + message: format!("Provider request failed: {}", e), + })?; + + let resp = json!({ + "jsonrpc": "2.0", + "id": id, + "result": response, + }); + Ok(serde_json::to_vec(&resp).unwrap()) + } } } } @@ -120,19 +134,18 @@ where #[tokio::test] async fn test_pbh_transaction_transfer_full_flow( ) -> anyhow::Result<()> { - let secrets = std::fs::read_to_string("sepolia_secrets.json")?; - let secrets: Vec = serde_json::from_str(&secrets)?; - let secret = secrets[0].clone(); + let secrets: String = std::fs::read_to_string("tests/sepolia_secrets.json")?; + + let secret: serde_json::Value = serde_json::from_str(&secrets)?; let private_key = secret["private_key"].as_str().unwrap(); let safe_address = secret["safe_address"].as_str().unwrap(); - let rpc_url = secret["rpc_url"].as_str().unwrap(); + let rpc_url: Url = secret["rpc_url"].as_str().unwrap().parse()?; let owner_signer = PrivateKeySigner::from_slice( &hex::decode(private_key)?, )?; let owner_key_hex = hex::encode(owner_signer.to_bytes()); - let owner = owner_signer.address(); let provider = ProviderBuilder::new() .wallet(owner_signer.clone()) @@ -147,10 +160,11 @@ async fn test_pbh_transaction_transfer_full_flow( // 8) Execute high-level transfer via transaction_transfer let safe_account = SafeSmartAccount::new(owner_key_hex, &safe_address.to_string())?; let amount = "1"; + let recipient = safe_address; let _user_op_hash = safe_account .transaction_transfer( - Network::WorldChain, + Network::WorldChainSepolia, &"0xC82Ea35634BcE95C394B6BC00626f827bB0F4801".to_string(), // WORLD SEPOLIA LINK TOKEN &recipient.to_string(), amount, From 28becaf1fb7ca3bf6f2242c2aaeec863030ab27d Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Fri, 29 Aug 2025 13:43:25 -0700 Subject: [PATCH 09/34] move code to world_id primitive file --- bedrock/src/primitives/mod.rs | 3 + bedrock/src/primitives/world_id.rs | 165 ++++++++++++++++++ bedrock/src/smart_account/transaction_4337.rs | 143 ++------------- bedrock/src/transaction/rpc.rs | 23 ++- .../tests/test_smart_account_transfer_pbh.rs | 36 ++-- 5 files changed, 217 insertions(+), 153 deletions(-) create mode 100644 bedrock/src/primitives/world_id.rs diff --git a/bedrock/src/primitives/mod.rs b/bedrock/src/primitives/mod.rs index fa7637c4..ed6f17dc 100644 --- a/bedrock/src/primitives/mod.rs +++ b/bedrock/src/primitives/mod.rs @@ -62,6 +62,9 @@ pub mod tooling_tests; /// Contract interfaces and data structures for ERC-4337 account abstraction pub mod contracts; +/// Introduces World ID identity functionality for Bedrock operations +pub mod world_id; + /// Supported blockchain networks for Bedrock operations #[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)] #[repr(u32)] diff --git a/bedrock/src/primitives/world_id.rs b/bedrock/src/primitives/world_id.rs new file mode 100644 index 00000000..a9b3ecf2 --- /dev/null +++ b/bedrock/src/primitives/world_id.rs @@ -0,0 +1,165 @@ +use alloy::sol_types::SolValue; +use reqwest::Client; +use semaphore_rs::identity::Identity; +use semaphore_rs::{hash_to_field, Field}; +use serde::{Deserialize, Serialize}; +use std::sync::{Arc, OnceLock}; +use world_chain_builder_pbh::{ + external_nullifier::{EncodedExternalNullifier, ExternalNullifier}, + payload::{PBHPayload as PbhPayload, Proof}, +}; + +use crate::{ + primitives::contracts::IEntryPoint::PackedUserOperation, + smart_account::UserOperation, +}; + +/// Inclusion proof for a given identity. +/// +/// This struct contains the root of the Merkle tree and the proof for a given identity. +/// +/// # Fields +/// * `root` - The root of the Merkle tree +/// * `proof` - The proof for the given identity +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct InclusionProof { + /// The root of the Merkle tree + pub root: Field, + /// The proof for the given identity + pub proof: semaphore_rs::poseidon_tree::Proof, +} + +/// Global World ID identity instance for Bedrock operations +static WORLD_ID_IDENTITY_INSTANCE: OnceLock> = OnceLock::new(); + +/// Sets the World ID identity. +/// +/// This function sets the World ID identity for Bedrock operations. +/// +/// # Arguments +/// * `identity` - The World ID identity to set +/// +/// # Returns +/// true if the World ID identity was set successfully, false otherwise. +/// +/// # Errors +/// This function will return false if the World ID identity is already initialized. +pub fn set_world_id_identity(identity: Arc) -> bool { + if WORLD_ID_IDENTITY_INSTANCE.set(identity).is_err() { + crate::warn!("World ID identity already initialized, ignoring"); + false + } else { + crate::info!("World ID identity initialized successfully"); + true + } +} + +/// Gets the World ID identity. +/// +/// # Returns +/// The World ID identity if it has been initialized, None otherwise. +pub fn get_world_id_identity() -> Option> { + WORLD_ID_IDENTITY_INSTANCE.get().cloned() +} + +/// Checks if the World ID identity has been initialized. +/// +/// # Returns +/// true if the World ID identity has been initialized, false otherwise. +pub fn is_world_id_identity_initialized() -> bool { + WORLD_ID_IDENTITY_INSTANCE.get().is_some() +} + +/// Generates a PBH proof for a given user operation. +pub async fn generate_pbh_proof(user_op: &UserOperation) -> PbhPayload { + // Convert from UserOperation to PackedUserOperation + // TODO: Fix this + let packed_user_op: PackedUserOperation = PackedUserOperation::from(user_op); + + let signal = hash_user_op(&packed_user_op); + + let external_nullifier = ExternalNullifier::v1(8, 2025, 1); + + // TODO: Autotmatically find an unused one + let encoded_external_nullifier = EncodedExternalNullifier::from(external_nullifier); + + let identity = get_world_id_identity().unwrap(); + + let inclusion_proof = fetch_inclusion_proof( + // TODO: Handle different envs + "https://signup-orb-ethereum.stage-crypto.worldcoin.dev", // Staging + &identity, + ) + .await + .unwrap(); + + let proof: semaphore_rs_proof::Proof = semaphore_rs::protocol::generate_proof( + &identity, + &inclusion_proof.proof, + encoded_external_nullifier.0, + signal, + ) + .expect("Failed to generate semaphore proof"); + + let nullifier_hash = semaphore_rs::protocol::generate_nullifier_hash( + &identity, + encoded_external_nullifier.0, + ); + + let proof = Proof(proof); + + PbhPayload { + external_nullifier, + nullifier_hash, + root: inclusion_proof.root, + proof, + } +} + +/// Fetches an inclusion proof for a given identity from the signup sequencer. +/// +/// This function sends a request to the sequencer to fetch the inclusion proof for a given identity. +/// +/// # Arguments +/// * `url` - The URL of the sequencer +/// * `identity` - The identity to fetch the proof for +pub async fn fetch_inclusion_proof( + url: &str, + identity: &Identity, +) -> eyre::Result { + let client = Client::new(); + + let commitment = identity.commitment(); + let response = client + .post(format!("{}/inclusionProof", url)) + .json(&serde_json::json! {{ + "identityCommitment": commitment, + }}) + .send() + .await? + .error_for_status()?; + + let proof: InclusionProof = response.json().await?; + + Ok(proof) +} + +/// Computes a ZK-friendly hash of a PackedUserOperation. +/// +/// This function extracts key fields (sender, nonce, callData) from a PackedUserOperation, +/// encodes them using ABI packed encoding, and converts the result to a Field element +/// suitable for use in zero-knowledge proof circuits. +/// +/// # Arguments +/// * `user_op` - The PackedUserOperation to hash +/// +/// # Returns +/// A Field element representing the hash of the user operation +pub fn hash_user_op(user_op: &PackedUserOperation) -> Field { + let hash = SolValue::abi_encode_packed(&( + &user_op.sender, + &user_op.nonce, + &user_op.callData, + )); + hash_to_field(hash.as_slice()) +} diff --git a/bedrock/src/smart_account/transaction_4337.rs b/bedrock/src/smart_account/transaction_4337.rs index 6ef7848e..a3b5729d 100644 --- a/bedrock/src/smart_account/transaction_4337.rs +++ b/bedrock/src/smart_account/transaction_4337.rs @@ -8,13 +8,14 @@ use crate::primitives::contracts::{ UserOperation, ENTRYPOINT_4337, GNOSIS_SAFE_4337_MODULE, }; -use alloy_primitives::keccak256; -use crate::primitives::contracts::{ - PBH_ENTRYPOINT_4337, PBH_SAFE_4337_MODULE_SEPOLIA, -}; +use crate::primitives::contracts::{PBH_ENTRYPOINT_4337, PBH_SAFE_4337_MODULE_SEPOLIA}; use crate::primitives::{Network, PrimitiveError}; use crate::smart_account::SafeSmartAccountSigner; + +use crate::primitives::world_id::generate_pbh_proof; + use crate::transaction::rpc::{RpcError, RpcProviderName}; +use alloy_primitives::keccak256; use alloy::primitives::{aliases::U48, Address, Bytes, FixedBytes}; use alloy::sol_types::SolValue; @@ -28,7 +29,6 @@ use serde::{Deserialize, Serialize}; use world_chain_builder_pbh::external_nullifier::{ EncodedExternalNullifier, ExternalNullifier, }; -use world_chain_builder_pbh::payload::{PBHPayload as PbhPayload, Proof}; /// The default validity duration for 4337 `UserOperation` signatures. /// @@ -41,12 +41,6 @@ pub struct SerializableIdentity { pub trapdoor: Field, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct InclusionProof { - pub root: Field, - pub proof: semaphore_rs::poseidon_tree::Proof, -} - impl From<&Identity> for SerializableIdentity { fn from(identity: &Identity) -> Self { Self { @@ -181,7 +175,7 @@ pub trait Is4337Encodable { // // // PBH Logic if pbh { - let pbh_payload = Self::generate_pbh_proof(&user_operation).await; + let pbh_payload = generate_pbh_proof(&user_operation).await; full_signature.extend_from_slice( PBHPayload::from(pbh_payload.clone()).abi_encode().as_ref(), ); @@ -189,134 +183,19 @@ pub trait Is4337Encodable { user_operation.signature = full_signature.into(); - // println!("user_operation: {user_operation:?}"); - // 5. Submit UserOperation let user_op_hash: FixedBytes<32> = rpc_client - // Always send to standard 4337 entrypoint even for PBH - // The bundler will route it to the PBH entrypoint if it's a PBH transaction - .send_user_operation(network, &user_operation, *ENTRYPOINT_4337, provider) + // Always send to standard 4337 entrypoint even for PBH + // The bundler will route it to the PBH entrypoint if it's a PBH transaction + .send_user_operation(network, &user_operation, *ENTRYPOINT_4337, provider) .await?; Ok(user_op_hash) } - /// Generates a PBH proof for a given user operation. - async fn generate_pbh_proof(user_op: &UserOperation) -> PbhPayload { - // Convert from UserOperation to PackedUserOperation - // TODO: Fix this - let packed_user_op: PackedUserOperation = PackedUserOperation::from(user_op); - - let signal = Self::hash_user_op(&packed_user_op); - - let external_nullifier = ExternalNullifier::v1(8, 2025, 0); - - // TODO: Autotmatically find an unused one - let encoded_external_nullifier = - EncodedExternalNullifier::from(external_nullifier); - - - let secrets: String = std::fs::read_to_string("tests/sepolia_secrets.json").unwrap(); - - let secret: serde_json::Value = serde_json::from_str(&secrets).unwrap(); - let nullifier = secret["nullifier"].as_str().unwrap(); - let trapdoor = secret["trapdoor"].as_str().unwrap(); - - let identity = Identity { - nullifier: nullifier.parse().unwrap(), - trapdoor: trapdoor.parse().unwrap(), - }; - - let identities = vec![identity]; - - let proofs = - futures::future::try_join_all(identities.iter().map(|identity| async { - Self::fetch_inclusion_proof( - "https://signup-orb-ethereum.stage-crypto.worldcoin.dev", // Staging - identity, - ) - .await - })) - .await - .unwrap(); - - let identity = identities[0].clone(); - let inclusion_proof = proofs[0].clone(); - - // println!("inclusion_proof: {inclusion_proof:?}"); - - let proof: semaphore_rs_proof::Proof = semaphore_rs::protocol::generate_proof( - &identity, - &inclusion_proof.proof, - encoded_external_nullifier.0, - signal, - ) - .expect("Failed to generate semaphore proof"); + // async fn get_pbh_nonce() -> u64 { - // println!("proof: {proof:?}"); - - let nullifier_hash = semaphore_rs::protocol::generate_nullifier_hash( - &identity, - encoded_external_nullifier.0, - ); - - let proof = Proof(proof); - - PbhPayload { - external_nullifier, - nullifier_hash, - root: inclusion_proof.root, - proof, - } - } - - /// Fetches an inclusion proof for a given identity from the signup sequencer. - /// - /// This function sends a request to the sequencer to fetch the inclusion proof for a given identity. - /// - /// # Arguments - /// * `url` - The URL of the sequencer - /// * `identity` - The identity to fetch the proof for - async fn fetch_inclusion_proof( - url: &str, - identity: &Identity, - ) -> eyre::Result { - let client = Client::new(); - - let commitment = identity.commitment(); - let response = client - .post(format!("{}/inclusionProof", url)) - .json(&serde_json::json! {{ - "identityCommitment": commitment, - }}) - .send() - .await? - .error_for_status()?; - - let proof: InclusionProof = response.json().await?; - - Ok(proof) - } - - /// Computes a ZK-friendly hash of a PackedUserOperation. - /// - /// This function extracts key fields (sender, nonce, callData) from a PackedUserOperation, - /// encodes them using ABI packed encoding, and converts the result to a Field element - /// suitable for use in zero-knowledge proof circuits. - /// - /// # Arguments - /// * `user_op` - The PackedUserOperation to hash - /// - /// # Returns - /// A Field element representing the hash of the user operation - fn hash_user_op(user_op: &PackedUserOperation) -> Field { - let hash = SolValue::abi_encode_packed(&( - &user_op.sender, - &user_op.nonce, - &user_op.callData, - )); - hash_to_field(hash.as_slice()) - } + // } } #[cfg(test)] diff --git a/bedrock/src/transaction/rpc.rs b/bedrock/src/transaction/rpc.rs index dae86226..a51ac582 100644 --- a/bedrock/src/transaction/rpc.rs +++ b/bedrock/src/transaction/rpc.rs @@ -313,12 +313,12 @@ impl RpcClient { entrypoint: Address, provider: RpcProviderName, ) -> Result, RpcError> { - // TODO: Move this logic to the right place for serialization // Serialize user operation, removing factory and factory_data if factory is address(0) - let mut user_op_value = serde_json::to_value(user_operation).map_err(|_| RpcError::JsonError)?; - + let mut user_op_value = + serde_json::to_value(user_operation).map_err(|_| RpcError::JsonError)?; + if let Some(user_op_obj) = user_op_value.as_object_mut() { // Remove factory and factory_data if factory is address(0) if let Some(factory_value) = user_op_obj.get("factory") { @@ -327,21 +327,28 @@ impl RpcClient { user_op_obj.remove("factoryData"); } } - + // Remove paymaster fields if paymaster is address(0) or None if let Some(paymaster_value) = user_op_obj.get("paymaster") { - if paymaster_value == "0x0000000000000000000000000000000000000000" || paymaster_value.is_null() { + if paymaster_value == "0x0000000000000000000000000000000000000000" + || paymaster_value.is_null() + { user_op_obj.remove("paymaster"); user_op_obj.remove("paymasterData"); user_op_obj.remove("paymasterPostOpGasLimit"); user_op_obj.remove("paymasterVerificationGasLimit"); } } - + // Add aggregator field - user_op_obj.insert("aggregator".to_string(), serde_json::Value::String("0x8af27ee9af538c48c7d2a2c8bd6a40ef830e2489".to_string())); + user_op_obj.insert( + "aggregator".to_string(), + serde_json::Value::String( + "0x8af27ee9af538c48c7d2a2c8bd6a40ef830e2489".to_string(), + ), + ); } - + let params = vec![ user_op_value, serde_json::Value::String(format!("{entrypoint:?}")), diff --git a/bedrock/tests/test_smart_account_transfer_pbh.rs b/bedrock/tests/test_smart_account_transfer_pbh.rs index c2a1fca7..6c9b0733 100644 --- a/bedrock/tests/test_smart_account_transfer_pbh.rs +++ b/bedrock/tests/test_smart_account_transfer_pbh.rs @@ -11,13 +11,13 @@ use bedrock::{ primitives::{ http_client::{ set_http_client, AuthenticatedHttpClient, HttpError, HttpHeader, HttpMethod, - }, - Network, + }, world_id::set_world_id_identity, Network }, smart_account::SafeSmartAccount, transaction::RpcProviderName, }; +use semaphore_rs::identity::Identity; use serde::Serialize; use serde_json::json; @@ -73,7 +73,8 @@ where message: "invalid json".into(), })?; - let method = root.get("method") + let method = root + .get("method") .and_then(|m| m.as_str()) .ok_or(HttpError::Generic { message: "invalid json".into(), @@ -108,16 +109,21 @@ where } // Forward all other methods to the actual provider _ => { - println!("method: {method}"); println!("params: {params}"); // Forward the JSON-RPC request to the provider - let response = self.provider.raw_request::(method.into(), params).await + let response = self + .provider + .raw_request::( + method.into(), + params, + ) + .await .map_err(|e| HttpError::Generic { message: format!("Provider request failed: {}", e), })?; - + let resp = json!({ "jsonrpc": "2.0", "id": id, @@ -132,18 +138,16 @@ where // ------------------ The test for the full transaction_transfer flow ------------------ #[tokio::test] -async fn test_pbh_transaction_transfer_full_flow( -) -> anyhow::Result<()> { +async fn test_pbh_transaction_transfer_full_flow() -> anyhow::Result<()> { let secrets: String = std::fs::read_to_string("tests/sepolia_secrets.json")?; - let secret: serde_json::Value = serde_json::from_str(&secrets)?; + let nullifier = secret["nullifier"].as_str().unwrap(); + let trapdoor = secret["trapdoor"].as_str().unwrap(); let private_key = secret["private_key"].as_str().unwrap(); let safe_address = secret["safe_address"].as_str().unwrap(); let rpc_url: Url = secret["rpc_url"].as_str().unwrap().parse()?; - let owner_signer = PrivateKeySigner::from_slice( - &hex::decode(private_key)?, - )?; + let owner_signer = PrivateKeySigner::from_slice(&hex::decode(private_key)?)?; let owner_key_hex = hex::encode(owner_signer.to_bytes()); @@ -157,11 +161,17 @@ async fn test_pbh_transaction_transfer_full_flow( }; let _ = set_http_client(Arc::new(client)); + let identity = Identity { + nullifier: nullifier.parse().unwrap(), + trapdoor: trapdoor.parse().unwrap(), + }; + let _ = set_world_id_identity(Arc::new(identity)); + // 8) Execute high-level transfer via transaction_transfer let safe_account = SafeSmartAccount::new(owner_key_hex, &safe_address.to_string())?; let amount = "1"; let recipient = safe_address; - + let _user_op_hash = safe_account .transaction_transfer( Network::WorldChainSepolia, From ba0246803e896db4cd75aabeb7e895c80ff676a7 Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Tue, 2 Sep 2025 14:02:04 -0700 Subject: [PATCH 10/34] working again with world_id file --- bedrock/src/primitives/contracts.rs | 6 + bedrock/src/primitives/world_id.rs | 113 ++++++++++++++++-- bedrock/src/smart_account/transaction_4337.rs | 4 +- .../tests/test_smart_account_transfer_pbh.rs | 4 +- 4 files changed, 117 insertions(+), 10 deletions(-) diff --git a/bedrock/src/primitives/contracts.rs b/bedrock/src/primitives/contracts.rs index 5d8eb09a..dcfafa9f 100644 --- a/bedrock/src/primitives/contracts.rs +++ b/bedrock/src/primitives/contracts.rs @@ -454,6 +454,7 @@ sol! { } } + #[sol(rpc)] contract IEntryPoint { #[derive(Default, serde::Serialize, serde::Deserialize, Debug)] struct PackedUserOperation { @@ -476,6 +477,7 @@ sol! { } } + #[sol(rpc)] contract IPBHEntryPoint { #[derive(Default)] struct PBHPayload { @@ -494,6 +496,10 @@ sol! { IMulticall3.Call3[] calls, PBHPayload payload, ) external; + + function getFirstUnspentNullifierHash(uint256[] calldata hashes) external view returns (int256); + + function getUnspentNullifierHashes(uint256[] calldata hashes) external view returns (uint256[] memory); } } diff --git a/bedrock/src/primitives/world_id.rs b/bedrock/src/primitives/world_id.rs index a9b3ecf2..38c99dbb 100644 --- a/bedrock/src/primitives/world_id.rs +++ b/bedrock/src/primitives/world_id.rs @@ -1,5 +1,6 @@ use alloy::sol_types::SolValue; -use reqwest::Client; +use chrono::{Datelike, Utc}; +use reqwest::{Client, Url}; use semaphore_rs::identity::Identity; use semaphore_rs::{hash_to_field, Field}; use serde::{Deserialize, Serialize}; @@ -9,11 +10,26 @@ use world_chain_builder_pbh::{ payload::{PBHPayload as PbhPayload, Proof}, }; +use crate::primitives::contracts::{IPBHEntryPoint, PBH_ENTRYPOINT_4337}; +use crate::primitives::Network; +use crate::transaction::rpc::get_rpc_client; use crate::{ primitives::contracts::IEntryPoint::PackedUserOperation, smart_account::UserOperation, }; +use alloy::primitives::U256; +use alloy::providers::{Provider, ProviderBuilder}; + +const STAGING_SEQUENCER_URL: &str = + "https://signup-orb-ethereum.stage-crypto.worldcoin.dev"; +const PRODUCTION_SEQUENCER_URL: &str = + "https://signup-orb-ethereum.crypto.worldcoin.org"; + +const STAGING_MAX_NONCE: u16 = u16::MAX; +// TODO: UPDATE THIS ONCE SET IN PRODUCTION +// const PRODUCTION_MAX_NONCE: u16 = 9000; + /// Inclusion proof for a given identity. /// /// This struct contains the root of the Merkle tree and the proof for a given identity. @@ -71,23 +87,32 @@ pub fn is_world_id_identity_initialized() -> bool { } /// Generates a PBH proof for a given user operation. -pub async fn generate_pbh_proof(user_op: &UserOperation) -> PbhPayload { +pub async fn generate_pbh_proof( + user_op: &UserOperation, + network: Network, +) -> PbhPayload { // Convert from UserOperation to PackedUserOperation - // TODO: Fix this + // TODO: Clean this up let packed_user_op: PackedUserOperation = PackedUserOperation::from(user_op); let signal = hash_user_op(&packed_user_op); - let external_nullifier = ExternalNullifier::v1(8, 2025, 1); + // TODO: Fix me + let external_nullifier = + find_unused_nullifier_hash("https://worldchain-sepolia.gateway.tenderly.co") + .await + .unwrap(); - // TODO: Autotmatically find an unused one let encoded_external_nullifier = EncodedExternalNullifier::from(external_nullifier); let identity = get_world_id_identity().unwrap(); let inclusion_proof = fetch_inclusion_proof( - // TODO: Handle different envs - "https://signup-orb-ethereum.stage-crypto.worldcoin.dev", // Staging + match network { + Network::WorldChain => PRODUCTION_SEQUENCER_URL, + Network::WorldChainSepolia => STAGING_SEQUENCER_URL, + _ => panic!("Invalid network for World ID"), + }, &identity, ) .await @@ -116,6 +141,80 @@ pub async fn generate_pbh_proof(user_op: &UserOperation) -> PbhPayload { } } +/// Finds the first unused nullifier hash for the current World ID identity. +pub async fn find_unused_nullifier_hash( + provider_url: &str, +) -> Result> { + let identity = get_world_id_identity().unwrap(); + let now = Utc::now(); + let current_year = now.year() as u16; + let current_month = now.month() as u16; + + // TODO: get this from global + let provider: alloy::providers::fillers::FillProvider< + alloy::providers::fillers::JoinFill< + alloy::providers::Identity, + alloy::providers::fillers::JoinFill< + alloy::providers::fillers::GasFiller, + alloy::providers::fillers::JoinFill< + alloy::providers::fillers::BlobGasFiller, + alloy::providers::fillers::JoinFill< + alloy::providers::fillers::NonceFiller, + alloy::providers::fillers::ChainIdFiller, + >, + >, + >, + >, + alloy::providers::RootProvider, + > = ProviderBuilder::new().connect_http(Url::parse(provider_url).unwrap()); + let contract: IPBHEntryPoint::IPBHEntryPointInstance< + alloy::providers::fillers::FillProvider< + alloy::providers::fillers::JoinFill< + alloy::providers::Identity, + alloy::providers::fillers::JoinFill< + alloy::providers::fillers::GasFiller, + alloy::providers::fillers::JoinFill< + alloy::providers::fillers::BlobGasFiller, + alloy::providers::fillers::JoinFill< + alloy::providers::fillers::NonceFiller, + alloy::providers::fillers::ChainIdFiller, + >, + >, + >, + >, + alloy::providers::RootProvider, + >, + > = IPBHEntryPoint::new(*PBH_ENTRYPOINT_4337, provider); + + // TODO: Batching and env switch + for nonce in 0..STAGING_MAX_NONCE { + let external_nullifier = + ExternalNullifier::v1(current_month as u8, current_year, nonce as u16); + let encoded_external_nullifier = + EncodedExternalNullifier::from(external_nullifier.clone()); + + let nullifier_hash = semaphore_rs::protocol::generate_nullifier_hash( + &identity, + encoded_external_nullifier.0, + ); + + let vec = vec![U256::from_be_bytes(nullifier_hash.to_be_bytes::<32>())]; + + let first_unused_index = + contract.getFirstUnspentNullifierHash(vec).call().await?; + + if first_unused_index.as_i64() != -1 { + println!("Month: {:?}", current_month); + println!("Year: {:?}", current_year); + println!("Nonce: {:?}", nonce); + + return Ok(external_nullifier); + } + } + + return Err("No PBH transactions remaining".into()); +} + /// Fetches an inclusion proof for a given identity from the signup sequencer. /// /// This function sends a request to the sequencer to fetch the inclusion proof for a given identity. diff --git a/bedrock/src/smart_account/transaction_4337.rs b/bedrock/src/smart_account/transaction_4337.rs index a3b5729d..c9407158 100644 --- a/bedrock/src/smart_account/transaction_4337.rs +++ b/bedrock/src/smart_account/transaction_4337.rs @@ -173,9 +173,9 @@ pub trait Is4337Encodable { full_signature.extend_from_slice(valid_until_bytes); full_signature.extend_from_slice(&signature.as_bytes()[..]); - // // // PBH Logic + // PBH Logic if pbh { - let pbh_payload = generate_pbh_proof(&user_operation).await; + let pbh_payload = generate_pbh_proof(&user_operation, network).await; full_signature.extend_from_slice( PBHPayload::from(pbh_payload.clone()).abi_encode().as_ref(), ); diff --git a/bedrock/tests/test_smart_account_transfer_pbh.rs b/bedrock/tests/test_smart_account_transfer_pbh.rs index 6c9b0733..2e5ebaa2 100644 --- a/bedrock/tests/test_smart_account_transfer_pbh.rs +++ b/bedrock/tests/test_smart_account_transfer_pbh.rs @@ -11,7 +11,9 @@ use bedrock::{ primitives::{ http_client::{ set_http_client, AuthenticatedHttpClient, HttpError, HttpHeader, HttpMethod, - }, world_id::set_world_id_identity, Network + }, + world_id::set_world_id_identity, + Network, }, smart_account::SafeSmartAccount, transaction::RpcProviderName, From e686037311c3eb80407910fc200bb6b4b60d0bfe Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Tue, 2 Sep 2025 16:06:19 -0700 Subject: [PATCH 11/34] auto fetch pbh nonce --- Cargo.lock | 14 +++ bedrock/Cargo.toml | 5 + bedrock/src/primitives/http_client.rs | 5 + bedrock/src/primitives/world_id.rs | 128 +++++++++++++------------- bedrock/src/transaction/rpc.rs | 59 ++++++++++++ 5 files changed, 147 insertions(+), 64 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 005d82e8..13b67e4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,6 +49,8 @@ dependencies = [ "alloy-node-bindings", "alloy-provider", "alloy-rpc-client", + "alloy-rpc-types", + "alloy-serde", "alloy-signer", "alloy-signer-aws", "alloy-signer-gcp", @@ -468,6 +470,18 @@ dependencies = [ "wasmtimer", ] +[[package]] +name = "alloy-rpc-types" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d47b637369245d2dafef84b223b1ff5ea59e6cd3a98d2d3516e32788a0b216df" +dependencies = [ + "alloy-primitives 1.3.1", + "alloy-rpc-types-eth", + "alloy-serde", + "serde", +] + [[package]] name = "alloy-rpc-types-anvil" version = "1.0.23" diff --git a/bedrock/Cargo.toml b/bedrock/Cargo.toml index 9bd35db8..b19f70e3 100644 --- a/bedrock/Cargo.toml +++ b/bedrock/Cargo.toml @@ -27,6 +27,11 @@ alloy = { version = "1.0.23", default-features = false, features = [ "eip712", "signer-local", "sol-types", + "rpc", + "rpc-types", + "providers", + "network", + "contract", ] } alloy-primitives = "1.3.1" alloy-rlp = "0.3.12" diff --git a/bedrock/src/primitives/http_client.rs b/bedrock/src/primitives/http_client.rs index b970f45d..f33d9962 100644 --- a/bedrock/src/primitives/http_client.rs +++ b/bedrock/src/primitives/http_client.rs @@ -1,5 +1,10 @@ use std::sync::{Arc, OnceLock}; +use alloy::{ + network::Ethereum, + providers::{Provider, ProviderBuilder, RootProvider}, +}; + /// Global HTTP client instance for Bedrock operations static HTTP_CLIENT_INSTANCE: OnceLock> = OnceLock::new(); diff --git a/bedrock/src/primitives/world_id.rs b/bedrock/src/primitives/world_id.rs index 38c99dbb..aa1c525f 100644 --- a/bedrock/src/primitives/world_id.rs +++ b/bedrock/src/primitives/world_id.rs @@ -1,4 +1,7 @@ -use alloy::sol_types::SolValue; +use alloy::sol_types::{SolCall, SolValue}; + +use alloy::primitives::{Address, Bytes}; +use alloy_primitives::I256; use chrono::{Datelike, Utc}; use reqwest::{Client, Url}; use semaphore_rs::identity::Identity; @@ -28,7 +31,9 @@ const PRODUCTION_SEQUENCER_URL: &str = const STAGING_MAX_NONCE: u16 = u16::MAX; // TODO: UPDATE THIS ONCE SET IN PRODUCTION -// const PRODUCTION_MAX_NONCE: u16 = 9000; +const PRODUCTION_MAX_NONCE: u16 = 9000; + +const MAX_NONCE_BATCH_SIZE: u16 = 100; /// Inclusion proof for a given identity. /// @@ -97,11 +102,7 @@ pub async fn generate_pbh_proof( let signal = hash_user_op(&packed_user_op); - // TODO: Fix me - let external_nullifier = - find_unused_nullifier_hash("https://worldchain-sepolia.gateway.tenderly.co") - .await - .unwrap(); + let external_nullifier = find_unused_nullifier_hash(network).await.unwrap(); let encoded_external_nullifier = EncodedExternalNullifier::from(external_nullifier); @@ -143,72 +144,71 @@ pub async fn generate_pbh_proof( /// Finds the first unused nullifier hash for the current World ID identity. pub async fn find_unused_nullifier_hash( - provider_url: &str, + network: Network, ) -> Result> { let identity = get_world_id_identity().unwrap(); let now = Utc::now(); let current_year = now.year() as u16; let current_month = now.month() as u16; - // TODO: get this from global - let provider: alloy::providers::fillers::FillProvider< - alloy::providers::fillers::JoinFill< - alloy::providers::Identity, - alloy::providers::fillers::JoinFill< - alloy::providers::fillers::GasFiller, - alloy::providers::fillers::JoinFill< - alloy::providers::fillers::BlobGasFiller, - alloy::providers::fillers::JoinFill< - alloy::providers::fillers::NonceFiller, - alloy::providers::fillers::ChainIdFiller, - >, - >, - >, - >, - alloy::providers::RootProvider, - > = ProviderBuilder::new().connect_http(Url::parse(provider_url).unwrap()); - let contract: IPBHEntryPoint::IPBHEntryPointInstance< - alloy::providers::fillers::FillProvider< - alloy::providers::fillers::JoinFill< - alloy::providers::Identity, - alloy::providers::fillers::JoinFill< - alloy::providers::fillers::GasFiller, - alloy::providers::fillers::JoinFill< - alloy::providers::fillers::BlobGasFiller, - alloy::providers::fillers::JoinFill< - alloy::providers::fillers::NonceFiller, - alloy::providers::fillers::ChainIdFiller, - >, - >, - >, - >, - alloy::providers::RootProvider, - >, - > = IPBHEntryPoint::new(*PBH_ENTRYPOINT_4337, provider); - - // TODO: Batching and env switch - for nonce in 0..STAGING_MAX_NONCE { - let external_nullifier = - ExternalNullifier::v1(current_month as u8, current_year, nonce as u16); - let encoded_external_nullifier = - EncodedExternalNullifier::from(external_nullifier.clone()); - - let nullifier_hash = semaphore_rs::protocol::generate_nullifier_hash( - &identity, - encoded_external_nullifier.0, - ); - - let vec = vec![U256::from_be_bytes(nullifier_hash.to_be_bytes::<32>())]; - - let first_unused_index = - contract.getFirstUnspentNullifierHash(vec).call().await?; - - if first_unused_index.as_i64() != -1 { + let max_nonce = match network { + Network::WorldChain => PRODUCTION_MAX_NONCE, + Network::WorldChainSepolia => STAGING_MAX_NONCE, + _ => panic!("Invalid network for PBH"), + }; + + let rpc_client = get_rpc_client().unwrap(); + + // Process nonces in batches for efficiency + for batch_start in (0..max_nonce).step_by(MAX_NONCE_BATCH_SIZE as usize) { + let batch_end = std::cmp::min(batch_start + MAX_NONCE_BATCH_SIZE, max_nonce); + let mut batch_hashes = Vec::new(); + let mut batch_external_nullifiers = Vec::new(); + + // Generate a batch of nullifier hashes + for nonce in batch_start..batch_end { + let external_nullifier = + ExternalNullifier::v1(current_month as u8, current_year, nonce as u16); + let encoded_external_nullifier = + EncodedExternalNullifier::from(external_nullifier.clone()); + + let nullifier_hash = semaphore_rs::protocol::generate_nullifier_hash( + &identity, + encoded_external_nullifier.0, + ); + + batch_hashes.push(U256::from_be_bytes(nullifier_hash.to_be_bytes::<32>())); + batch_external_nullifiers.push(external_nullifier); + } + + // Call contract with batch of hashes + let call = IPBHEntryPoint::getFirstUnspentNullifierHashCall { + hashes: batch_hashes, + }; + + let result = rpc_client + .eth_call( + network, + *PBH_ENTRYPOINT_4337, + Bytes::from(call.abi_encode()), + ) + .await?; + + let unsigned_value = U256::from_be_slice(&result); + let signed_from_slice = I256::from_raw(unsigned_value); + + // If result is not -1, we found an unused nullifier hash + if signed_from_slice != I256::MINUS_ONE { + let index = unsigned_value.to::(); + let actual_nonce = batch_start + index as u16; + + println!("Found unused nullifier!"); println!("Month: {:?}", current_month); println!("Year: {:?}", current_year); - println!("Nonce: {:?}", nonce); + println!("Actual nonce: {:?}", actual_nonce); - return Ok(external_nullifier); + // Return the external nullifier for the found index + return Ok(batch_external_nullifiers[index].clone()); } } diff --git a/bedrock/src/transaction/rpc.rs b/bedrock/src/transaction/rpc.rs index a51ac582..b10d9ea7 100644 --- a/bedrock/src/transaction/rpc.rs +++ b/bedrock/src/transaction/rpc.rs @@ -14,6 +14,7 @@ use crate::{ use alloy::hex::FromHex; use alloy::primitives::{Address, Bytes, FixedBytes, U128, U256}; use serde::{Deserialize, Serialize}; +use serde_json::Map; use serde_json::Value; use std::sync::{Arc, OnceLock}; @@ -39,6 +40,9 @@ pub enum RpcMethod { /// Submit a signed `UserOperation` #[serde(rename = "eth_sendUserOperation")] SendUserOperation, + /// Make a generic RPC eth_call to read data from a contract + #[serde(rename = "eth_call")] + EthCall, } /// 4337 provider selection to be passed by native apps @@ -68,6 +72,7 @@ impl RpcMethod { match self { Self::SponsorUserOperation => "wa_sponsorUserOperation", Self::SendUserOperation => "eth_sendUserOperation", + Self::EthCall => "eth_call", } } } @@ -106,6 +111,7 @@ struct ErrorPayload { /// Errors that can occur when interacting with RPC operations. #[crate::bedrock_error] +#[derive(Debug, Deserialize)] pub enum RpcError { /// HTTP request failed #[error("HTTP request failed: {0}")] @@ -189,6 +195,8 @@ impl RpcClient { format!("/v1/rpc/{}", network.network_name()) } + // async fn call_pbh_contract + /// Makes a generic RPC call with typed parameters and result, adding provider header /// /// # Arguments @@ -362,6 +370,57 @@ impl RpcClient { message: format!("Invalid userOpHash format: {e}"), }) } + + /// Makes a generic RPC call with typed parameters and result, adding provider header + /// + /// # Arguments + /// - `network`: target network + /// - `method`: JSON-RPC method to invoke + /// - `params`: JSON-RPC params (typed) + /// - `provider`: selected 4337 provider to include in headers + pub async fn eth_call( + &self, + network: Network, + to: Address, + data: Bytes, + ) -> Result { + // var raw = JSON.stringify({ + // "method": "eth_call", + // "params": [ + // { + // "to": "0x6b175474e89094c44da98b954eedeac495271d0f", + // "data": "0x70a082310000000000000000000000006E0d01A76C3Cf4288372a29124A26D4353EE51BE" + // }, + // "latest", Block number or "latest" + // }) + + let params = vec![ + serde_json::Value::Object(Map::from_iter([ + ( + "to".to_string(), + serde_json::Value::String(format!("{to:?}")), + ), + ( + "data".to_string(), + serde_json::Value::String(format!("{data:?}")), + ), + ])), + serde_json::Value::String("latest".to_string()), + ]; + + let result: String = self + .rpc_call( + network, + RpcMethod::EthCall, + params, + RpcProviderName::Alchemy, + ) + .await?; + + Bytes::from_hex(&result).map_err(|e| RpcError::InvalidResponse { + message: format!("Invalid eth_call result format: {e}"), + }) + } } /// Gets the global RPC client, initializing it on first access. From c9c69d0408b7be8ad97dadfb0297ce51bd10982c Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Tue, 2 Sep 2025 16:07:09 -0700 Subject: [PATCH 12/34] cleanup --- bedrock/src/primitives/contracts.rs | 12 ++++---- bedrock/src/primitives/http_client.rs | 4 --- bedrock/src/primitives/world_id.rs | 28 +++++++++---------- bedrock/src/smart_account/transaction_4337.rs | 15 +++------- bedrock/src/transaction/rpc.rs | 2 +- .../tests/test_smart_account_transfer_pbh.rs | 8 +++--- 6 files changed, 29 insertions(+), 40 deletions(-) diff --git a/bedrock/src/primitives/contracts.rs b/bedrock/src/primitives/contracts.rs index dcfafa9f..215f7871 100644 --- a/bedrock/src/primitives/contracts.rs +++ b/bedrock/src/primitives/contracts.rs @@ -8,7 +8,7 @@ use alloy::sol_types::SolValue; use ruint::aliases::U256; use std::{str::FromStr, sync::LazyLock}; use world_chain_builder_pbh::external_nullifier::EncodedExternalNullifier; -use world_chain_builder_pbh::payload::{PBHPayload as PbhPayload, TREE_DEPTH}; +use world_chain_builder_pbh::payload::PBHPayload as PbhPayload; /// static SAFE_OP_TYPEHASH: LazyLock> = LazyLock::new(|| { @@ -25,7 +25,7 @@ pub static ENTRYPOINT_4337: LazyLock
= LazyLock::new(|| { .expect("failed to decode ENTRYPOINT_4337") }); -/// Multichain address for PBH_ENTRYPOINT_4337 +/// Multichain address for `PBH_ENTRYPOINT_4337` /// Contract reference: pub static PBH_ENTRYPOINT_4337: LazyLock
= LazyLock::new(|| { Address::from_str("0x0000000000A21818Ee9F93BB4f2AAad305b5397C") @@ -340,7 +340,7 @@ impl EncodedSafeOpStruct { impl From for EncodedSafeOpStruct { /// Converts a `UserOperation` into an `EncodedSafeOpStruct`. /// - /// This implementation extracts validity timestamps from the UserOperation's signature. + /// This implementation extracts validity timestamps from the `UserOperation`'s signature. /// If the signature doesn't contain valid timestamps, it uses zero values as defaults. /// /// # Example @@ -366,8 +366,8 @@ impl From for IEntryPoint::PackedUserOperation { /// Converts a `UserOperation` into a `PackedUserOperation`. /// /// This conversion packs gas limits and fees into bytes32 fields as required by EIP-4337. - /// - `accountGasLimits`: verification_gas_limit (upper 128 bits) + call_gas_limit (lower 128 bits) - /// - `gasFees`: max_priority_fee_per_gas (upper 128 bits) + max_fee_per_gas (lower 128 bits) + /// - `accountGasLimits`: `verification_gas_limit` (upper 128 bits) + `call_gas_limit` (lower 128 bits) + /// - `gasFees`: `max_priority_fee_per_gas` (upper 128 bits) + `max_fee_per_gas` (lower 128 bits) /// /// # Example /// ```rust @@ -407,7 +407,7 @@ impl From<&UserOperation> for IEntryPoint::PackedUserOperation { /// Converts a `&UserOperation` into a `PackedUserOperation`. /// /// This conversion packs gas limits and fees into bytes32 fields as required by EIP-4337. - /// This implementation works with borrowed UserOperations to avoid unnecessary moves. + /// This implementation works with borrowed `UserOperations` to avoid unnecessary moves. /// /// # Example /// ```rust diff --git a/bedrock/src/primitives/http_client.rs b/bedrock/src/primitives/http_client.rs index f33d9962..2dae5b78 100644 --- a/bedrock/src/primitives/http_client.rs +++ b/bedrock/src/primitives/http_client.rs @@ -1,9 +1,5 @@ use std::sync::{Arc, OnceLock}; -use alloy::{ - network::Ethereum, - providers::{Provider, ProviderBuilder, RootProvider}, -}; /// Global HTTP client instance for Bedrock operations static HTTP_CLIENT_INSTANCE: OnceLock> = diff --git a/bedrock/src/primitives/world_id.rs b/bedrock/src/primitives/world_id.rs index aa1c525f..57840ab8 100644 --- a/bedrock/src/primitives/world_id.rs +++ b/bedrock/src/primitives/world_id.rs @@ -1,9 +1,9 @@ use alloy::sol_types::{SolCall, SolValue}; -use alloy::primitives::{Address, Bytes}; +use alloy::primitives::Bytes; use alloy_primitives::I256; use chrono::{Datelike, Utc}; -use reqwest::{Client, Url}; +use reqwest::Client; use semaphore_rs::identity::Identity; use semaphore_rs::{hash_to_field, Field}; use serde::{Deserialize, Serialize}; @@ -22,7 +22,7 @@ use crate::{ }; use alloy::primitives::U256; -use alloy::providers::{Provider, ProviderBuilder}; +use alloy::providers::Provider; const STAGING_SEQUENCER_URL: &str = "https://signup-orb-ethereum.stage-crypto.worldcoin.dev"; @@ -168,9 +168,9 @@ pub async fn find_unused_nullifier_hash( // Generate a batch of nullifier hashes for nonce in batch_start..batch_end { let external_nullifier = - ExternalNullifier::v1(current_month as u8, current_year, nonce as u16); + ExternalNullifier::v1(current_month as u8, current_year, nonce); let encoded_external_nullifier = - EncodedExternalNullifier::from(external_nullifier.clone()); + EncodedExternalNullifier::from(external_nullifier); let nullifier_hash = semaphore_rs::protocol::generate_nullifier_hash( &identity, @@ -203,16 +203,16 @@ pub async fn find_unused_nullifier_hash( let actual_nonce = batch_start + index as u16; println!("Found unused nullifier!"); - println!("Month: {:?}", current_month); - println!("Year: {:?}", current_year); - println!("Actual nonce: {:?}", actual_nonce); + println!("Month: {current_month:?}"); + println!("Year: {current_year:?}"); + println!("Actual nonce: {actual_nonce:?}"); // Return the external nullifier for the found index - return Ok(batch_external_nullifiers[index].clone()); + return Ok(batch_external_nullifiers[index]); } } - return Err("No PBH transactions remaining".into()); + Err("No PBH transactions remaining".into()) } /// Fetches an inclusion proof for a given identity from the signup sequencer. @@ -230,7 +230,7 @@ pub async fn fetch_inclusion_proof( let commitment = identity.commitment(); let response = client - .post(format!("{}/inclusionProof", url)) + .post(format!("{url}/inclusionProof")) .json(&serde_json::json! {{ "identityCommitment": commitment, }}) @@ -243,14 +243,14 @@ pub async fn fetch_inclusion_proof( Ok(proof) } -/// Computes a ZK-friendly hash of a PackedUserOperation. +/// Computes a ZK-friendly hash of a `PackedUserOperation`. /// -/// This function extracts key fields (sender, nonce, callData) from a PackedUserOperation, +/// This function extracts key fields (sender, nonce, callData) from a `PackedUserOperation`, /// encodes them using ABI packed encoding, and converts the result to a Field element /// suitable for use in zero-knowledge proof circuits. /// /// # Arguments -/// * `user_op` - The PackedUserOperation to hash +/// * `user_op` - The `PackedUserOperation` to hash /// /// # Returns /// A Field element representing the hash of the user operation diff --git a/bedrock/src/smart_account/transaction_4337.rs b/bedrock/src/smart_account/transaction_4337.rs index c9407158..71b205ed 100644 --- a/bedrock/src/smart_account/transaction_4337.rs +++ b/bedrock/src/smart_account/transaction_4337.rs @@ -4,8 +4,8 @@ //! use crate::primitives::contracts::{ - EncodedSafeOpStruct, IEntryPoint::PackedUserOperation, IPBHEntryPoint::PBHPayload, - UserOperation, ENTRYPOINT_4337, GNOSIS_SAFE_4337_MODULE, + EncodedSafeOpStruct, IPBHEntryPoint::PBHPayload, + UserOperation, ENTRYPOINT_4337, }; use crate::primitives::contracts::{PBH_ENTRYPOINT_4337, PBH_SAFE_4337_MODULE_SEPOLIA}; @@ -15,20 +15,13 @@ use crate::smart_account::SafeSmartAccountSigner; use crate::primitives::world_id::generate_pbh_proof; use crate::transaction::rpc::{RpcError, RpcProviderName}; -use alloy_primitives::keccak256; use alloy::primitives::{aliases::U48, Address, Bytes, FixedBytes}; use alloy::sol_types::SolValue; use chrono::{Duration, Utc}; -use eyre; -use futures::future; -use reqwest::Client; use semaphore_rs::identity::Identity; -use semaphore_rs::{hash_to_field, Field}; +use semaphore_rs::Field; use serde::{Deserialize, Serialize}; -use world_chain_builder_pbh::external_nullifier::{ - EncodedExternalNullifier, ExternalNullifier, -}; /// The default validity duration for 4337 `UserOperation` signatures. /// @@ -177,7 +170,7 @@ pub trait Is4337Encodable { if pbh { let pbh_payload = generate_pbh_proof(&user_operation, network).await; full_signature.extend_from_slice( - PBHPayload::from(pbh_payload.clone()).abi_encode().as_ref(), + PBHPayload::from(pbh_payload).abi_encode().as_ref(), ); } diff --git a/bedrock/src/transaction/rpc.rs b/bedrock/src/transaction/rpc.rs index b10d9ea7..99613b69 100644 --- a/bedrock/src/transaction/rpc.rs +++ b/bedrock/src/transaction/rpc.rs @@ -40,7 +40,7 @@ pub enum RpcMethod { /// Submit a signed `UserOperation` #[serde(rename = "eth_sendUserOperation")] SendUserOperation, - /// Make a generic RPC eth_call to read data from a contract + /// Make a generic RPC `eth_call` to read data from a contract #[serde(rename = "eth_call")] EthCall, } diff --git a/bedrock/tests/test_smart_account_transfer_pbh.rs b/bedrock/tests/test_smart_account_transfer_pbh.rs index 2e5ebaa2..50fb87af 100644 --- a/bedrock/tests/test_smart_account_transfer_pbh.rs +++ b/bedrock/tests/test_smart_account_transfer_pbh.rs @@ -123,7 +123,7 @@ where ) .await .map_err(|e| HttpError::Generic { - message: format!("Provider request failed: {}", e), + message: format!("Provider request failed: {e}"), })?; let resp = json!({ @@ -170,15 +170,15 @@ async fn test_pbh_transaction_transfer_full_flow() -> anyhow::Result<()> { let _ = set_world_id_identity(Arc::new(identity)); // 8) Execute high-level transfer via transaction_transfer - let safe_account = SafeSmartAccount::new(owner_key_hex, &safe_address.to_string())?; + let safe_account = SafeSmartAccount::new(owner_key_hex, safe_address)?; let amount = "1"; let recipient = safe_address; let _user_op_hash = safe_account .transaction_transfer( Network::WorldChainSepolia, - &"0xC82Ea35634BcE95C394B6BC00626f827bB0F4801".to_string(), // WORLD SEPOLIA LINK TOKEN - &recipient.to_string(), + "0xC82Ea35634BcE95C394B6BC00626f827bB0F4801", // WORLD SEPOLIA LINK TOKEN + recipient, amount, true, RpcProviderName::Alchemy, From 404359b44270599bc159d19e224fdb00a5f54546 Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Tue, 2 Sep 2025 16:08:53 -0700 Subject: [PATCH 13/34] cleanup --- bedrock/src/primitives/contracts.rs | 41 ------------------- bedrock/src/primitives/world_id.rs | 14 ++----- bedrock/src/smart_account/transaction_4337.rs | 2 +- 3 files changed, 4 insertions(+), 53 deletions(-) diff --git a/bedrock/src/primitives/contracts.rs b/bedrock/src/primitives/contracts.rs index 215f7871..94164ad1 100644 --- a/bedrock/src/primitives/contracts.rs +++ b/bedrock/src/primitives/contracts.rs @@ -403,47 +403,6 @@ impl From for IEntryPoint::PackedUserOperation { } } -impl From<&UserOperation> for IEntryPoint::PackedUserOperation { - /// Converts a `&UserOperation` into a `PackedUserOperation`. - /// - /// This conversion packs gas limits and fees into bytes32 fields as required by EIP-4337. - /// This implementation works with borrowed `UserOperations` to avoid unnecessary moves. - /// - /// # Example - /// ```rust - /// use bedrock::primitives::contracts::{UserOperation, IEntryPoint::PackedUserOperation}; - /// - /// let user_op = UserOperation::default(); - /// let packed_user_op: PackedUserOperation = (&user_op).into(); - /// // user_op is still available for use - /// ``` - fn from(user_op: &UserOperation) -> Self { - // Pack verification_gas_limit (upper 128 bits) + call_gas_limit (lower 128 bits) into accountGasLimits - let verification_gas_u256 = U256::from(user_op.verification_gas_limit); - let call_gas_u256 = U256::from(user_op.call_gas_limit); - let account_gas_limits: U256 = (verification_gas_u256 << 128) | call_gas_u256; - - // Pack max_priority_fee_per_gas (upper 128 bits) + max_fee_per_gas (lower 128 bits) into gasFees - let max_priority_fee_u256 = U256::from(user_op.max_priority_fee_per_gas); - let max_fee_u256 = U256::from(user_op.max_fee_per_gas); - let gas_fees: U256 = (max_priority_fee_u256 << 128) | max_fee_u256; - - Self { - sender: user_op.sender, - nonce: user_op.nonce, - initCode: user_op.get_init_code(), - callData: user_op.call_data.clone(), - accountGasLimits: FixedBytes::from_slice( - &account_gas_limits.to_be_bytes::<32>(), - ), - preVerificationGas: user_op.pre_verification_gas, - gasFees: FixedBytes::from_slice(&gas_fees.to_be_bytes::<32>()), - paymasterAndData: user_op.get_paymaster_and_data(), - signature: user_op.signature.clone(), - } - } -} - sol! { contract IMulticall3 { #[derive(Default)] diff --git a/bedrock/src/primitives/world_id.rs b/bedrock/src/primitives/world_id.rs index 57840ab8..12fbd35b 100644 --- a/bedrock/src/primitives/world_id.rs +++ b/bedrock/src/primitives/world_id.rs @@ -65,13 +65,11 @@ static WORLD_ID_IDENTITY_INSTANCE: OnceLock> = OnceLock::new(); /// /// # Errors /// This function will return false if the World ID identity is already initialized. -pub fn set_world_id_identity(identity: Arc) -> bool { +pub fn set_world_id_identity(identity: Arc) { if WORLD_ID_IDENTITY_INSTANCE.set(identity).is_err() { crate::warn!("World ID identity already initialized, ignoring"); - false } else { crate::info!("World ID identity initialized successfully"); - true } } @@ -93,19 +91,13 @@ pub fn is_world_id_identity_initialized() -> bool { /// Generates a PBH proof for a given user operation. pub async fn generate_pbh_proof( - user_op: &UserOperation, + user_op: UserOperation, network: Network, ) -> PbhPayload { - // Convert from UserOperation to PackedUserOperation - // TODO: Clean this up let packed_user_op: PackedUserOperation = PackedUserOperation::from(user_op); - let signal = hash_user_op(&packed_user_op); - let external_nullifier = find_unused_nullifier_hash(network).await.unwrap(); - let encoded_external_nullifier = EncodedExternalNullifier::from(external_nullifier); - let identity = get_world_id_identity().unwrap(); let inclusion_proof = fetch_inclusion_proof( @@ -142,7 +134,7 @@ pub async fn generate_pbh_proof( } } -/// Finds the first unused nullifier hash for the current World ID identity. +/// Finds the first unused nullifier hash for the current World ID identity in batches pub async fn find_unused_nullifier_hash( network: Network, ) -> Result> { diff --git a/bedrock/src/smart_account/transaction_4337.rs b/bedrock/src/smart_account/transaction_4337.rs index 71b205ed..863f2a37 100644 --- a/bedrock/src/smart_account/transaction_4337.rs +++ b/bedrock/src/smart_account/transaction_4337.rs @@ -168,7 +168,7 @@ pub trait Is4337Encodable { // PBH Logic if pbh { - let pbh_payload = generate_pbh_proof(&user_operation, network).await; + let pbh_payload = generate_pbh_proof(user_operation, network).await; full_signature.extend_from_slice( PBHPayload::from(pbh_payload).abi_encode().as_ref(), ); From 4566288ae4a0c86271149028988843d9189d6078 Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Tue, 2 Sep 2025 16:10:18 -0700 Subject: [PATCH 14/34] rename test --- .gitignore | 2 -- ..._smart_account_transfer_pbh.rs => test_smart_pbh_sepolia.rs} | 0 2 files changed, 2 deletions(-) rename bedrock/tests/{test_smart_account_transfer_pbh.rs => test_smart_pbh_sepolia.rs} (100%) diff --git a/.gitignore b/.gitignore index c0ad5e46..e1844ec9 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,4 @@ kotlin/bedrock-tests/bin/ # foundry out/ -load_test_identities.json -test_identities.json sepolia_secrets.json \ No newline at end of file diff --git a/bedrock/tests/test_smart_account_transfer_pbh.rs b/bedrock/tests/test_smart_pbh_sepolia.rs similarity index 100% rename from bedrock/tests/test_smart_account_transfer_pbh.rs rename to bedrock/tests/test_smart_pbh_sepolia.rs From fee360b057262a4d3c9183486f1e1c8c3ff886e0 Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Tue, 2 Sep 2025 16:13:59 -0700 Subject: [PATCH 15/34] fix domain_separator --- bedrock/src/primitives/contracts.rs | 2 +- bedrock/src/primitives/http_client.rs | 1 - bedrock/src/smart_account/transaction_4337.rs | 33 +++++++++++++------ 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/bedrock/src/primitives/contracts.rs b/bedrock/src/primitives/contracts.rs index 94164ad1..7532653c 100644 --- a/bedrock/src/primitives/contracts.rs +++ b/bedrock/src/primitives/contracts.rs @@ -25,7 +25,7 @@ pub static ENTRYPOINT_4337: LazyLock
= LazyLock::new(|| { .expect("failed to decode ENTRYPOINT_4337") }); -/// Multichain address for `PBH_ENTRYPOINT_4337` +/// Address for `PBH_ENTRYPOINT_4337` - same on sepolia and mainnet /// Contract reference: pub static PBH_ENTRYPOINT_4337: LazyLock
= LazyLock::new(|| { Address::from_str("0x0000000000A21818Ee9F93BB4f2AAad305b5397C") diff --git a/bedrock/src/primitives/http_client.rs b/bedrock/src/primitives/http_client.rs index 2dae5b78..b970f45d 100644 --- a/bedrock/src/primitives/http_client.rs +++ b/bedrock/src/primitives/http_client.rs @@ -1,6 +1,5 @@ use std::sync::{Arc, OnceLock}; - /// Global HTTP client instance for Bedrock operations static HTTP_CLIENT_INSTANCE: OnceLock> = OnceLock::new(); diff --git a/bedrock/src/smart_account/transaction_4337.rs b/bedrock/src/smart_account/transaction_4337.rs index 863f2a37..0e91c74a 100644 --- a/bedrock/src/smart_account/transaction_4337.rs +++ b/bedrock/src/smart_account/transaction_4337.rs @@ -4,13 +4,14 @@ //! use crate::primitives::contracts::{ - EncodedSafeOpStruct, IPBHEntryPoint::PBHPayload, - UserOperation, ENTRYPOINT_4337, + EncodedSafeOpStruct, IPBHEntryPoint::PBHPayload, UserOperation, ENTRYPOINT_4337, }; -use crate::primitives::contracts::{PBH_ENTRYPOINT_4337, PBH_SAFE_4337_MODULE_SEPOLIA}; +use crate::primitives::contracts::{ + PBH_ENTRYPOINT_4337, PBH_SAFE_4337_MODULE_MAINNET, PBH_SAFE_4337_MODULE_SEPOLIA, +}; use crate::primitives::{Network, PrimitiveError}; -use crate::smart_account::SafeSmartAccountSigner; +use crate::smart_account::{SafeSmartAccountSigner, GNOSIS_SAFE_4337_MODULE}; use crate::primitives::world_id::generate_pbh_proof; @@ -153,11 +154,24 @@ pub trait Is4337Encodable { valid_until_u48, )?; + let domain_separator: Address; + + if pbh { + match network { + Network::WorldChain => domain_separator = *PBH_SAFE_4337_MODULE_MAINNET, + Network::WorldChainSepolia => { + domain_separator = *PBH_SAFE_4337_MODULE_SEPOLIA + } + _ => panic!("Invalid network for PBH"), + } + } else { + domain_separator = *GNOSIS_SAFE_4337_MODULE; + } + let signature = safe_account.sign_digest( encoded_safe_op.into_transaction_hash(), network as u32, - // Some(*GNOSIS_SAFE_4337_MODULE), - Some(*PBH_SAFE_4337_MODULE_SEPOLIA), + Some(domain_separator), )?; // Compose the final signature once (timestamps + actual 65-byte signature) @@ -168,10 +182,9 @@ pub trait Is4337Encodable { // PBH Logic if pbh { - let pbh_payload = generate_pbh_proof(user_operation, network).await; - full_signature.extend_from_slice( - PBHPayload::from(pbh_payload).abi_encode().as_ref(), - ); + let pbh_payload = generate_pbh_proof(user_operation.clone(), network).await; + full_signature + .extend_from_slice(PBHPayload::from(pbh_payload).abi_encode().as_ref()); } user_operation.signature = full_signature.into(); From d14d427f5828f14f61c43e8b8ad4b30c6c172df2 Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Tue, 2 Sep 2025 16:15:20 -0700 Subject: [PATCH 16/34] remove fields from UserOperation::new_with_defaults --- bedrock/src/primitives/contracts.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/bedrock/src/primitives/contracts.rs b/bedrock/src/primitives/contracts.rs index 7532653c..57364402 100644 --- a/bedrock/src/primitives/contracts.rs +++ b/bedrock/src/primitives/contracts.rs @@ -175,13 +175,6 @@ impl UserOperation { nonce, call_data, signature: vec![0xff; USER_OPERATION_SIGNATURE_LENGTH].into(), - factory: Address::ZERO, - factory_data: Bytes::new(), - // call_gas_limit: U128::MAX, - // verification_gas_limit: U128::MAX, - // pre_verification_gas: U256::MAX, - // max_priority_fee_per_gas: U128::MAX, - // max_fee_per_gas: U128::MAX, ..Default::default() } } From 90b72b3f14ec6a100e651b61704ec5f6662b5689 Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Tue, 2 Sep 2025 16:17:23 -0700 Subject: [PATCH 17/34] rename world-chain-builder pbhPayload as RustPBHPayload --- bedrock/src/primitives/contracts.rs | 6 +++--- bedrock/src/primitives/world_id.rs | 7 +++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/bedrock/src/primitives/contracts.rs b/bedrock/src/primitives/contracts.rs index 57364402..5a9d58a3 100644 --- a/bedrock/src/primitives/contracts.rs +++ b/bedrock/src/primitives/contracts.rs @@ -8,7 +8,7 @@ use alloy::sol_types::SolValue; use ruint::aliases::U256; use std::{str::FromStr, sync::LazyLock}; use world_chain_builder_pbh::external_nullifier::EncodedExternalNullifier; -use world_chain_builder_pbh::payload::PBHPayload as PbhPayload; +use world_chain_builder_pbh::payload::PBHPayload as RustPBHPayload; /// static SAFE_OP_TYPEHASH: LazyLock> = LazyLock::new(|| { @@ -455,8 +455,8 @@ sol! { } } -impl From for PBHPayload { - fn from(val: PbhPayload) -> Self { +impl From for PBHPayload { + fn from(val: RustPBHPayload) -> Self { let p0 = val.proof.0 .0 .0; let p1 = val.proof.0 .0 .1; let p2 = val.proof.0 .1 .0[0]; diff --git a/bedrock/src/primitives/world_id.rs b/bedrock/src/primitives/world_id.rs index 12fbd35b..6c13de37 100644 --- a/bedrock/src/primitives/world_id.rs +++ b/bedrock/src/primitives/world_id.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; use std::sync::{Arc, OnceLock}; use world_chain_builder_pbh::{ external_nullifier::{EncodedExternalNullifier, ExternalNullifier}, - payload::{PBHPayload as PbhPayload, Proof}, + payload::{PBHPayload as RustPBHPayload, Proof}, }; use crate::primitives::contracts::{IPBHEntryPoint, PBH_ENTRYPOINT_4337}; @@ -22,7 +22,6 @@ use crate::{ }; use alloy::primitives::U256; -use alloy::providers::Provider; const STAGING_SEQUENCER_URL: &str = "https://signup-orb-ethereum.stage-crypto.worldcoin.dev"; @@ -93,7 +92,7 @@ pub fn is_world_id_identity_initialized() -> bool { pub async fn generate_pbh_proof( user_op: UserOperation, network: Network, -) -> PbhPayload { +) -> RustPBHPayload { let packed_user_op: PackedUserOperation = PackedUserOperation::from(user_op); let signal = hash_user_op(&packed_user_op); let external_nullifier = find_unused_nullifier_hash(network).await.unwrap(); @@ -126,7 +125,7 @@ pub async fn generate_pbh_proof( let proof = Proof(proof); - PbhPayload { + RustPBHPayload { external_nullifier, nullifier_hash, root: inclusion_proof.root, From d0b6984d8de4712de4dbaca8df4129bcc939613a Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Tue, 2 Sep 2025 16:27:55 -0700 Subject: [PATCH 18/34] cleanup --- bedrock/src/primitives/contracts.rs | 6 ++--- bedrock/src/primitives/world_id.rs | 25 +++++++++++-------- bedrock/src/smart_account/transaction_4337.rs | 18 ------------- 3 files changed, 17 insertions(+), 32 deletions(-) diff --git a/bedrock/src/primitives/contracts.rs b/bedrock/src/primitives/contracts.rs index 5a9d58a3..fd962da4 100644 --- a/bedrock/src/primitives/contracts.rs +++ b/bedrock/src/primitives/contracts.rs @@ -8,7 +8,7 @@ use alloy::sol_types::SolValue; use ruint::aliases::U256; use std::{str::FromStr, sync::LazyLock}; use world_chain_builder_pbh::external_nullifier::EncodedExternalNullifier; -use world_chain_builder_pbh::payload::PBHPayload as RustPBHPayload; +use world_chain_builder_pbh::payload::PBHPayload as WorldchainBuilderPBHPayload; /// static SAFE_OP_TYPEHASH: LazyLock> = LazyLock::new(|| { @@ -455,8 +455,8 @@ sol! { } } -impl From for PBHPayload { - fn from(val: RustPBHPayload) -> Self { +impl From for PBHPayload { + fn from(val: WorldchainBuilderPBHPayload) -> Self { let p0 = val.proof.0 .0 .0; let p1 = val.proof.0 .0 .1; let p2 = val.proof.0 .1 .0[0]; diff --git a/bedrock/src/primitives/world_id.rs b/bedrock/src/primitives/world_id.rs index 6c13de37..2fec343b 100644 --- a/bedrock/src/primitives/world_id.rs +++ b/bedrock/src/primitives/world_id.rs @@ -5,12 +5,17 @@ use alloy_primitives::I256; use chrono::{Datelike, Utc}; use reqwest::Client; use semaphore_rs::identity::Identity; +use semaphore_rs::poseidon_tree::Proof as PoseidonTreeProof; +use semaphore_rs::protocol::{generate_nullifier_hash, generate_proof}; use semaphore_rs::{hash_to_field, Field}; +use semaphore_rs_proof::Proof as SemaphoreProof; use serde::{Deserialize, Serialize}; use std::sync::{Arc, OnceLock}; use world_chain_builder_pbh::{ external_nullifier::{EncodedExternalNullifier, ExternalNullifier}, - payload::{PBHPayload as RustPBHPayload, Proof}, + payload::{ + PBHPayload as WorldchainBuilderPBHPayload, Proof as WorldchainBuilderProof, + }, }; use crate::primitives::contracts::{IPBHEntryPoint, PBH_ENTRYPOINT_4337}; @@ -46,7 +51,7 @@ pub struct InclusionProof { /// The root of the Merkle tree pub root: Field, /// The proof for the given identity - pub proof: semaphore_rs::poseidon_tree::Proof, + pub proof: PoseidonTreeProof, } /// Global World ID identity instance for Bedrock operations @@ -92,7 +97,7 @@ pub fn is_world_id_identity_initialized() -> bool { pub async fn generate_pbh_proof( user_op: UserOperation, network: Network, -) -> RustPBHPayload { +) -> WorldchainBuilderPBHPayload { let packed_user_op: PackedUserOperation = PackedUserOperation::from(user_op); let signal = hash_user_op(&packed_user_op); let external_nullifier = find_unused_nullifier_hash(network).await.unwrap(); @@ -110,7 +115,7 @@ pub async fn generate_pbh_proof( .await .unwrap(); - let proof: semaphore_rs_proof::Proof = semaphore_rs::protocol::generate_proof( + let proof: SemaphoreProof = generate_proof( &identity, &inclusion_proof.proof, encoded_external_nullifier.0, @@ -118,14 +123,12 @@ pub async fn generate_pbh_proof( ) .expect("Failed to generate semaphore proof"); - let nullifier_hash = semaphore_rs::protocol::generate_nullifier_hash( - &identity, - encoded_external_nullifier.0, - ); + let nullifier_hash = + generate_nullifier_hash(&identity, encoded_external_nullifier.0); - let proof = Proof(proof); + let proof = WorldchainBuilderProof(proof); - RustPBHPayload { + WorldchainBuilderPBHPayload { external_nullifier, nullifier_hash, root: inclusion_proof.root, @@ -163,7 +166,7 @@ pub async fn find_unused_nullifier_hash( let encoded_external_nullifier = EncodedExternalNullifier::from(external_nullifier); - let nullifier_hash = semaphore_rs::protocol::generate_nullifier_hash( + let nullifier_hash = generate_nullifier_hash( &identity, encoded_external_nullifier.0, ); diff --git a/bedrock/src/smart_account/transaction_4337.rs b/bedrock/src/smart_account/transaction_4337.rs index 0e91c74a..04f48396 100644 --- a/bedrock/src/smart_account/transaction_4337.rs +++ b/bedrock/src/smart_account/transaction_4337.rs @@ -20,30 +20,12 @@ use crate::transaction::rpc::{RpcError, RpcProviderName}; use alloy::primitives::{aliases::U48, Address, Bytes, FixedBytes}; use alloy::sol_types::SolValue; use chrono::{Duration, Utc}; -use semaphore_rs::identity::Identity; -use semaphore_rs::Field; -use serde::{Deserialize, Serialize}; /// The default validity duration for 4337 `UserOperation` signatures. /// /// Operations are valid for this duration from the time they are signed. const USER_OPERATION_VALIDITY_DURATION_MINUTES: i64 = 30; -#[derive(Debug, Serialize, Deserialize)] -pub struct SerializableIdentity { - pub nullifier: Field, - pub trapdoor: Field, -} - -impl From<&Identity> for SerializableIdentity { - fn from(identity: &Identity) -> Self { - Self { - nullifier: identity.nullifier, - trapdoor: identity.trapdoor, - } - } -} - /// Identifies a transaction that can be encoded, signed and executed as a 4337 `UserOperation`. #[allow(async_fn_in_trait)] pub trait Is4337Encodable { From af473d4fe7e722bfacecdf33f3441a9d7c1e5e63 Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Tue, 2 Sep 2025 16:29:05 -0700 Subject: [PATCH 19/34] remove unused code --- bedrock/src/primitives/contracts.rs | 25 ------------------------- bedrock/src/primitives/world_id.rs | 6 ++---- 2 files changed, 2 insertions(+), 29 deletions(-) diff --git a/bedrock/src/primitives/contracts.rs b/bedrock/src/primitives/contracts.rs index fd962da4..f815f94f 100644 --- a/bedrock/src/primitives/contracts.rs +++ b/bedrock/src/primitives/contracts.rs @@ -330,31 +330,6 @@ impl EncodedSafeOpStruct { } } -impl From for EncodedSafeOpStruct { - /// Converts a `UserOperation` into an `EncodedSafeOpStruct`. - /// - /// This implementation extracts validity timestamps from the `UserOperation`'s signature. - /// If the signature doesn't contain valid timestamps, it uses zero values as defaults. - /// - /// # Example - /// ```rust - /// use bedrock::primitives::contracts::{UserOperation, EncodedSafeOpStruct}; - /// - /// let user_op = UserOperation::default(); - /// let encoded_safe_op: EncodedSafeOpStruct = user_op.into(); - /// ``` - fn from(user_op: UserOperation) -> Self { - // Extract validity timestamps from the signature, or use defaults - let (valid_after, valid_until) = user_op - .extract_validity_timestamps() - .unwrap_or((U48::ZERO, U48::ZERO)); - - // Use the existing method to create the struct - Self::from_user_op_with_validity(&user_op, valid_after, valid_until) - .expect("Failed to convert UserOperation to EncodedSafeOpStruct") - } -} - impl From for IEntryPoint::PackedUserOperation { /// Converts a `UserOperation` into a `PackedUserOperation`. /// diff --git a/bedrock/src/primitives/world_id.rs b/bedrock/src/primitives/world_id.rs index 2fec343b..453b8c6c 100644 --- a/bedrock/src/primitives/world_id.rs +++ b/bedrock/src/primitives/world_id.rs @@ -166,10 +166,8 @@ pub async fn find_unused_nullifier_hash( let encoded_external_nullifier = EncodedExternalNullifier::from(external_nullifier); - let nullifier_hash = generate_nullifier_hash( - &identity, - encoded_external_nullifier.0, - ); + let nullifier_hash = + generate_nullifier_hash(&identity, encoded_external_nullifier.0); batch_hashes.push(U256::from_be_bytes(nullifier_hash.to_be_bytes::<32>())); batch_external_nullifiers.push(external_nullifier); From c5b0269dc76be5835af58347f91567580e89b0e2 Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Tue, 2 Sep 2025 16:30:42 -0700 Subject: [PATCH 20/34] clean --- bedrock/src/smart_account/transaction_4337.rs | 105 ------------------ 1 file changed, 105 deletions(-) diff --git a/bedrock/src/smart_account/transaction_4337.rs b/bedrock/src/smart_account/transaction_4337.rs index 04f48396..31928d7a 100644 --- a/bedrock/src/smart_account/transaction_4337.rs +++ b/bedrock/src/smart_account/transaction_4337.rs @@ -180,10 +180,6 @@ pub trait Is4337Encodable { Ok(user_op_hash) } - - // async fn get_pbh_nonce() -> u64 { - - // } } #[cfg(test)] @@ -506,105 +502,4 @@ mod tests { panic!("Expected InvalidInput error"); } } - - #[test] - fn test_user_operation_to_encoded_safe_op_struct_conversion() { - use alloy::primitives::{address, U256}; - use std::str::FromStr; - - // Create a UserOperation with valid signature containing timestamps - let mut signature = Vec::with_capacity(77); - - // Add validAfter (6 bytes) - timestamp 1704067200 - let valid_after_timestamp: u64 = 1704067200; - let valid_after_bytes = valid_after_timestamp.to_be_bytes(); - signature.extend_from_slice(&valid_after_bytes[2..8]); - - // Add validUntil (6 bytes) - timestamp 1735689600 - let valid_until_timestamp: u64 = 1735689600; - let valid_until_bytes = valid_until_timestamp.to_be_bytes(); - signature.extend_from_slice(&valid_until_bytes[2..8]); - - // Add dummy ECDSA signature (65 bytes) - signature.extend_from_slice(&[0xff; 65]); - - let user_op = UserOperation { - sender: address!("0x1111111111111111111111111111111111111111"), - nonce: U256::from(42), - call_data: Bytes::from_str("0x1234abcd").unwrap(), - signature: signature.into(), - call_gas_limit: 100000, - verification_gas_limit: 50000, - pre_verification_gas: U256::from(30000), - max_fee_per_gas: 2000000000, - max_priority_fee_per_gas: 1000000000, - ..Default::default() - }; - - // Test the From trait conversion - let encoded_safe_op: EncodedSafeOpStruct = user_op.clone().into(); - - // Verify the conversion worked correctly - assert_eq!(encoded_safe_op.safe, user_op.sender); - assert_eq!(encoded_safe_op.nonce, user_op.nonce); - assert_eq!(encoded_safe_op.call_gas_limit, user_op.call_gas_limit); - assert_eq!( - encoded_safe_op.verification_gas_limit, - user_op.verification_gas_limit - ); - assert_eq!( - encoded_safe_op.pre_verification_gas, - user_op.pre_verification_gas - ); - assert_eq!(encoded_safe_op.max_fee_per_gas, user_op.max_fee_per_gas); - assert_eq!( - encoded_safe_op.max_priority_fee_per_gas, - user_op.max_priority_fee_per_gas - ); - assert_eq!( - encoded_safe_op.valid_after, - U48::from(valid_after_timestamp) - ); - assert_eq!( - encoded_safe_op.valid_until, - U48::from(valid_until_timestamp) - ); - assert_eq!(encoded_safe_op.entry_point, *ENTRYPOINT_4337); - assert_eq!( - encoded_safe_op.call_data_hash, - keccak256(&user_op.call_data) - ); - assert_eq!( - encoded_safe_op.init_code_hash, - keccak256(user_op.get_init_code()) - ); - assert_eq!( - encoded_safe_op.paymaster_and_data_hash, - keccak256(user_op.get_paymaster_and_data()) - ); - } - - #[test] - fn test_user_operation_to_encoded_safe_op_struct_with_invalid_signature() { - use alloy::primitives::{address, U256}; - use std::str::FromStr; - - // Create a UserOperation with invalid signature (too short) - let user_op = UserOperation { - sender: address!("0x1111111111111111111111111111111111111111"), - nonce: U256::from(42), - call_data: Bytes::from_str("0x1234abcd").unwrap(), - signature: vec![0xff; 65].into(), // Invalid length - ..Default::default() - }; - - // Test the From trait conversion with invalid signature - let encoded_safe_op: EncodedSafeOpStruct = user_op.clone().into(); - - // Should use default timestamps (zero) when signature is invalid - assert_eq!(encoded_safe_op.valid_after, U48::ZERO); - assert_eq!(encoded_safe_op.valid_until, U48::ZERO); - assert_eq!(encoded_safe_op.safe, user_op.sender); - assert_eq!(encoded_safe_op.nonce, user_op.nonce); - } } From f6a9c37f619aba0ceaeaa76a923dfe0126bffe27 Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Wed, 3 Sep 2025 13:07:09 -0700 Subject: [PATCH 21/34] cleanly remove fields from rpcUserOperation --- bedrock/src/primitives/contracts.rs | 136 +++++++++++++++++- bedrock/src/smart_account/transaction_4337.rs | 16 ++- bedrock/src/transaction/rpc.rs | 67 ++++----- 3 files changed, 175 insertions(+), 44 deletions(-) diff --git a/bedrock/src/primitives/contracts.rs b/bedrock/src/primitives/contracts.rs index f815f94f..9d7b6ae6 100644 --- a/bedrock/src/primitives/contracts.rs +++ b/bedrock/src/primitives/contracts.rs @@ -1,3 +1,4 @@ +use crate::primitives::contracts::IEntryPoint::PackedUserOperation; use crate::primitives::contracts::IPBHEntryPoint::PBHPayload; use crate::primitives::{HttpError, PrimitiveError}; use crate::transaction::rpc::SponsorUserOperationResponse; @@ -5,7 +6,9 @@ use alloy::hex::FromHex; use alloy::primitives::{aliases::U48, keccak256, Address, Bytes, FixedBytes}; use alloy::sol; use alloy::sol_types::SolValue; +use alloy_primitives::{U128, U64, U8}; use ruint::aliases::U256; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::{str::FromStr, sync::LazyLock}; use world_chain_builder_pbh::external_nullifier::EncodedExternalNullifier; use world_chain_builder_pbh::payload::PBHPayload as WorldchainBuilderPBHPayload; @@ -18,6 +21,20 @@ static SAFE_OP_TYPEHASH: LazyLock> = LazyLock::new(|| { .expect("error initializing `SAFE_OP_TYPEHASH`") }); +/// PBHSignatureAggregator - Sepolia +/// Contract reference: +pub static PBH_SIGNATURE_AGGREGATOR_SEPOLIA: LazyLock
= LazyLock::new(|| { + Address::from_str("0x8af27Ee9AF538C48C7D2a2c8BD6a40eF830e2489") + .expect("failed to decode PBH_SIGNATURE_AGGREGATOR_SEPOLIA") +}); + +/// PBHSignatureAggregator - Mainnet +/// Contract reference: +pub static PBH_SIGNATURE_AGGREGATOR_MAINNET: LazyLock
= LazyLock::new(|| { + Address::from_str("0xd21306c75c956142c73c0c3bab282be68595081e") + .expect("failed to decode PBH_SIGNATURE_AGGREGATOR_MAINNET") +}); + /// v0.7.0 `EntryPoint` contract /// Contract reference: pub static ENTRYPOINT_4337: LazyLock
= LazyLock::new(|| { @@ -54,8 +71,122 @@ pub static PBH_SAFE_4337_MODULE_MAINNET: LazyLock
= LazyLock::new(|| { /// /// This is the length of a regular ECDSA signature with r,s,v (32 + 32 + 1 = 65 bytes) + 12 bytes for the validity timestamps. const USER_OPERATION_SIGNATURE_LENGTH: usize = 77; +/// authorization tuple for 7702 txn support +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct RpcEip7702Auth { + /// The chain ID of the authorization. + pub chain_id: U64, + /// The address of the authorization. + pub address: Address, + /// The nonce for the authorization. + pub nonce: U64, + /// signed authorizzation tuple. + pub y_parity: U8, + /// signed authorizzation tuple. + pub r: U256, + /// signed authorizzation tuple. + pub s: U256, +} + +/// User operation definition for RPC inputs +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct RpcUserOperationV0_7 { + sender: Address, + nonce: U256, + call_data: Bytes, + call_gas_limit: U128, + verification_gas_limit: U128, + pre_verification_gas: U256, + max_priority_fee_per_gas: U128, + max_fee_per_gas: U128, + #[serde(skip_serializing_if = "Option::is_none")] + factory: Option
, + #[serde(skip_serializing_if = "Option::is_none")] + factory_data: Option, + #[serde(skip_serializing_if = "Option::is_none")] + paymaster: Option
, + #[serde(skip_serializing_if = "Option::is_none")] + paymaster_verification_gas_limit: Option, + #[serde(skip_serializing_if = "Option::is_none")] + paymaster_post_op_gas_limit: Option, + #[serde(skip_serializing_if = "Option::is_none")] + paymaster_data: Option, + signature: Bytes, + #[serde(skip_serializing_if = "Option::is_none")] + eip7702_auth: Option, + #[serde(skip_serializing_if = "Option::is_none")] + aggregator: Option
, +} + +#[allow(clippy::from_over_into)] +impl Into for (PackedUserOperation, Address) { + fn into(self) -> RpcUserOperationV0_7 { + let (user_op, aggregator) = self; + RpcUserOperationV0_7 { + sender: user_op.sender, + nonce: user_op.nonce, + factory: None, + call_data: user_op.callData, + factory_data: None, + verification_gas_limit: (U256::from_be_bytes( + user_op.accountGasLimits.into(), + ) >> U256::from(128)) + .to(), + call_gas_limit: (U256::from_be_bytes(user_op.accountGasLimits.into()) + & U256::from_str("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF").unwrap()) + .to(), + pre_verification_gas: user_op.preVerificationGas, + max_priority_fee_per_gas: (U256::from_be_bytes(user_op.gasFees.into()) + >> U256::from(128)) + .to(), + max_fee_per_gas: (U256::from_be_bytes(user_op.gasFees.into()) + & U256::from_str("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF").unwrap()) + .to(), + signature: user_op.signature, + paymaster: None, + paymaster_data: None, + paymaster_post_op_gas_limit: None, + paymaster_verification_gas_limit: None, + aggregator: Some(aggregator), + eip7702_auth: None, + } + } +} -// --- JSON serialization helpers for ERC-4337 --- +#[allow(clippy::from_over_into)] +impl Into for PackedUserOperation { + fn into(self) -> RpcUserOperationV0_7 { + RpcUserOperationV0_7 { + sender: self.sender, + nonce: self.nonce, + factory: None, + call_data: self.callData, + factory_data: None, + verification_gas_limit: (U256::from_be_bytes(self.accountGasLimits.into()) + >> U256::from(128)) + .to(), + call_gas_limit: (U256::from_be_bytes(self.accountGasLimits.into()) + & U256::from_str("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF").unwrap()) + .to(), + pre_verification_gas: self.preVerificationGas, + max_priority_fee_per_gas: (U256::from_be_bytes(self.gasFees.into()) + >> U256::from(128)) + .to(), + max_fee_per_gas: (U256::from_be_bytes(self.gasFees.into()) + & U256::from_str("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF").unwrap()) + .to(), + signature: self.signature, + paymaster: None, + paymaster_data: None, + paymaster_post_op_gas_limit: None, + paymaster_verification_gas_limit: None, + aggregator: None, + eip7702_auth: None, + } + } +} fn serialize_u128_as_hex( value: &u128, @@ -75,7 +206,6 @@ fn serialize_u256_as_hex( } sol! { - /// Interface for the `Safe4337Module` contract. /// /// Reference: @@ -103,6 +233,7 @@ sol! { #[serde(serialize_with = "serialize_u256_as_hex")] uint256 nonce; /// Account Factory for new Accounts OR `0x7702` flag for EIP-7702 Accounts, otherwise address(0) + // #[serde(serialize_with = "serialize_if_non_zero_address")] address factory; /// Data for the Account Factory if factory is provided OR EIP-7702 initialization data, or empty array bytes factory_data; @@ -128,6 +259,7 @@ sol! { #[serde(serialize_with = "serialize_u128_as_hex")] uint128 max_priority_fee_per_gas; /// Address of paymaster contract, (or empty, if the sender pays for gas by itself) + // #[serde(serialize_with = "serialize_if_non_zero_address")] address paymaster; /// The amount of gas to allocate for the paymaster validation code (only if paymaster exists) /// Even though the type is `uint256`, in the `Safe4337Module` (see `EncodedSafeOpStruct`), it is expected as `uint128` for `paymasterAndData` validation. diff --git a/bedrock/src/smart_account/transaction_4337.rs b/bedrock/src/smart_account/transaction_4337.rs index 31928d7a..49516919 100644 --- a/bedrock/src/smart_account/transaction_4337.rs +++ b/bedrock/src/smart_account/transaction_4337.rs @@ -9,6 +9,7 @@ use crate::primitives::contracts::{ use crate::primitives::contracts::{ PBH_ENTRYPOINT_4337, PBH_SAFE_4337_MODULE_MAINNET, PBH_SAFE_4337_MODULE_SEPOLIA, + PBH_SIGNATURE_AGGREGATOR_MAINNET, PBH_SIGNATURE_AGGREGATOR_SEPOLIA, }; use crate::primitives::{Network, PrimitiveError}; use crate::smart_account::{SafeSmartAccountSigner, GNOSIS_SAFE_4337_MODULE}; @@ -137,14 +138,19 @@ pub trait Is4337Encodable { )?; let domain_separator: Address; + let mut aggregator: Option
= None; if pbh { match network { - Network::WorldChain => domain_separator = *PBH_SAFE_4337_MODULE_MAINNET, + Network::WorldChain => { + domain_separator = *PBH_SAFE_4337_MODULE_MAINNET; + aggregator = Some(*PBH_SIGNATURE_AGGREGATOR_MAINNET); + } Network::WorldChainSepolia => { - domain_separator = *PBH_SAFE_4337_MODULE_SEPOLIA + domain_separator = *PBH_SAFE_4337_MODULE_SEPOLIA; + aggregator = Some(*PBH_SIGNATURE_AGGREGATOR_SEPOLIA); } - _ => panic!("Invalid network for PBH"), + _ => return Err(RpcError::InvalidRequest(format!("Invalid network {:?} for PBH", network))), } } else { domain_separator = *GNOSIS_SAFE_4337_MODULE; @@ -167,6 +173,8 @@ pub trait Is4337Encodable { let pbh_payload = generate_pbh_proof(user_operation.clone(), network).await; full_signature .extend_from_slice(PBHPayload::from(pbh_payload).abi_encode().as_ref()); + + // user_operation.aggregator = Some(address!("0x8af27ee9af538c48c7d2a2c8bd6a40ef830e2489")); } user_operation.signature = full_signature.into(); @@ -175,7 +183,7 @@ pub trait Is4337Encodable { let user_op_hash: FixedBytes<32> = rpc_client // Always send to standard 4337 entrypoint even for PBH // The bundler will route it to the PBH entrypoint if it's a PBH transaction - .send_user_operation(network, &user_operation, *ENTRYPOINT_4337, provider) + .send_user_operation(network, &user_operation, *ENTRYPOINT_4337, provider, aggregator) .await?; Ok(user_op_hash) diff --git a/bedrock/src/transaction/rpc.rs b/bedrock/src/transaction/rpc.rs index 99613b69..fce33961 100644 --- a/bedrock/src/transaction/rpc.rs +++ b/bedrock/src/transaction/rpc.rs @@ -7,6 +7,7 @@ use crate::{ primitives::http_client::{get_http_client, HttpHeader}, primitives::{ + contracts::{IEntryPoint::PackedUserOperation, RpcUserOperationV0_7}, AuthenticatedHttpClient, HttpError, HttpMethod, Network, PrimitiveError, }, smart_account::{SafeSmartAccountError, UserOperation}, @@ -14,8 +15,7 @@ use crate::{ use alloy::hex::FromHex; use alloy::primitives::{Address, Bytes, FixedBytes, U128, U256}; use serde::{Deserialize, Serialize}; -use serde_json::Map; -use serde_json::Value; +use serde_json::{Map, Value}; use std::sync::{Arc, OnceLock}; /// Global RPC client instance for Bedrock operations @@ -148,6 +148,11 @@ pub enum RpcError { /// Safe Smart Account operation error #[error("Safe Smart Account operation failed: {0}")] SafeSmartAccountError(#[from] SafeSmartAccountError), + + /// Invalid inputs like invalid network + #[error("Invalid request: {0}")] + InvalidRequest(String), + } /// Response from `wa_sponsorUserOperation` @@ -306,6 +311,13 @@ impl RpcClient { /// Submits a signed `UserOperation` via `eth_sendUserOperation` /// + /// # Arguments + /// - `network`: target network + /// - `user_operation`: the user operation to submit + /// - `entrypoint`: the entry point contract address + /// - `provider`: selected 4337 provider to include in headers + /// - `aggregator`: optional aggregator address to include in the user operation + /// /// # Errors /// /// Returns an error if: @@ -320,42 +332,21 @@ impl RpcClient { user_operation: &UserOperation, entrypoint: Address, provider: RpcProviderName, + aggregator: Option
, ) -> Result, RpcError> { - // TODO: Move this logic to the right place for serialization - - // Serialize user operation, removing factory and factory_data if factory is address(0) - let mut user_op_value = - serde_json::to_value(user_operation).map_err(|_| RpcError::JsonError)?; - - if let Some(user_op_obj) = user_op_value.as_object_mut() { - // Remove factory and factory_data if factory is address(0) - if let Some(factory_value) = user_op_obj.get("factory") { - if factory_value == "0x0000000000000000000000000000000000000000" { - user_op_obj.remove("factory"); - user_op_obj.remove("factoryData"); - } - } - - // Remove paymaster fields if paymaster is address(0) or None - if let Some(paymaster_value) = user_op_obj.get("paymaster") { - if paymaster_value == "0x0000000000000000000000000000000000000000" - || paymaster_value.is_null() - { - user_op_obj.remove("paymaster"); - user_op_obj.remove("paymasterData"); - user_op_obj.remove("paymasterPostOpGasLimit"); - user_op_obj.remove("paymasterVerificationGasLimit"); - } - } - - // Add aggregator field - user_op_obj.insert( - "aggregator".to_string(), - serde_json::Value::String( - "0x8af27ee9af538c48c7d2a2c8bd6a40ef830e2489".to_string(), - ), - ); - } + let packed_user_op: PackedUserOperation = user_operation.clone().into(); + + // Convert to RpcUserOperationV0_7 based on whether aggregator is provided + let rpc_user_op: RpcUserOperationV0_7 = if let Some(agg_addr) = aggregator { + (packed_user_op, agg_addr).into() + } else { + packed_user_op.into() + }; + + // Serialize the RpcUserOperationV0_7 to JSON + // Seralization will remove unused fields like factory and paymaster from JSON + let user_op_value = + serde_json::to_value(&rpc_user_op).map_err(|_| RpcError::JsonError)?; let params = vec![ user_op_value, @@ -405,7 +396,7 @@ impl RpcClient { serde_json::Value::String(format!("{data:?}")), ), ])), - serde_json::Value::String("latest".to_string()), + serde_json::Value::String("latest".to_string()), // TODO: Allow passing in block number ]; let result: String = self From 345c030f9727f4da807f1779ce3070410723c4b4 Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Wed, 3 Sep 2025 13:08:02 -0700 Subject: [PATCH 22/34] fix --- bedrock/src/primitives/contracts.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bedrock/src/primitives/contracts.rs b/bedrock/src/primitives/contracts.rs index 9d7b6ae6..293719f7 100644 --- a/bedrock/src/primitives/contracts.rs +++ b/bedrock/src/primitives/contracts.rs @@ -233,7 +233,7 @@ sol! { #[serde(serialize_with = "serialize_u256_as_hex")] uint256 nonce; /// Account Factory for new Accounts OR `0x7702` flag for EIP-7702 Accounts, otherwise address(0) - // #[serde(serialize_with = "serialize_if_non_zero_address")] + #[serde(serialize_with = "serialize_if_non_zero_address")] address factory; /// Data for the Account Factory if factory is provided OR EIP-7702 initialization data, or empty array bytes factory_data; @@ -259,7 +259,7 @@ sol! { #[serde(serialize_with = "serialize_u128_as_hex")] uint128 max_priority_fee_per_gas; /// Address of paymaster contract, (or empty, if the sender pays for gas by itself) - // #[serde(serialize_with = "serialize_if_non_zero_address")] + #[serde(serialize_with = "serialize_if_non_zero_address")] address paymaster; /// The amount of gas to allocate for the paymaster validation code (only if paymaster exists) /// Even though the type is `uint256`, in the `Safe4337Module` (see `EncodedSafeOpStruct`), it is expected as `uint128` for `paymasterAndData` validation. From 11c312c7a7bda15cdd3bf5656abb5fb86ae9114e Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Wed, 3 Sep 2025 13:08:16 -0700 Subject: [PATCH 23/34] fix --- bedrock/src/primitives/contracts.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/bedrock/src/primitives/contracts.rs b/bedrock/src/primitives/contracts.rs index 293719f7..7841a978 100644 --- a/bedrock/src/primitives/contracts.rs +++ b/bedrock/src/primitives/contracts.rs @@ -233,7 +233,6 @@ sol! { #[serde(serialize_with = "serialize_u256_as_hex")] uint256 nonce; /// Account Factory for new Accounts OR `0x7702` flag for EIP-7702 Accounts, otherwise address(0) - #[serde(serialize_with = "serialize_if_non_zero_address")] address factory; /// Data for the Account Factory if factory is provided OR EIP-7702 initialization data, or empty array bytes factory_data; @@ -259,7 +258,6 @@ sol! { #[serde(serialize_with = "serialize_u128_as_hex")] uint128 max_priority_fee_per_gas; /// Address of paymaster contract, (or empty, if the sender pays for gas by itself) - #[serde(serialize_with = "serialize_if_non_zero_address")] address paymaster; /// The amount of gas to allocate for the paymaster validation code (only if paymaster exists) /// Even though the type is `uint256`, in the `Safe4337Module` (see `EncodedSafeOpStruct`), it is expected as `uint128` for `paymasterAndData` validation. From 77415fbd785f155036c504fb996a92ea5ab17778 Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Wed, 3 Sep 2025 13:41:57 -0700 Subject: [PATCH 24/34] add world id errors --- bedrock/src/primitives/world_id.rs | 107 +++++++++++++----- bedrock/src/smart_account/transaction_4337.rs | 32 +++++- bedrock/src/transaction/rpc.rs | 4 +- 3 files changed, 108 insertions(+), 35 deletions(-) diff --git a/bedrock/src/primitives/world_id.rs b/bedrock/src/primitives/world_id.rs index 453b8c6c..74c96d4a 100644 --- a/bedrock/src/primitives/world_id.rs +++ b/bedrock/src/primitives/world_id.rs @@ -6,7 +6,7 @@ use chrono::{Datelike, Utc}; use reqwest::Client; use semaphore_rs::identity::Identity; use semaphore_rs::poseidon_tree::Proof as PoseidonTreeProof; -use semaphore_rs::protocol::{generate_nullifier_hash, generate_proof}; +use semaphore_rs::protocol::{generate_nullifier_hash, generate_proof, ProofError}; use semaphore_rs::{hash_to_field, Field}; use semaphore_rs_proof::Proof as SemaphoreProof; use serde::{Deserialize, Serialize}; @@ -21,6 +21,7 @@ use world_chain_builder_pbh::{ use crate::primitives::contracts::{IPBHEntryPoint, PBH_ENTRYPOINT_4337}; use crate::primitives::Network; use crate::transaction::rpc::get_rpc_client; +use crate::transaction::RpcError; use crate::{ primitives::contracts::IEntryPoint::PackedUserOperation, smart_account::UserOperation, @@ -54,6 +55,30 @@ pub struct InclusionProof { pub proof: PoseidonTreeProof, } +/// Errors that can occur when interacting with RPC operations. +#[crate::bedrock_error] +#[derive(Debug, Deserialize)] +pub enum WorldIdError { + /// WorldID identity has not been initialized + #[error("WorldID identity not initialized. Call set_world_id_identity() first.")] + WorldIdIdentityNotInitialized, + /// Failed to fetch inclusion proof from sequencer + #[error("Inclusion proof error: {0}")] + InclusionProofError(String), + /// User has no more PBH transactions remaining + #[error("No PBH transactions remaining")] + NoPBHTransactionsRemaining, + /// Invalid network for World ID + #[error("Invalid network for World ID: {0}")] + InvalidNetworkError(String), + /// Proof error + #[error("Proof error: {0}")] + ProofError(#[from] ProofError), + /// RPC error + #[error("RPC error: {0}")] + RpcError(#[from] RpcError), +} + /// Global World ID identity instance for Bedrock operations static WORLD_ID_IDENTITY_INSTANCE: OnceLock> = OnceLock::new(); @@ -97,50 +122,59 @@ pub fn is_world_id_identity_initialized() -> bool { pub async fn generate_pbh_proof( user_op: UserOperation, network: Network, -) -> WorldchainBuilderPBHPayload { +) -> Result { let packed_user_op: PackedUserOperation = PackedUserOperation::from(user_op); let signal = hash_user_op(&packed_user_op); - let external_nullifier = find_unused_nullifier_hash(network).await.unwrap(); + let external_nullifier = find_unused_nullifier_hash(network).await?; let encoded_external_nullifier = EncodedExternalNullifier::from(external_nullifier); - let identity = get_world_id_identity().unwrap(); + let identity = + get_world_id_identity().ok_or(WorldIdError::WorldIdIdentityNotInitialized)?; let inclusion_proof = fetch_inclusion_proof( match network { Network::WorldChain => PRODUCTION_SEQUENCER_URL, Network::WorldChainSepolia => STAGING_SEQUENCER_URL, - _ => panic!("Invalid network for World ID"), + _ => return Err(WorldIdError::InvalidNetworkError(network.to_string())), }, - &identity, + (*identity).clone(), ) .await - .unwrap(); + .map_err(|e| { + WorldIdError::InclusionProofError(format!( + "Failed to fetch inclusion proof: {e}" + )) + })?; let proof: SemaphoreProof = generate_proof( &identity, &inclusion_proof.proof, encoded_external_nullifier.0, signal, - ) - .expect("Failed to generate semaphore proof"); + )?; let nullifier_hash = generate_nullifier_hash(&identity, encoded_external_nullifier.0); let proof = WorldchainBuilderProof(proof); - WorldchainBuilderPBHPayload { + Ok(WorldchainBuilderPBHPayload { external_nullifier, nullifier_hash, root: inclusion_proof.root, proof, - } + }) } /// Finds the first unused nullifier hash for the current World ID identity in batches pub async fn find_unused_nullifier_hash( network: Network, -) -> Result> { - let identity = get_world_id_identity().unwrap(); +) -> Result { + let identity = + get_world_id_identity().ok_or(WorldIdError::WorldIdIdentityNotInitialized)?; + + let rpc_client: &'static crate::transaction::RpcClient = get_rpc_client() + .map_err(|_| WorldIdError::RpcError(RpcError::HttpClientNotInitialized))?; + let now = Utc::now(); let current_year = now.year() as u16; let current_month = now.month() as u16; @@ -148,12 +182,10 @@ pub async fn find_unused_nullifier_hash( let max_nonce = match network { Network::WorldChain => PRODUCTION_MAX_NONCE, Network::WorldChainSepolia => STAGING_MAX_NONCE, - _ => panic!("Invalid network for PBH"), + _ => return Err(WorldIdError::InvalidNetworkError(network.to_string())), }; - let rpc_client = get_rpc_client().unwrap(); - - // Process nonces in batches for efficiency + // Process nonces in batches for batch_start in (0..max_nonce).step_by(MAX_NONCE_BATCH_SIZE as usize) { let batch_end = std::cmp::min(batch_start + MAX_NONCE_BATCH_SIZE, max_nonce); let mut batch_hashes = Vec::new(); @@ -178,13 +210,21 @@ pub async fn find_unused_nullifier_hash( hashes: batch_hashes, }; - let result = rpc_client + // Try the RPC call, but continue to next batch if this one fails + let result = match rpc_client .eth_call( network, *PBH_ENTRYPOINT_4337, Bytes::from(call.abi_encode()), ) - .await?; + .await + { + Ok(result) => result, + Err(e) => { + crate::warn!("Failed to fetch first unused nullifier hash for batch. Continuing to next batch. {e}"); + continue; + } + }; let unsigned_value = U256::from_be_slice(&result); let signed_from_slice = I256::from_raw(unsigned_value); @@ -204,7 +244,7 @@ pub async fn find_unused_nullifier_hash( } } - Err("No PBH transactions remaining".into()) + Err(WorldIdError::NoPBHTransactionsRemaining) } /// Fetches an inclusion proof for a given identity from the signup sequencer. @@ -214,23 +254,36 @@ pub async fn find_unused_nullifier_hash( /// # Arguments /// * `url` - The URL of the sequencer /// * `identity` - The identity to fetch the proof for +/// +/// # Errors +/// Returns `WorldIdError::InclusionProofError` if the request fails or response cannot be parsed. pub async fn fetch_inclusion_proof( url: &str, - identity: &Identity, -) -> eyre::Result { + identity: Identity, +) -> Result { let client = Client::new(); - let commitment = identity.commitment(); + + // Make the HTTP request and map all errors to WorldIdError::InclusionProofError let response = client .post(format!("{url}/inclusionProof")) .json(&serde_json::json! {{ "identityCommitment": commitment, }}) .send() - .await? - .error_for_status()?; - - let proof: InclusionProof = response.json().await?; + .await + .map_err(|e| { + WorldIdError::InclusionProofError(format!("HTTP request failed: {e}")) + })? + .error_for_status() + .map_err(|e| { + WorldIdError::InclusionProofError(format!("HTTP status error: {e}")) + })?; + + // Parse the JSON response and map parsing errors + let proof: InclusionProof = response.json().await.map_err(|e| { + WorldIdError::InclusionProofError(format!("Failed to parse response: {e}")) + })?; Ok(proof) } diff --git a/bedrock/src/smart_account/transaction_4337.rs b/bedrock/src/smart_account/transaction_4337.rs index 49516919..d45b49f2 100644 --- a/bedrock/src/smart_account/transaction_4337.rs +++ b/bedrock/src/smart_account/transaction_4337.rs @@ -150,7 +150,12 @@ pub trait Is4337Encodable { domain_separator = *PBH_SAFE_4337_MODULE_SEPOLIA; aggregator = Some(*PBH_SIGNATURE_AGGREGATOR_SEPOLIA); } - _ => return Err(RpcError::InvalidRequest(format!("Invalid network {:?} for PBH", network))), + _ => { + return Err(RpcError::InvalidRequest(format!( + "Invalid network {:?} for PBH", + network + ))) + } } } else { domain_separator = *GNOSIS_SAFE_4337_MODULE; @@ -171,10 +176,19 @@ pub trait Is4337Encodable { // PBH Logic if pbh { let pbh_payload = generate_pbh_proof(user_operation.clone(), network).await; - full_signature - .extend_from_slice(PBHPayload::from(pbh_payload).abi_encode().as_ref()); - - // user_operation.aggregator = Some(address!("0x8af27ee9af538c48c7d2a2c8bd6a40ef830e2489")); + match pbh_payload { + Ok(pbh_payload) => { + full_signature.extend_from_slice( + PBHPayload::from(pbh_payload).abi_encode().as_ref(), + ); + } + Err(e) => { + // TODO: Send standard user operation if PBH logic fails at any point + return Err(RpcError::InvalidRequest(format!( + "Failed to generate PBH payload{e}" + ))); + } + } } user_operation.signature = full_signature.into(); @@ -183,7 +197,13 @@ pub trait Is4337Encodable { let user_op_hash: FixedBytes<32> = rpc_client // Always send to standard 4337 entrypoint even for PBH // The bundler will route it to the PBH entrypoint if it's a PBH transaction - .send_user_operation(network, &user_operation, *ENTRYPOINT_4337, provider, aggregator) + .send_user_operation( + network, + &user_operation, + *ENTRYPOINT_4337, + provider, + aggregator, + ) .await?; Ok(user_op_hash) diff --git a/bedrock/src/transaction/rpc.rs b/bedrock/src/transaction/rpc.rs index fce33961..475da7b5 100644 --- a/bedrock/src/transaction/rpc.rs +++ b/bedrock/src/transaction/rpc.rs @@ -149,10 +149,9 @@ pub enum RpcError { #[error("Safe Smart Account operation failed: {0}")] SafeSmartAccountError(#[from] SafeSmartAccountError), - /// Invalid inputs like invalid network + /// Invalid inputs #[error("Invalid request: {0}")] InvalidRequest(String), - } /// Response from `wa_sponsorUserOperation` @@ -297,6 +296,7 @@ impl RpcClient { // Build params as a positional array. If no token is provided, omit the 3rd param entirely // so the backend can auto-fill an empty object as needed. let mut params: Vec = Vec::with_capacity(3); + // TODO: Should this be RPCUserOperationV0_7? params.push( serde_json::to_value(user_operation).map_err(|_| RpcError::JsonError)?, ); From e7e8e54544ac0b852c965e3506caf1a06aa17bb6 Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Wed, 3 Sep 2025 13:52:32 -0700 Subject: [PATCH 25/34] fix merge conflicts --- Cargo.lock | 6 +----- bedrock/Cargo.toml | 4 ---- bedrock/src/root_key/mod.rs | 2 +- bedrock/src/smart_account/transaction_4337.rs | 7 ++++--- bedrock/src/transaction/mod.rs | 3 +-- bedrock/tests/test_smart_pbh_sepolia.rs | 1 + 6 files changed, 8 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ab9e8334..2cc71b34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1565,16 +1565,12 @@ dependencies = [ "rand 0.9.2", "reqwest 0.12.23", "ruint", + "secrecy", "semaphore-rs", "semaphore-rs-proof", "serde", "serde_json", "strum", - "rand 0.8.5", - "ruint", - "secrecy", - "serde", - "serde_json", "subtle", "syn 2.0.104", "test-case", diff --git a/bedrock/Cargo.toml b/bedrock/Cargo.toml index d9edf8ca..460734ca 100644 --- a/bedrock/Cargo.toml +++ b/bedrock/Cargo.toml @@ -45,7 +45,6 @@ chrono = { version = "0.4.41", default-features = false, features = [ ] } eyre = "0.6.12" futures = "0.3.31" -chrono = { version = "0.4.41", default-features = false, features = ["now", "std"] } hex = "0.4.3" log = "0.4.22" proc-macro2 = "1.0" @@ -58,10 +57,7 @@ semaphore-rs-proof = "0.3.1" serde = "1.0.219" serde_json = "1.0" strum = "0.27.1" -ruint = { version = "1.15.0", default-features = false, features = ["serde"] } secrecy = { version = "0.10", features = ["serde"] } -serde = "1.0.219" -serde_json = "1.0" subtle = "2.6.1" syn = { version = "2.0", features = ["full"] } test-case = "3.3.1" diff --git a/bedrock/src/root_key/mod.rs b/bedrock/src/root_key/mod.rs index 5f45b3af..e5f2e0b3 100644 --- a/bedrock/src/root_key/mod.rs +++ b/bedrock/src/root_key/mod.rs @@ -1,5 +1,5 @@ use bedrock_macros::{bedrock_error, bedrock_export}; -use rand::{rngs::OsRng, RngCore}; +use rand::{rngs::OsRng, RngCore, TryRngCore}; use secrecy::{ExposeSecret, SecretBox}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use subtle::ConstantTimeEq; diff --git a/bedrock/src/smart_account/transaction_4337.rs b/bedrock/src/smart_account/transaction_4337.rs index 54e39f77..ad30a155 100644 --- a/bedrock/src/smart_account/transaction_4337.rs +++ b/bedrock/src/smart_account/transaction_4337.rs @@ -11,10 +11,11 @@ use crate::primitives::contracts::{ PBH_ENTRYPOINT_4337, PBH_SAFE_4337_MODULE_MAINNET, PBH_SAFE_4337_MODULE_SEPOLIA, PBH_SIGNATURE_AGGREGATOR_MAINNET, PBH_SIGNATURE_AGGREGATOR_SEPOLIA, }; -use crate::primitives::{Network, PrimitiveError}; -use crate::smart_account::{SafeSmartAccountSigner, GNOSIS_SAFE_4337_MODULE}; use crate::primitives::world_id::generate_pbh_proof; -use crate::smart_account::{SafeSmartAccount, SafeSmartAccountSigner}; +use crate::primitives::{Network, PrimitiveError}; +use crate::smart_account::{ + SafeSmartAccount, SafeSmartAccountSigner, GNOSIS_SAFE_4337_MODULE, +}; use crate::transaction::rpc::{RpcError, RpcProviderName}; use alloy::primitives::{aliases::U48, Address, Bytes, FixedBytes}; diff --git a/bedrock/src/transaction/mod.rs b/bedrock/src/transaction/mod.rs index f0f9eab7..3d332e2d 100644 --- a/bedrock/src/transaction/mod.rs +++ b/bedrock/src/transaction/mod.rs @@ -63,7 +63,6 @@ impl SafeSmartAccount { pub async fn transaction_transfer( &self, network: Network, - // TODO: Use struct for transaction parameters token_address: &str, to_address: &str, amount: &str, @@ -85,7 +84,7 @@ impl SafeSmartAccount { let provider = RpcProviderName::Alchemy; let user_op_hash = transaction - .sign_and_execute(self, Network::WorldChain, None, pbh, Some(metadata), provider) + .sign_and_execute(self, network, None, Some(metadata), pbh, provider) .await .map_err(|e| TransactionError::Generic { message: format!("Failed to execute transaction: {e}"), diff --git a/bedrock/tests/test_smart_pbh_sepolia.rs b/bedrock/tests/test_smart_pbh_sepolia.rs index 50fb87af..80b93f5c 100644 --- a/bedrock/tests/test_smart_pbh_sepolia.rs +++ b/bedrock/tests/test_smart_pbh_sepolia.rs @@ -182,6 +182,7 @@ async fn test_pbh_transaction_transfer_full_flow() -> anyhow::Result<()> { amount, true, RpcProviderName::Alchemy, + None ) .await .expect("transaction_transfer failed"); From 48a6ffa9865f6305abf46beac9120af6e02c4473 Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Wed, 3 Sep 2025 13:57:01 -0700 Subject: [PATCH 26/34] clippy --- bedrock/src/primitives/contracts.rs | 6 +++--- bedrock/src/root_key/mod.rs | 2 +- bedrock/src/smart_account/transaction_4337.rs | 3 +-- bedrock/tests/test_smart_account_transfer.rs | 7 ++++--- bedrock/tests/test_smart_pbh_sepolia.rs | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/bedrock/src/primitives/contracts.rs b/bedrock/src/primitives/contracts.rs index 7841a978..8f2ffd22 100644 --- a/bedrock/src/primitives/contracts.rs +++ b/bedrock/src/primitives/contracts.rs @@ -8,7 +8,7 @@ use alloy::sol; use alloy::sol_types::SolValue; use alloy_primitives::{U128, U64, U8}; use ruint::aliases::U256; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde::{Deserialize, Serialize, Serializer}; use std::{str::FromStr, sync::LazyLock}; use world_chain_builder_pbh::external_nullifier::EncodedExternalNullifier; use world_chain_builder_pbh::payload::PBHPayload as WorldchainBuilderPBHPayload; @@ -21,14 +21,14 @@ static SAFE_OP_TYPEHASH: LazyLock> = LazyLock::new(|| { .expect("error initializing `SAFE_OP_TYPEHASH`") }); -/// PBHSignatureAggregator - Sepolia +/// `PBHSignatureAggregator` - Sepolia /// Contract reference: pub static PBH_SIGNATURE_AGGREGATOR_SEPOLIA: LazyLock
= LazyLock::new(|| { Address::from_str("0x8af27Ee9AF538C48C7D2a2c8BD6a40eF830e2489") .expect("failed to decode PBH_SIGNATURE_AGGREGATOR_SEPOLIA") }); -/// PBHSignatureAggregator - Mainnet +/// `PBHSignatureAggregator` - Mainnet /// Contract reference: pub static PBH_SIGNATURE_AGGREGATOR_MAINNET: LazyLock
= LazyLock::new(|| { Address::from_str("0xd21306c75c956142c73c0c3bab282be68595081e") diff --git a/bedrock/src/root_key/mod.rs b/bedrock/src/root_key/mod.rs index e5f2e0b3..72ab0f14 100644 --- a/bedrock/src/root_key/mod.rs +++ b/bedrock/src/root_key/mod.rs @@ -1,5 +1,5 @@ use bedrock_macros::{bedrock_error, bedrock_export}; -use rand::{rngs::OsRng, RngCore, TryRngCore}; +use rand::{rngs::OsRng, TryRngCore}; use secrecy::{ExposeSecret, SecretBox}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use subtle::ConstantTimeEq; diff --git a/bedrock/src/smart_account/transaction_4337.rs b/bedrock/src/smart_account/transaction_4337.rs index ad30a155..0a2f1d30 100644 --- a/bedrock/src/smart_account/transaction_4337.rs +++ b/bedrock/src/smart_account/transaction_4337.rs @@ -147,8 +147,7 @@ pub trait Is4337Encodable { } _ => { return Err(RpcError::InvalidRequest(format!( - "Invalid network {:?} for PBH", - network + "Invalid network {network:?} for PBH" ))) } } diff --git a/bedrock/tests/test_smart_account_transfer.rs b/bedrock/tests/test_smart_account_transfer.rs index da47baa3..51b075e9 100644 --- a/bedrock/tests/test_smart_account_transfer.rs +++ b/bedrock/tests/test_smart_account_transfer.rs @@ -9,11 +9,11 @@ use alloy::{ }; use bedrock::{ - primitives::http_client::{ + primitives::{http_client::{ set_http_client, AuthenticatedHttpClient, HttpError, HttpHeader, HttpMethod, - }, + }, Network}, smart_account::{SafeSmartAccount, ENTRYPOINT_4337}, - transaction::foreign::UnparsedUserOperation, + transaction::{foreign::UnparsedUserOperation, RpcProviderName}, }; use serde::Serialize; @@ -294,6 +294,7 @@ async fn test_transaction_transfer_full_flow_executes_user_operation_non_pbh( let amount = "1000000000000000000"; // 1 WLD let _user_op_hash = safe_account .transaction_transfer( + Network::WorldChain, &wld_token_address.to_string(), &recipient.to_string(), amount, diff --git a/bedrock/tests/test_smart_pbh_sepolia.rs b/bedrock/tests/test_smart_pbh_sepolia.rs index 80b93f5c..29c31e44 100644 --- a/bedrock/tests/test_smart_pbh_sepolia.rs +++ b/bedrock/tests/test_smart_pbh_sepolia.rs @@ -167,7 +167,7 @@ async fn test_pbh_transaction_transfer_full_flow() -> anyhow::Result<()> { nullifier: nullifier.parse().unwrap(), trapdoor: trapdoor.parse().unwrap(), }; - let _ = set_world_id_identity(Arc::new(identity)); + set_world_id_identity(Arc::new(identity)); // 8) Execute high-level transfer via transaction_transfer let safe_account = SafeSmartAccount::new(owner_key_hex, safe_address)?; From 18efe3b5ba2aa60c90349d286ecb39030d23b729 Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Wed, 3 Sep 2025 13:58:38 -0700 Subject: [PATCH 27/34] fmt --- bedrock/tests/test_smart_account_transfer.rs | 9 ++++++--- bedrock/tests/test_smart_pbh_sepolia.rs | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/bedrock/tests/test_smart_account_transfer.rs b/bedrock/tests/test_smart_account_transfer.rs index 51b075e9..e8680afa 100644 --- a/bedrock/tests/test_smart_account_transfer.rs +++ b/bedrock/tests/test_smart_account_transfer.rs @@ -9,9 +9,12 @@ use alloy::{ }; use bedrock::{ - primitives::{http_client::{ - set_http_client, AuthenticatedHttpClient, HttpError, HttpHeader, HttpMethod, - }, Network}, + primitives::{ + http_client::{ + set_http_client, AuthenticatedHttpClient, HttpError, HttpHeader, HttpMethod, + }, + Network, + }, smart_account::{SafeSmartAccount, ENTRYPOINT_4337}, transaction::{foreign::UnparsedUserOperation, RpcProviderName}, }; diff --git a/bedrock/tests/test_smart_pbh_sepolia.rs b/bedrock/tests/test_smart_pbh_sepolia.rs index 29c31e44..3168f1ab 100644 --- a/bedrock/tests/test_smart_pbh_sepolia.rs +++ b/bedrock/tests/test_smart_pbh_sepolia.rs @@ -182,7 +182,7 @@ async fn test_pbh_transaction_transfer_full_flow() -> anyhow::Result<()> { amount, true, RpcProviderName::Alchemy, - None + None, ) .await .expect("transaction_transfer failed"); From 73db958e8202c0315c86c7264f1f2530d5faa4fb Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Wed, 3 Sep 2025 14:06:25 -0700 Subject: [PATCH 28/34] fixes --- Cargo.lock | 1 - bedrock/Cargo.toml | 1 - bedrock/src/primitives/contracts.rs | 2 +- bedrock/src/primitives/mod.rs | 2 +- bedrock/src/transaction/mod.rs | 2 +- bedrock/tests/test_smart_pbh_sepolia.rs | 9 +++++---- 6 files changed, 8 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2cc71b34..bc9932ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1556,7 +1556,6 @@ dependencies = [ "bon", "chrono", "dotenvy", - "eyre", "futures", "hex", "log", diff --git a/bedrock/Cargo.toml b/bedrock/Cargo.toml index 460734ca..8d3682fa 100644 --- a/bedrock/Cargo.toml +++ b/bedrock/Cargo.toml @@ -43,7 +43,6 @@ chrono = { version = "0.4.41", default-features = false, features = [ "now", "std", ] } -eyre = "0.6.12" futures = "0.3.31" hex = "0.4.3" log = "0.4.22" diff --git a/bedrock/src/primitives/contracts.rs b/bedrock/src/primitives/contracts.rs index 8f2ffd22..93366337 100644 --- a/bedrock/src/primitives/contracts.rs +++ b/bedrock/src/primitives/contracts.rs @@ -8,7 +8,7 @@ use alloy::sol; use alloy::sol_types::SolValue; use alloy_primitives::{U128, U64, U8}; use ruint::aliases::U256; -use serde::{Deserialize, Serialize, Serializer}; +use serde::{Deserialize, Serialize}; use std::{str::FromStr, sync::LazyLock}; use world_chain_builder_pbh::external_nullifier::EncodedExternalNullifier; use world_chain_builder_pbh::payload::PBHPayload as WorldchainBuilderPBHPayload; diff --git a/bedrock/src/primitives/mod.rs b/bedrock/src/primitives/mod.rs index ed6f17dc..60e02c85 100644 --- a/bedrock/src/primitives/mod.rs +++ b/bedrock/src/primitives/mod.rs @@ -75,7 +75,7 @@ pub enum Network { Optimism = 10, /// World Chain (chain ID: 480) WorldChain = 480, - /// World Chain Sepolia (chain ID: 481) + /// World Chain Sepolia (chain ID: 4801) WorldChainSepolia = 4801, } diff --git a/bedrock/src/transaction/mod.rs b/bedrock/src/transaction/mod.rs index 3d332e2d..c5216aff 100644 --- a/bedrock/src/transaction/mod.rs +++ b/bedrock/src/transaction/mod.rs @@ -67,7 +67,7 @@ impl SafeSmartAccount { to_address: &str, amount: &str, pbh: bool, - provider: RpcProviderName, + _provider: RpcProviderName, transfer_association: Option, ) -> Result { let token_address = Address::parse_from_ffi(token_address, "token_address")?; diff --git a/bedrock/tests/test_smart_pbh_sepolia.rs b/bedrock/tests/test_smart_pbh_sepolia.rs index 3168f1ab..d735423b 100644 --- a/bedrock/tests/test_smart_pbh_sepolia.rs +++ b/bedrock/tests/test_smart_pbh_sepolia.rs @@ -27,7 +27,7 @@ mod common; // ------------------ Mock HTTP client that actually executes the op on Anvil ------------------ #[derive(Clone)] -struct AnvilBackedHttpClient

+struct AlchemyBackedHttpClient

where P: Provider + Clone + Send + Sync + 'static, { @@ -49,7 +49,7 @@ struct SponsorUserOperationResponseLite<'a> { } #[async_trait::async_trait] -impl

AuthenticatedHttpClient for AnvilBackedHttpClient

+impl

AuthenticatedHttpClient for AlchemyBackedHttpClient

where P: Provider + Clone + Send + Sync + 'static, { @@ -141,6 +141,7 @@ where #[tokio::test] async fn test_pbh_transaction_transfer_full_flow() -> anyhow::Result<()> { + // TODO: Read from env so it can pass in CI let secrets: String = std::fs::read_to_string("tests/sepolia_secrets.json")?; let secret: serde_json::Value = serde_json::from_str(&secrets)?; let nullifier = secret["nullifier"].as_str().unwrap(); @@ -157,8 +158,8 @@ async fn test_pbh_transaction_transfer_full_flow() -> anyhow::Result<()> { .wallet(owner_signer.clone()) .connect_http(rpc_url); - // 7) Install mocked HTTP client that routes calls to Anvil - let client = AnvilBackedHttpClient { + // 7) HTTP client that routes calls to Alchemy + let client = AlchemyBackedHttpClient { provider: provider.clone(), }; let _ = set_http_client(Arc::new(client)); From d2dc7f940c296362d71618f205aecfe3d67cda26 Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Wed, 3 Sep 2025 14:09:26 -0700 Subject: [PATCH 29/34] remove prints --- bedrock/src/primitives/world_id.rs | 7 +------ bedrock/tests/test_smart_pbh_sepolia.rs | 3 --- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/bedrock/src/primitives/world_id.rs b/bedrock/src/primitives/world_id.rs index 74c96d4a..7bb2edb1 100644 --- a/bedrock/src/primitives/world_id.rs +++ b/bedrock/src/primitives/world_id.rs @@ -233,12 +233,7 @@ pub async fn find_unused_nullifier_hash( if signed_from_slice != I256::MINUS_ONE { let index = unsigned_value.to::(); let actual_nonce = batch_start + index as u16; - - println!("Found unused nullifier!"); - println!("Month: {current_month:?}"); - println!("Year: {current_year:?}"); - println!("Actual nonce: {actual_nonce:?}"); - + crate::info!("Found unused nullifier! Month: {current_month:?}, Year: {current_year:?}, Nonce: {actual_nonce:?}"); // Return the external nullifier for the found index return Ok(batch_external_nullifiers[index]); } diff --git a/bedrock/tests/test_smart_pbh_sepolia.rs b/bedrock/tests/test_smart_pbh_sepolia.rs index d735423b..407fb8d8 100644 --- a/bedrock/tests/test_smart_pbh_sepolia.rs +++ b/bedrock/tests/test_smart_pbh_sepolia.rs @@ -111,9 +111,6 @@ where } // Forward all other methods to the actual provider _ => { - println!("method: {method}"); - println!("params: {params}"); - // Forward the JSON-RPC request to the provider let response = self .provider From 9f94cdc11961e0dacb473534ee808620c0d4306a Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Wed, 3 Sep 2025 14:18:43 -0700 Subject: [PATCH 30/34] clippy --- bedrock/src/primitives/world_id.rs | 19 ++++++++++++ bedrock/src/smart_account/transaction_4337.rs | 2 +- bedrock/src/transaction/mod.rs | 30 ++----------------- bedrock/src/transaction/rpc.rs | 2 ++ bedrock/tests/test_smart_account_transfer.rs | 3 +- bedrock/tests/test_smart_pbh_sepolia.rs | 2 -- 6 files changed, 26 insertions(+), 32 deletions(-) diff --git a/bedrock/src/primitives/world_id.rs b/bedrock/src/primitives/world_id.rs index 7bb2edb1..ca5aad1f 100644 --- a/bedrock/src/primitives/world_id.rs +++ b/bedrock/src/primitives/world_id.rs @@ -119,6 +119,15 @@ pub fn is_world_id_identity_initialized() -> bool { } /// Generates a PBH proof for a given user operation. +/// # Arguments +/// - `user_op`: The user operation to generate the PBH proof for. +/// - `network`: The network to generate the PBH proof for. +/// # Errors +/// - Will throw a `WorldIdError` if the identity is not initialized. +/// - Will throw a `WorldIdError` if the network is invalid. +/// - Will throw a `WorldIdError` if the RPC client is not initialized. +/// - Will throw a `WorldIdError` if the inclusion proof fails. +/// - Will throw a `WorldIdError` if the unable to find a unused nullifier hash. pub async fn generate_pbh_proof( user_op: UserOperation, network: Network, @@ -166,6 +175,12 @@ pub async fn generate_pbh_proof( } /// Finds the first unused nullifier hash for the current World ID identity in batches +/// # Arguments +/// - `network`: The network to find the unused nullifier hash on. +/// # Errors +/// - Will throw a `WorldIdError` if the identity is not initialized. +/// - Will throw a `WorldIdError` if the RPC client is not initialized. +/// - Will throw a `WorldIdError` if the network is invalid. pub async fn find_unused_nullifier_hash( network: Network, ) -> Result { @@ -176,7 +191,9 @@ pub async fn find_unused_nullifier_hash( .map_err(|_| WorldIdError::RpcError(RpcError::HttpClientNotInitialized))?; let now = Utc::now(); + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] let current_year = now.year() as u16; + #[allow(clippy::cast_possible_truncation)] let current_month = now.month() as u16; let max_nonce = match network { @@ -193,6 +210,7 @@ pub async fn find_unused_nullifier_hash( // Generate a batch of nullifier hashes for nonce in batch_start..batch_end { + #[allow(clippy::cast_possible_truncation)] let external_nullifier = ExternalNullifier::v1(current_month as u8, current_year, nonce); let encoded_external_nullifier = @@ -232,6 +250,7 @@ pub async fn find_unused_nullifier_hash( // If result is not -1, we found an unused nullifier hash if signed_from_slice != I256::MINUS_ONE { let index = unsigned_value.to::(); + #[allow(clippy::cast_possible_truncation)] let actual_nonce = batch_start + index as u16; crate::info!("Found unused nullifier! Month: {current_month:?}, Year: {current_year:?}, Nonce: {actual_nonce:?}"); // Return the external nullifier for the found index diff --git a/bedrock/src/smart_account/transaction_4337.rs b/bedrock/src/smart_account/transaction_4337.rs index 0a2f1d30..4d5c8977 100644 --- a/bedrock/src/smart_account/transaction_4337.rs +++ b/bedrock/src/smart_account/transaction_4337.rs @@ -179,7 +179,7 @@ pub trait Is4337Encodable { Err(e) => { // TODO: Send standard user operation if PBH logic fails at any point return Err(RpcError::InvalidRequest(format!( - "Failed to generate PBH payload{e}" + "Failed to generate PBH payload {e}" ))); } } diff --git a/bedrock/src/transaction/mod.rs b/bedrock/src/transaction/mod.rs index c5216aff..ca69eb14 100644 --- a/bedrock/src/transaction/mod.rs +++ b/bedrock/src/transaction/mod.rs @@ -24,38 +24,15 @@ pub enum TransactionError { /// Extensions to `SafeSmartAccount` to enable high-level APIs for transactions. #[bedrock_export] impl SafeSmartAccount { - /// Allows executing an ERC-20 token transfer **on World Chain**. + /// Allows executing an ERC-20 token transfer /// /// # Arguments + /// - `network`: The network to transfer the tokens on. /// - `token_address`: The address of the ERC-20 token to transfer. /// - `to_address`: The address of the recipient. /// - `amount`: The amount of tokens to transfer as a stringified integer with the decimals of the token (e.g. 18 for USDC or WLD) + /// - `pbh`: Whether to use PBH. /// - `transfer_association`: Metadata value. The association of the transfer. - /// - /// # Example - /// - /// ```rust,no_run - /// use bedrock::smart_account::SafeSmartAccount; - /// use bedrock::transaction::TransactionError; - /// use bedrock::primitives::Network; - /// - /// # async fn example() -> Result<(), TransactionError> { - /// // Assume we have a configured SafeSmartAccount - /// # let safe_account = SafeSmartAccount::new("test_key".to_string(), "0x1234567890123456789012345678901234567890").unwrap(); - /// - /// // Transfer USDC on World Chain - /// let tx_hash = safe_account.transaction_transfer( - /// "0x79A02482A880BCE3F13E09Da970dC34DB4cD24d1", // USDC on World Chain - /// "0x1234567890123456789012345678901234567890", - /// "1000000", // 1 USDC (6 decimals) - /// None, - /// ).await?; - /// - /// println!("Transaction hash: {}", tx_hash.to_hex_string()); - /// # Ok(()) - /// # } - /// ``` - /// /// # Errors /// - Will throw a parsing error if any of the provided attributes are invalid. /// - Will throw an RPC error if the transaction submission fails. @@ -67,7 +44,6 @@ impl SafeSmartAccount { to_address: &str, amount: &str, pbh: bool, - _provider: RpcProviderName, transfer_association: Option, ) -> Result { let token_address = Address::parse_from_ffi(token_address, "token_address")?; diff --git a/bedrock/src/transaction/rpc.rs b/bedrock/src/transaction/rpc.rs index 475da7b5..894abea8 100644 --- a/bedrock/src/transaction/rpc.rs +++ b/bedrock/src/transaction/rpc.rs @@ -369,6 +369,8 @@ impl RpcClient { /// - `method`: JSON-RPC method to invoke /// - `params`: JSON-RPC params (typed) /// - `provider`: selected 4337 provider to include in headers + /// # Errors + /// - Will throw an RPC error if the RPC call fails. pub async fn eth_call( &self, network: Network, diff --git a/bedrock/tests/test_smart_account_transfer.rs b/bedrock/tests/test_smart_account_transfer.rs index e8680afa..5f193ea9 100644 --- a/bedrock/tests/test_smart_account_transfer.rs +++ b/bedrock/tests/test_smart_account_transfer.rs @@ -16,7 +16,7 @@ use bedrock::{ Network, }, smart_account::{SafeSmartAccount, ENTRYPOINT_4337}, - transaction::{foreign::UnparsedUserOperation, RpcProviderName}, + transaction::foreign::UnparsedUserOperation, }; use serde::Serialize; @@ -302,7 +302,6 @@ async fn test_transaction_transfer_full_flow_executes_user_operation_non_pbh( &recipient.to_string(), amount, false, - RpcProviderName::Alchemy, None, ) .await diff --git a/bedrock/tests/test_smart_pbh_sepolia.rs b/bedrock/tests/test_smart_pbh_sepolia.rs index 407fb8d8..ee973d7a 100644 --- a/bedrock/tests/test_smart_pbh_sepolia.rs +++ b/bedrock/tests/test_smart_pbh_sepolia.rs @@ -16,7 +16,6 @@ use bedrock::{ Network, }, smart_account::SafeSmartAccount, - transaction::RpcProviderName, }; use semaphore_rs::identity::Identity; @@ -179,7 +178,6 @@ async fn test_pbh_transaction_transfer_full_flow() -> anyhow::Result<()> { recipient, amount, true, - RpcProviderName::Alchemy, None, ) .await From dc981c1102f660bd238ffe26c60de4266c015042 Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Wed, 3 Sep 2025 14:51:53 -0700 Subject: [PATCH 31/34] clippy --- bedrock/src/primitives/world_id.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bedrock/src/primitives/world_id.rs b/bedrock/src/primitives/world_id.rs index ca5aad1f..87dd7560 100644 --- a/bedrock/src/primitives/world_id.rs +++ b/bedrock/src/primitives/world_id.rs @@ -59,7 +59,7 @@ pub struct InclusionProof { #[crate::bedrock_error] #[derive(Debug, Deserialize)] pub enum WorldIdError { - /// WorldID identity has not been initialized + /// Identity has not been initialized #[error("WorldID identity not initialized. Call set_world_id_identity() first.")] WorldIdIdentityNotInitialized, /// Failed to fetch inclusion proof from sequencer From 640dab8d87fdfb4493795f4e14eeb17b3f8de541 Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Wed, 3 Sep 2025 15:01:53 -0700 Subject: [PATCH 32/34] move secrets to env --- .env.example | 10 +++++++++- bedrock/tests/common.rs | 2 +- bedrock/tests/test_smart_pbh_sepolia.rs | 23 +++++++++++++---------- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/.env.example b/.env.example index ff0a5a8f..f0e4ef0f 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,9 @@ -WORLDCHAIN_RPC_URL=https://worldchain-mainnet.g.alchemy.com/public # substitute with your own RPC in case of rate limits \ No newline at end of file +WORLDCHAIN_RPC_URL=https://worldchain-mainnet.g.alchemy.com/public # substitute with your own RPC in case of rate limits + +# E2E +WORLDCHAIN_RPC_URL_SEPOLIA= +TESTNET_NULLIFIER= +TESTNET_TRAPDOOR= +# No 0x +TESTNET_PRIVATE_KEY= +TESTNET_SAFE_ADDRESS= \ No newline at end of file diff --git a/bedrock/tests/common.rs b/bedrock/tests/common.rs index d7c624bd..2f0f8605 100644 --- a/bedrock/tests/common.rs +++ b/bedrock/tests/common.rs @@ -113,7 +113,7 @@ pub const SAFE_MODULE_SETUP_ADDRESS: Address = #[allow(dead_code)] // this is extensively used in Integration Tests pub fn setup_anvil() -> AnvilInstance { dotenvy::dotenv().ok(); - let rpc_url = std::env::var("WORLDCHAIN_RPC_URL").unwrap_or_else(|_| { + let rpc_url: String = std::env::var("WORLDCHAIN_RPC_URL").unwrap_or_else(|_| { // Fallback to a public, no-key RPC if available. "https://worldchain-mainnet.g.alchemy.com/v2/demo".to_string() }); diff --git a/bedrock/tests/test_smart_pbh_sepolia.rs b/bedrock/tests/test_smart_pbh_sepolia.rs index ee973d7a..3b3104ed 100644 --- a/bedrock/tests/test_smart_pbh_sepolia.rs +++ b/bedrock/tests/test_smart_pbh_sepolia.rs @@ -7,6 +7,8 @@ use alloy::{ }; use reqwest::Url; +use dotenvy::dotenv; + use bedrock::{ primitives::{ http_client::{ @@ -137,14 +139,15 @@ where #[tokio::test] async fn test_pbh_transaction_transfer_full_flow() -> anyhow::Result<()> { - // TODO: Read from env so it can pass in CI - let secrets: String = std::fs::read_to_string("tests/sepolia_secrets.json")?; - let secret: serde_json::Value = serde_json::from_str(&secrets)?; - let nullifier = secret["nullifier"].as_str().unwrap(); - let trapdoor = secret["trapdoor"].as_str().unwrap(); - let private_key = secret["private_key"].as_str().unwrap(); - let safe_address = secret["safe_address"].as_str().unwrap(); - let rpc_url: Url = secret["rpc_url"].as_str().unwrap().parse()?; + dotenv().ok(); + + let rpc_url: Url = std::env::var("WORLDCHAIN_RPC_URL_SEPOLIA") + .unwrap() + .parse()?; + let nullifier = std::env::var("TESTNET_NULLIFIER").unwrap(); + let trapdoor = std::env::var("TESTNET_TRAPDOOR").unwrap(); + let private_key = std::env::var("TESTNET_PRIVATE_KEY").unwrap(); + let safe_address = std::env::var("TESTNET_SAFE_ADDRESS").unwrap(); let owner_signer = PrivateKeySigner::from_slice(&hex::decode(private_key)?)?; @@ -167,7 +170,7 @@ async fn test_pbh_transaction_transfer_full_flow() -> anyhow::Result<()> { set_world_id_identity(Arc::new(identity)); // 8) Execute high-level transfer via transaction_transfer - let safe_account = SafeSmartAccount::new(owner_key_hex, safe_address)?; + let safe_account = SafeSmartAccount::new(owner_key_hex, safe_address.as_str())?; let amount = "1"; let recipient = safe_address; @@ -175,7 +178,7 @@ async fn test_pbh_transaction_transfer_full_flow() -> anyhow::Result<()> { .transaction_transfer( Network::WorldChainSepolia, "0xC82Ea35634BcE95C394B6BC00626f827bB0F4801", // WORLD SEPOLIA LINK TOKEN - recipient, + recipient.as_str(), amount, true, None, From 9f6f17050477a60f3616d40d916c6e2346657c3f Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Wed, 3 Sep 2025 15:02:24 -0700 Subject: [PATCH 33/34] cleanup --- bedrock/src/transaction/rpc.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/bedrock/src/transaction/rpc.rs b/bedrock/src/transaction/rpc.rs index 894abea8..754b4a6f 100644 --- a/bedrock/src/transaction/rpc.rs +++ b/bedrock/src/transaction/rpc.rs @@ -377,16 +377,6 @@ impl RpcClient { to: Address, data: Bytes, ) -> Result { - // var raw = JSON.stringify({ - // "method": "eth_call", - // "params": [ - // { - // "to": "0x6b175474e89094c44da98b954eedeac495271d0f", - // "data": "0x70a082310000000000000000000000006E0d01A76C3Cf4288372a29124A26D4353EE51BE" - // }, - // "latest", Block number or "latest" - // }) - let params = vec![ serde_json::Value::Object(Map::from_iter([ ( From 9f87aae5e82ee78be348b3ea6e5e256ac91be9e9 Mon Sep 17 00:00:00 2001 From: Karan Kurbur Date: Wed, 3 Sep 2025 15:03:55 -0700 Subject: [PATCH 34/34] no unused imports --- bedrock/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bedrock/src/lib.rs b/bedrock/src/lib.rs index 01e769c9..0a91287f 100644 --- a/bedrock/src/lib.rs +++ b/bedrock/src/lib.rs @@ -3,7 +3,8 @@ clippy::pedantic, clippy::nursery, missing_docs, - dead_code + dead_code, + unused_imports )] //! `bedrock` is the foundational library which powers World App's crypto wallet