From 990aaf28a02f0aa3ffade9151770a9278a3ce7c6 Mon Sep 17 00:00:00 2001 From: Michael Montour Date: Tue, 12 Nov 2024 13:56:39 -0800 Subject: [PATCH] feat: Adds support for v0.7 AA contracts. (#13) * feat: Adds support for v0.7 AA contracts. Updates protocol version to 0.3 in offchain-RPC calls, updates Python examples. Other example / SDK repositories will also need to be updated. Applies some audit recommendations to HCHelper. * fix: "cargo fmt" cleanup Changes to be committed: modified: crates/builder/src/bundle_proposer.rs modified: crates/rpc/src/eth/api.rs modified: crates/rpc/src/eth/router.rs modified: crates/types/src/hybrid_compute.rs modified: crates/types/src/user_operation/mod.rs modified: crates/types/src/user_operation/v0_6.rs modified: crates/types/src/user_operation/v0_7.rs * fix: Use OpenZeppelin Ownable and ReentrancyGuard for HCHelper as suggested by audit. The index of the ResponseCache storage slot is now detected rather than being hard-coded. Changes to be committed: modified: bin/rundler/src/cli/mod.rs modified: crates/rpc/src/eth/api.rs modified: crates/rpc/src/eth/router.rs modified: crates/types/contracts/src/hc0_7/HCHelper.sol modified: crates/types/src/hybrid_compute.rs * fix: "cargo fmt" Changes to be committed: modified: bin/rundler/src/cli/mod.rs modified: crates/rpc/src/eth/api.rs modified: crates/types/src/hybrid_compute.rs * fix: some pylint cleanup. Changes to be committed: modified: aa_utils/__init__.py modified: offchain/offchain_utils.py * fix: default to v0.7 in deploy-local.py Changes to be committed: modified: deploy-local.py * fix: sync the TestHybrid contract with the documetation. Changes to be committed: modified: ../crates/types/contracts/src/hc0_7/TestHybrid.sol * fix: force the EntryPoint in deployer script, fund ha0 acct directly. Changes to be committed: modified: hc_scripts/LocalDeploy_v7.s.sol --- Cargo.lock | 1 + bin/rundler/src/cli/mod.rs | 23 +- crates/builder/src/bundle_proposer.rs | 24 +- crates/provider/src/ethers/provider.rs | 10 +- crates/rpc/src/eth/api.rs | 146 +++--- crates/rpc/src/eth/router.rs | 3 +- crates/rpc/src/types/v0_7.rs | 8 +- crates/types/Cargo.toml | 1 + crates/types/build.rs | 21 +- ...pleDeploy.s.sol => ExampleDeploy_v6.s.sol} | 0 .../hc_scripts/ExampleDeploy_v7.s.sol | 37 ++ ...LocalDeploy.s.sol => LocalDeploy_v6.s.sol} | 0 .../contracts/hc_scripts/LocalDeploy_v7.s.sol | 79 ++++ crates/types/contracts/src/hc0_7/HCHelper.sol | 200 ++++++++ .../contracts/src/hc0_7/HybridAccount.sol | 173 +++++++ .../src/hc0_7/HybridAccountFactory.sol | 54 +++ .../contracts/src/hc0_7/TestAuctionSystem.sol | 103 +++++ .../types/contracts/src/hc0_7/TestCaptcha.sol | 72 +++ .../types/contracts/src/hc0_7/TestHybrid.sol | 108 +++++ crates/types/contracts/src/hc0_7/TestKyc.sol | 41 ++ .../src/hc0_7/TestRainfallInsurance.sol | 117 +++++ .../contracts/src/hc0_7/TestSportsBetting.sol | 114 +++++ .../contracts/src/hc0_7/TestTokenPrice.sol | 35 ++ crates/types/contracts/src/v0_7/imports.sol | 1 + crates/types/src/hybrid_compute.rs | 433 ++++++++++++------ crates/types/src/user_operation/mod.rs | 24 +- crates/types/src/user_operation/v0_6.rs | 9 +- crates/types/src/user_operation/v0_7.rs | 30 +- hybrid-compute/aa-client.py | 4 +- hybrid-compute/aa_utils/__init__.py | 86 +++- hybrid-compute/deploy-local.py | 115 ++++- hybrid-compute/local.env | 1 - .../offchain/add_sub_2/add_sub_2_offchain.py | 6 +- .../offchain/add_sub_2/add_sub_2_test.py | 6 +- .../auction_system/auction_system_offchain.py | 6 +- .../auction_system/auction_system_test.py | 6 +- .../offchain/check_kyc/check_kyc_offchain.py | 6 +- .../offchain/check_kyc/check_kyc_test.py | 4 +- .../get_token_price_offchain.py | 6 +- .../get_token_price/get_token_price_test.py | 2 +- hybrid-compute/offchain/offchain_utils.py | 67 ++- .../rainfall_insurance_offchain.py | 6 +- .../rainfall_insurance_test.py | 4 +- .../offchain/ramble/ramble_offchain.py | 6 +- hybrid-compute/offchain/ramble/ramble_test.py | 2 +- .../sports_betting/sports_betting_offchain.py | 5 +- .../sports_betting/sports_betting_test.py | 8 +- hybrid-compute/offchain/userop.py | 9 +- hybrid-compute/offchain/userop_utils.py | 7 +- .../verify_captcha/captcha_offchain.py | 7 +- .../offchain/verify_captcha/captcha_test.py | 2 +- hybrid-compute/runit.sh | 2 +- 52 files changed, 1913 insertions(+), 327 deletions(-) rename crates/types/contracts/hc_scripts/{ExampleDeploy.s.sol => ExampleDeploy_v6.s.sol} (100%) create mode 100644 crates/types/contracts/hc_scripts/ExampleDeploy_v7.s.sol rename crates/types/contracts/hc_scripts/{LocalDeploy.s.sol => LocalDeploy_v6.s.sol} (100%) create mode 100644 crates/types/contracts/hc_scripts/LocalDeploy_v7.s.sol create mode 100644 crates/types/contracts/src/hc0_7/HCHelper.sol create mode 100644 crates/types/contracts/src/hc0_7/HybridAccount.sol create mode 100644 crates/types/contracts/src/hc0_7/HybridAccountFactory.sol create mode 100644 crates/types/contracts/src/hc0_7/TestAuctionSystem.sol create mode 100644 crates/types/contracts/src/hc0_7/TestCaptcha.sol create mode 100644 crates/types/contracts/src/hc0_7/TestHybrid.sol create mode 100644 crates/types/contracts/src/hc0_7/TestKyc.sol create mode 100644 crates/types/contracts/src/hc0_7/TestRainfallInsurance.sol create mode 100644 crates/types/contracts/src/hc0_7/TestSportsBetting.sol create mode 100644 crates/types/contracts/src/hc0_7/TestTokenPrice.sol diff --git a/Cargo.lock b/Cargo.lock index 98a8720b..25ebf3f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4452,6 +4452,7 @@ dependencies = [ "serde_json", "strum 0.26.1", "thiserror", + "tokio", ] [[package]] diff --git a/bin/rundler/src/cli/mod.rs b/bin/rundler/src/cli/mod.rs index 1564f84c..005f9dcf 100644 --- a/bin/rundler/src/cli/mod.rs +++ b/bin/rundler/src/cli/mod.rs @@ -32,7 +32,7 @@ use rundler_rpc::{EthApiSettings, RundlerApiSettings}; use rundler_sim::{ EstimationSettings, PrecheckSettings, PriorityFeeMode, SimulationSettings, MIN_CALL_GAS_LIMIT, }; -use rundler_types::hybrid_compute; +use rundler_types::{contracts::v0_7::hc_helper::HCHelper, hybrid_compute}; /// Main entry point for the CLI /// @@ -53,19 +53,26 @@ pub async fn run() -> anyhow::Result<()> { let cs = chain_spec::resolve_chain_spec(&opt.common.network, &opt.common.chain_spec); tracing::info!("Chain spec: {:#?}", cs); + let node_http = opt + .common + .node_http + .clone() + .expect("must provide node_http"); + let p2 = rundler_provider::new_provider(&node_http, None)?; + let hx = HCHelper::new(opt.common.hc_helper_addr, p2); + let slot_idx = hx + .response_slot() + .await + .expect("Failed to get ResponseSlot"); hybrid_compute::init( opt.common.hc_helper_addr, opt.common.hc_sys_account, opt.common.hc_sys_owner, opt.common.hc_sys_privkey, - //opt.common.entry_points[0].parse::
().expect("Must provide an entry_point"), - cs.entry_point_address_v0_6, - cs.id, - opt.common - .node_http - .clone() - .expect("Must provide node_http"), + cs.clone(), + node_http, + slot_idx, ); match opt.command { diff --git a/crates/builder/src/bundle_proposer.rs b/crates/builder/src/bundle_proposer.rs index 25519364..12a8c07d 100644 --- a/crates/builder/src/bundle_proposer.rs +++ b/crates/builder/src/bundle_proposer.rs @@ -530,6 +530,7 @@ where let mut gas_spent = self.settings.chain_spec.transaction_intrinsic_gas; let mut cleanup_keys: Vec = Vec::new(); let mut constructed_bundle_size = BUNDLE_BYTE_OVERHEAD; + for (po, simulation) in ops_with_simulations { let op = po.clone().uo; let simulation = match simulation { @@ -637,7 +638,11 @@ where if hc_ent.is_some() { gas_spent += hc_ent.clone().unwrap().oc_gas; //println!("HC insert, hc_ent {:?}", hc_ent); - let u_op2: UserOperationVariant = hc_ent.clone().unwrap().user_op.into(); + let u_op2: UserOperationVariant = hc_ent + .clone() + .unwrap() + .user_op + .into_variant(&self.settings.chain_spec); let sim_result = self .simulator @@ -679,16 +684,23 @@ where .get_nonce(cfg.sys_account, U256::zero()) .await .unwrap(); - let cleanup_op: UserOperationVariant = - hybrid_compute::rr_op(&cfg, c_nonce, cleanup_keys) - .await - .into(); + let is_v7 = + self.entry_point.address() == self.settings.chain_spec.entry_point_address_v0_7; + let cleanup_op: UserOperationVariant = hybrid_compute::rr_op( + &cfg, + self.entry_point.address(), + c_nonce, + cleanup_keys, + is_v7, + ) + .await + .into_variant(&self.settings.chain_spec); let cleanup_sim = self .simulator .simulate_validation(cleanup_op.clone().into(), None, None) .await - .expect("Failed to unwrap sim_result"); // FIXME + .expect("Failed to unwrap sim_result"); context .groups_by_aggregator diff --git a/crates/provider/src/ethers/provider.rs b/crates/provider/src/ethers/provider.rs index 7242feb0..b17f24ce 100644 --- a/crates/provider/src/ethers/provider.rs +++ b/crates/provider/src/ethers/provider.rs @@ -141,10 +141,10 @@ impl Provider for EthersProvider { block_id: Option, trace_options: GethDebugTracingCallOptions, ) -> ProviderResult { - println!( - "HC debug_trace_call overrides {:?} tx {:?}", - trace_options.state_overrides, tx - ); + //println!( + // "HC debug_trace_call overrides {:?}", + // trace_options.state_overrides + //); println!("HC will use BlockNumber::Latest instead of {:?}", block_id); let ret = Middleware::debug_trace_call( self, @@ -153,7 +153,7 @@ impl Provider for EthersProvider { trace_options, ) .await; - println!("HC debug_trace_call ret {:?}", ret); + //println!("HC debug_trace_call ret {:?}", ret); Ok(ret?) } diff --git a/crates/rpc/src/eth/api.rs b/crates/rpc/src/eth/api.rs index 12466dcc..c38c25b3 100644 --- a/crates/rpc/src/eth/api.rs +++ b/crates/rpc/src/eth/api.rs @@ -24,7 +24,7 @@ use jsonrpsee::{ }; use rundler_types::{ chain::ChainSpec, - contracts::v0_6::{hc_helper::HCHelper as HH2, simple_account::SimpleAccount}, + contracts::{v0_6::simple_account::SimpleAccount, v0_7::hc_helper::HCHelper}, hybrid_compute, pool::Pool, UserOperation, UserOperationOptionalGas, UserOperationVariant, @@ -156,22 +156,19 @@ where // max_simulate_handle_ops_gas: 0, // validation_estimation_gas_fee: 0, //}; - let op6: rundler_types::v0_6::UserOperationOptionalGas = op.clone().into(); - let hh = op6 - .clone() - .into_user_operation(U256::from(0), U256::from(0)) - .hc_hash(); + let hh = op.hc_hash(); println!("HC api.rs hh {:?}", hh); + let op_t: UserOperationVariant = op.into_variant(&self.chain_spec); - let ep_addr = hybrid_compute::hc_ep_addr(revert_data); + let ep_addr = hybrid_compute::hc_ha_addr(revert_data); - let n_key: U256 = op6.nonce >> 64; - let at_price = op6.max_priority_fee_per_gas; - //let hc_nonce = context.gas_estimator.entry_point.get_nonce(op6.sender, n_key).await.unwrap(); + let n_key: U256 = op_t.nonce() >> 64; + let at_price = Some(op_t.max_priority_fee_per_gas()); + //let hc_nonce = context.gas_estimator.entry_point.get_nonce(op_t.sender(), n_key).await.unwrap(); let hc_nonce = self .router - .get_nonce(&entry_point, op6.sender, n_key) + .get_nonce(&entry_point, op_t.sender(), n_key) .await .unwrap(); @@ -182,11 +179,14 @@ where .unwrap(); println!( "HC hc_nonce {:?} err_nonce {:?} op_nonce {:?} n_key {:?}", - hc_nonce, err_nonce, op6.nonce, n_key + hc_nonce, + err_nonce, + op_t.nonce(), + n_key ); let p2 = rundler_provider::new_provider(&self.hc.node_http, None)?; - let hx = HH2::new(self.hc.helper_addr, p2.clone()); + let hx = HCHelper::new(self.hc.helper_addr, p2.clone()); let url = hx.registered_callers(ep_addr).await.expect("url_decode").1; println!("HC registered_caller url {:?}", url); @@ -206,9 +206,10 @@ where let payload = hex::encode(hybrid_compute::hc_req_payload(revert_data)); let n_bytes: [u8; 32] = (hc_nonce).into(); let src_n = hex::encode(n_bytes); - let src_addr = hex::encode(op6.sender); + let src_addr = hex::encode(op_t.sender()); + + let oo_n_key: U256 = U256::from_big_endian(op_t.sender().as_fixed_bytes()); - let oo_n_key: U256 = U256::from_big_endian(op6.sender.as_fixed_bytes()); let oo_nonce = self .router .get_nonce(&entry_point, ep_addr, oo_n_key) @@ -222,10 +223,31 @@ where "Failed to look up HybridAccount owner" ))); } - const REQ_VERSION: &str = "0.2"; + // This version parameter tells the offchain RPC which version of the + // AA contracts are being used. This affects the hashing algorithm needed + // to generate an offchain signature. + const REQ_VERSION_V6: &str = "0.2"; + const REQ_VERSION_V7: &str = "0.3"; + + let is_v7 = match self.router.get_ep_version(&entry_point)? { + rundler_types::EntryPointVersion::V0_7 => true, + rundler_types::EntryPointVersion::V0_6 => false, + rundler_types::EntryPointVersion::Unspecified => { + return Err(EthRpcError::Internal(anyhow::anyhow!( + "HC04: Unknown EntryPoint version" + ))) + } + }; let mut params = ObjectParams::new(); - let _ = params.insert("ver", REQ_VERSION); + let _ = params.insert( + "ver", + if is_v7 { + REQ_VERSION_V7 + } else { + REQ_VERSION_V6 + }, + ); let _ = params.insert("sk", sk_hex); let _ = params.insert("src_addr", src_addr); let _ = params.insert("src_nonce", src_n); @@ -250,11 +272,11 @@ where let resp_hex = resp["response"].as_str().unwrap(); let sig_hex: String = resp["signature"].as_str().unwrap().into(); let hc_res: Bytes = hex::decode(resp_hex).unwrap().into(); - //println!("HC api.rs do_op result sk {:?} success {:?} res {:?}", sub_key, op_success, hc_res); err_hc = hybrid_compute::external_op( + entry_point, hh, - op6.sender, + op_t.sender(), hc_nonce, op_success, &hc_res, @@ -266,6 +288,7 @@ where &self.hc, ha_owner.unwrap(), err_nonce, + is_v7, ) .await; } else { @@ -329,11 +352,12 @@ where entry_point, err_hc.clone(), sub_key, - op6.sender, + op_t.sender(), hc_nonce, err_nonce, map_key, &self.hc, + is_v7, ) .await; } @@ -349,31 +373,19 @@ where println!("HC api.rs Ok gas result2 = {:?}", result2); let r3a = result2.unwrap(); match r3a { - RpcGasEstimate::V0_6(abc) => { - r3 = abc; + RpcGasEstimate::V0_6(est) => { + r3 = est; } - _ => { - return Err(EthRpcError::Internal(anyhow::anyhow!( - "HC04 user_op gas estimation failed" - ))); + RpcGasEstimate::V0_7(est) => { + r3 = RpcGasEstimateV0_6 { + pre_verification_gas: est.pre_verification_gas, + call_gas_limit: est.call_gas_limit, + verification_gas_limit: est.verification_gas_limit, + }; } } - let op_tmp = hybrid_compute::get_hc_ent(hh).unwrap().user_op; - let op_tmp_2: rundler_types::v0_6::UserOperationOptionalGas = - rundler_types::v0_6::UserOperationOptionalGas { - sender: op_tmp.sender, - nonce: op_tmp.nonce, - init_code: op_tmp.init_code, - call_data: op_tmp.call_data, - call_gas_limit: Some(op_tmp.call_gas_limit), - verification_gas_limit: Some(op_tmp.verification_gas_limit), - pre_verification_gas: Some(op_tmp.pre_verification_gas), - max_fee_per_gas: Some(op_tmp.max_fee_per_gas), - max_priority_fee_per_gas: Some(op_tmp.max_priority_fee_per_gas), - paymaster_and_data: op_tmp.paymaster_and_data, - signature: op_tmp.signature, - }; + let op_tmp_2 = hybrid_compute::get_hc_ent(hh).unwrap().user_op; // The op_tmp_2 below specifies a 0 gas price, but we need to estimate the L1 fee at the // price offered by real userOperation which will be paying for it. @@ -382,7 +394,7 @@ where .router .estimate_gas( &entry_point, - rundler_types::UserOperationOptionalGas::V0_6(op_tmp_2.clone()), + op_tmp_2.clone(), Some(spoof::State::default()), at_price, ) @@ -396,17 +408,17 @@ where }; let r2: RpcGasEstimateV0_6 = match r2a? { - RpcGasEstimate::V0_6(estimate) => estimate, - _ => { - return Err(EthRpcError::Internal(anyhow::anyhow!( - "HC04 offchain_op gas estimation failed" - ))); - } + RpcGasEstimate::V0_6(est) => est, + RpcGasEstimate::V0_7(est) => RpcGasEstimateV0_6 { + pre_verification_gas: est.pre_verification_gas, + call_gas_limit: est.call_gas_limit, + verification_gas_limit: est.verification_gas_limit, + }, }; // The current formula used to estimate gas usage in the offchain_rpc service // sometimes underestimates the true cost. For now all we can do is error here. - if r2.call_gas_limit > op_tmp_2.call_gas_limit.unwrap() { + if r2.call_gas_limit > op_tmp_2.into_variant(&self.chain_spec).call_gas_limit() { println!("HC op_tmp_2 failed, call_gas_limit too low"); let msg = "HC04: Offchain call_gas_limit too low".to_string(); return Err(EthRpcError::Internal(anyhow::anyhow!(msg))); @@ -421,38 +433,28 @@ where .get_nonce(&entry_point, self.hc.sys_account, U256::zero()) .await .unwrap(); - let cleanup_op = hybrid_compute::rr_op(&self.hc, c_nonce, cleanup_keys.clone()).await; - let op_tmp_4: rundler_types::v0_6::UserOperationOptionalGas = - rundler_types::v0_6::UserOperationOptionalGas { - sender: cleanup_op.sender, - nonce: cleanup_op.nonce, - init_code: cleanup_op.init_code, - call_data: cleanup_op.call_data, - call_gas_limit: Some(cleanup_op.call_gas_limit), - verification_gas_limit: Some(cleanup_op.verification_gas_limit), - pre_verification_gas: Some(cleanup_op.pre_verification_gas), - max_fee_per_gas: Some(cleanup_op.max_fee_per_gas), - max_priority_fee_per_gas: Some(cleanup_op.max_priority_fee_per_gas), - paymaster_and_data: cleanup_op.paymaster_and_data, - signature: cleanup_op.signature, - }; - println!("HC op_tmp_4 {:?} {:?}", op_tmp_4, cleanup_keys); + let cleanup_op = + hybrid_compute::rr_op(&self.hc, entry_point, c_nonce, cleanup_keys.clone(), is_v7) + .await; + + println!("HC cleanup_op {:?} {:?}", cleanup_op, cleanup_keys); let r4a = self .router .estimate_gas( &entry_point, - rundler_types::UserOperationOptionalGas::V0_6(op_tmp_4), + // rundler_types::UserOperationOptionalGas::V0_6(op_tmp_4), + cleanup_op, Some(spoof::State::default()), at_price, ) .await; let r4: RpcGasEstimateV0_6 = match r4a? { - RpcGasEstimate::V0_6(estimate) => estimate, - _ => { - return Err(EthRpcError::Internal(anyhow::anyhow!( - "HC04 cleanup_op gas estimation failed" - ))); - } + RpcGasEstimate::V0_6(est) => est, + RpcGasEstimate::V0_7(est) => RpcGasEstimateV0_6 { + pre_verification_gas: est.pre_verification_gas, + call_gas_limit: est.call_gas_limit, + verification_gas_limit: est.verification_gas_limit, + }, }; let cleanup_gas = diff --git a/crates/rpc/src/eth/router.rs b/crates/rpc/src/eth/router.rs index d2a7146e..cf0dbf2d 100644 --- a/crates/rpc/src/eth/router.rs +++ b/crates/rpc/src/eth/router.rs @@ -198,7 +198,6 @@ impl EntryPointRouter { .get_nonce(addr, key) .await .map_err(Into::into) - //Ok(U256::from(0)) } pub(crate) async fn check_signature( @@ -213,7 +212,7 @@ impl EntryPointRouter { .map_err(Into::into) } - fn get_ep_version(&self, entry_point: &Address) -> EthResult { + pub(crate) fn get_ep_version(&self, entry_point: &Address) -> EthResult { if let Some((addr, _)) = self.v0_6 { if addr == *entry_point { return Ok(EntryPointVersion::V0_6); diff --git a/crates/rpc/src/types/v0_7.rs b/crates/rpc/src/types/v0_7.rs index ccd9ced6..7808c1bb 100644 --- a/crates/rpc/src/types/v0_7.rs +++ b/crates/rpc/src/types/v0_7.rs @@ -176,10 +176,10 @@ impl From for UserOperationOptionalGas { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub(crate) struct RpcGasEstimate { - pre_verification_gas: U256, - call_gas_limit: U256, - verification_gas_limit: U256, - paymaster_verification_gas_limit: Option, + pub(crate) pre_verification_gas: U256, + pub(crate) call_gas_limit: U256, + pub(crate) verification_gas_limit: U256, + pub(crate) paymaster_verification_gas_limit: Option, } impl From for RpcGasEstimate { diff --git a/crates/types/Cargo.toml b/crates/types/Cargo.toml index a9904f9e..d0929b4b 100644 --- a/crates/types/Cargo.toml +++ b/crates/types/Cargo.toml @@ -32,6 +32,7 @@ ethers.workspace = true [dev-dependencies] rundler-types = { path = ".", features = ["test-utils"] } +tokio.workspace = true # For hybrid compute testing. [features] test-utils = [ "mockall" ] diff --git a/crates/types/build.rs b/crates/types/build.rs index 3a854f83..56fc658f 100644 --- a/crates/types/build.rs +++ b/crates/types/build.rs @@ -38,7 +38,7 @@ fn generate_v0_6_bindings() -> Result<(), Box> { // hybrid compute run_command( - forge_build("../lib/account-abstraction-versions/v0_6/contracts") + forge_build("hc0_6") .arg("--remappings") .arg("@openzeppelin/=lib/openzeppelin-contracts-versions/v4_9") .arg("--remappings") @@ -57,8 +57,9 @@ fn generate_v0_6_bindings() -> Result<(), Box> { abigen_of("v0_6", "VerifyingPaymaster")?, abigen_of("v0_6", "CallGasEstimationProxy")?, // hybrid compute - abigen_of("v0_6", "INonceManager")?, - abigen_of("v0_6", "HCHelper")?, + abigen_of("hc0_6", "INonceManager")?, + abigen_of("hc0_6", "HCHelper")?, + abigen_of("hc0_6", "HybridAccount")?, ]) .build()? .write_to_module("src/contracts/v0_6", false)?; @@ -75,6 +76,16 @@ fn generate_v0_7_bindings() -> Result<(), Box> { "generate ABIs", )?; + run_command( + forge_build("hc0_7") + .arg("--remappings") + .arg("@openzeppelin/=lib/openzeppelin-contracts-versions/v5_0") + .arg("--remappings") + .arg("@gnosis.pm/safe-contracts=lib/safe-smart-account"), + "https://getfoundry.sh/", + "generate ABIs", + )?; + MultiAbigen::from_abigens([ abigen_of("v0_7", "IEntryPoint")?, abigen_of("v0_7", "IAccount")?, @@ -85,6 +96,10 @@ fn generate_v0_7_bindings() -> Result<(), Box> { abigen_of("v0_7", "EntryPointSimulations")?, abigen_of("v0_7", "CallGasEstimationProxy")?, abigen_of("v0_7", "SenderCreator")?, + // hybrid compute + abigen_of("hc0_7", "INonceManager")?, + abigen_of("hc0_7", "HCHelper")?, + abigen_of("hc0_7", "HybridAccount")?, ]) .build()? .write_to_module("src/contracts/v0_7", false)?; diff --git a/crates/types/contracts/hc_scripts/ExampleDeploy.s.sol b/crates/types/contracts/hc_scripts/ExampleDeploy_v6.s.sol similarity index 100% rename from crates/types/contracts/hc_scripts/ExampleDeploy.s.sol rename to crates/types/contracts/hc_scripts/ExampleDeploy_v6.s.sol diff --git a/crates/types/contracts/hc_scripts/ExampleDeploy_v7.s.sol b/crates/types/contracts/hc_scripts/ExampleDeploy_v7.s.sol new file mode 100644 index 00000000..95ec612f --- /dev/null +++ b/crates/types/contracts/hc_scripts/ExampleDeploy_v7.s.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +import "forge-std/Script.sol"; +import "src/hc0_7/HybridAccount.sol"; +import "src/hc0_7/TestAuctionSystem.sol"; +import "src/hc0_7/TestCaptcha.sol"; +import "src/hc0_7/TestHybrid.sol"; +import "src/hc0_7/TestRainfallInsurance.sol"; +import "src/hc0_7/TestSportsBetting.sol"; +import "src/hc0_7/TestKyc.sol"; +import "src/hc0_7/TestTokenPrice.sol"; + +contract LocalDeploy is Script { + function run() external + returns (address[7] memory) { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + + address payable ha1Addr = payable(vm.envAddress("OC_HYBRID_ACCOUNT")); + HybridAccount ha1; + + address[7] memory ret; + + vm.startBroadcast(deployerPrivateKey); + + ret[0] = address(new AuctionFactory(ha1Addr)); + ret[1] = address(new TestCaptcha(ha1Addr)); + ret[2] = address(new TestHybrid(ha1Addr)); + ret[3] = address(new RainfallInsurance(ha1Addr)); + ret[4] = address(new SportsBetting(ha1Addr)); + ret[5] = address(new TestKyc(ha1Addr)); + ret[6] = address(new TestTokenPrice(ha1Addr)); + + vm.stopBroadcast(); + return ret; + } +} diff --git a/crates/types/contracts/hc_scripts/LocalDeploy.s.sol b/crates/types/contracts/hc_scripts/LocalDeploy_v6.s.sol similarity index 100% rename from crates/types/contracts/hc_scripts/LocalDeploy.s.sol rename to crates/types/contracts/hc_scripts/LocalDeploy_v6.s.sol diff --git a/crates/types/contracts/hc_scripts/LocalDeploy_v7.s.sol b/crates/types/contracts/hc_scripts/LocalDeploy_v7.s.sol new file mode 100644 index 00000000..6364214b --- /dev/null +++ b/crates/types/contracts/hc_scripts/LocalDeploy_v7.s.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +import "forge-std/Script.sol"; +import "lib/account-abstraction-versions/v0_7/contracts/core/EntryPoint.sol"; +import "src/hc0_7/HCHelper.sol"; +import "src/hc0_7/HybridAccountFactory.sol"; +import "lib/account-abstraction-versions/v0_7/contracts/samples/SimpleAccountFactory.sol"; + +contract LocalDeploy is Script { + function run() external + returns (address[5] memory) { + address deployAddr = vm.envAddress("DEPLOY_ADDR"); + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address hcSysOwner = vm.envAddress("HC_SYS_OWNER"); + require (hcSysOwner != address(0), "HC_SYS_OWNER not set"); + uint256 deploySalt = vm.envOr("DEPLOY_SALT",uint256(0)); // Change this to force redeployment of contracts + + address bobaAddr = vm.envOr("BOBA_TOKEN", 0x4200000000000000000000000000000000000023); + + EntryPoint ept; + HCHelper helper; + SimpleAccountFactory saf; + HybridAccountFactory haf; + HybridAccount ha0; + + bytes32 salt_val = bytes32(deploySalt); + uint112 min_deposit = 0.001 ether; + + vm.startBroadcast(deployerPrivateKey); + + // EntryPointAddr is hard-coded for the v0.7 implementation + ept = EntryPoint(payable(0x0000000071727De22E5E9d8BAf0edAc6f37da032)); + + { + address helperAddr = vm.envOr("HC_HELPER_ADDR", 0x0000000000000000000000000000000000000000); + if (helperAddr != address(0) && helperAddr.code.length > 0) { + helper = HCHelper(helperAddr); + } else { + helper = new HCHelper{salt: salt_val}(address(ept), bobaAddr, deployAddr); + } + } + { + address safAddr = vm.envOr("SA_FACTORY_ADDR", 0x0000000000000000000000000000000000000000); + if (safAddr != address(0) && safAddr.code.length > 0) { + saf = SimpleAccountFactory(safAddr); + } else { + saf = new SimpleAccountFactory(ept); + } + } + { + address hafAddr = vm.envOr("HA_FACTORY_ADDR", 0x0000000000000000000000000000000000000000); + if (hafAddr != address(0) && hafAddr.code.length > 0) { + haf = HybridAccountFactory(hafAddr); + } else { + haf = new HybridAccountFactory(ept, address(helper)); + } + } + { + address ha0Addr = vm.envOr("HC_SYS_ACCOUNT", 0x0000000000000000000000000000000000000000); + if (ha0Addr != address(0) && ha0Addr.code.length > 0) { + ha0 = HybridAccount(payable(ha0Addr)); + } else { + ha0 = haf.createAccount(hcSysOwner,0); + } + } + if (helper.systemAccount() != address(ha0)) { + helper.SetSystemAccount(address(ha0)); + } + + // Previous version deposited to EntryPoint, here we fund the acct directly + if (address(ha0).balance < min_deposit) { + payable(address(ha0)).transfer(min_deposit - address(ha0).balance); + } + + vm.stopBroadcast(); + return [address(ept),address(helper), address(saf), address(haf), address(ha0)]; + } +} diff --git a/crates/types/contracts/src/hc0_7/HCHelper.sol b/crates/types/contracts/src/hc0_7/HCHelper.sol new file mode 100644 index 00000000..ca74ebe1 --- /dev/null +++ b/crates/types/contracts/src/hc0_7/HCHelper.sol @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.12; + +import "account-abstraction/v0_7/interfaces/INonceManager.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract HCHelper is ReentrancyGuard, Ownable { + using SafeERC20 for IERC20; + + event SystemAccountSet(address oldAccount, address newAccount); + event RegisteredUrl(address contract_addr, string url); + event TokenWithdrawal(address withdrawTo, uint256 amount); + + // Response data is stored here by PutResponse() and then consumed by TryCallOffchain(). + mapping(bytes32=>bytes) ResponseCache; + + // BOBA token address + address public tokenAddr; + + // Token amount required to purchase each prepaid credit (may be 0 for testing) + uint256 public pricePerCall; + + // Account which is used to insert system error responses. Currently a single + // address but could be extended to a list of authorized accounts if needed. + address public systemAccount; + + // Data stored per RegisteredCaller + struct callerInfo { + address owner; + string url; + uint256 credits; + } + + // Contracts which are allowed to use Hybrid Compute. + mapping(address=>callerInfo) public RegisteredCallers; + + // AA EntryPoint + address immutable entryPoint; + + // Constructor + constructor(address _entryPoint, address _tokenAddr, address _owner) Ownable(_owner) { + entryPoint = _entryPoint; + tokenAddr = _tokenAddr; + } + + // Change the SystemAccount address (used for error responses) + function SetSystemAccount(address _systemAccount) public onlyOwner { + emit SystemAccountSet(systemAccount, _systemAccount); + systemAccount = _systemAccount; + } + + // Temporary method, until an auto-registration protocol is developed. + function RegisterUrl(address contract_addr, string calldata url) public onlyOwner { + require(bytes(url).length > 0, "URL cannot be empty"); + RegisteredCallers[contract_addr].owner = msg.sender; + RegisteredCallers[contract_addr].url = url; + emit RegisteredUrl(contract_addr, url); + } + + // Set or change the per-call token price (0 is allowed). Does not affect + // existing credit balances, only applies to new AddCredit() calls. + function SetPrice(uint256 _pricePerCall) public onlyOwner { + pricePerCall = _pricePerCall; + } + + // Purchase credits allowing the specified contract to perform HC calls. + // The token cost is (pricePerCall() * numCredits) and is non-refundable + function AddCredit(address contract_addr, uint256 numCredits) public nonReentrant { + uint256 tokenPrice = numCredits * pricePerCall; + RegisteredCallers[contract_addr].credits += numCredits; + IERC20(tokenAddr).safeTransferFrom(msg.sender, address(this), tokenPrice); + } + + // Allow the owner to withdraw tokens + function WithdrawTokens(uint256 amount, address withdrawTo) public onlyOwner nonReentrant { + emit TokenWithdrawal(withdrawTo, amount); + IERC20(tokenAddr).safeTransfer(withdrawTo, amount); + } + + // Called from a HybridAccount contract, to populate the response which it will + // subsequently request in TryCallOffchain() + function PutResponse(bytes32 subKey, bytes calldata response) public { + require(RegisteredCallers[msg.sender].owner != address(0), "Unregistered caller"); + require(response.length % 32 == 0, "Response not properly encoded"); + require(response.length >= 32*4, "Response too short"); + + (,, uint32 errCode,) = abi.decode(response,(address, uint256, uint32, bytes)); + require(errCode < 2, "invalid errCode for PutResponse()"); + + bytes32 mapKey = keccak256(abi.encodePacked(msg.sender, subKey)); + ResponseCache[mapKey] = response; + } + + // Allow the system to supply an error response for unsuccessful requests. + // Any such response will only be retrieved if there was nothing supplied + // by PutResponse() + function PutSysResponse(bytes32 subKey, bytes calldata response) public { + require(msg.sender == systemAccount, "Only systemAccount may call PutSysResponse"); + require(response.length >= 32*4, "Response too short"); + + (,, uint32 errCode,) = abi.decode(response,(address, uint256, uint32, bytes)); + require(errCode >= 2, "PutSysResponse() may only be used for error responses"); + + bytes32 mapKey = keccak256(abi.encodePacked(address(this), subKey)); + ResponseCache[mapKey] = response; + } + + // Remove one or more map entries (only needed if response was not retrieved normally). + function RemoveResponses(bytes32[] calldata mapKeys) public { + require(msg.sender == systemAccount, "Only systemAccount may call RemoveResponses"); + for (uint32 i = 0; i < mapKeys.length; i++) { + delete(ResponseCache[mapKeys[i]]); + } + } + + // Try to retrieve an entry, also removing it from the mapping. This + // function will check for stale entries by checking the nonce of the srcAccount. + // Stale entries will return a "not found" condition. + function getEntry(bytes32 mapKey) internal returns (bool, uint32, bytes memory) { + bytes memory entry; + bool found; + uint32 errCode; + bytes memory response; + address srcAddr; + uint256 srcNonce; + + entry = ResponseCache[mapKey]; + delete(ResponseCache[mapKey]); + + if (entry.length == 1) { + // Used during state simulation to verify that a trigger request actually came from this helper contract + revert("_HC_VRFY"); + } else if (entry.length != 0) { + found = true; + (srcAddr, srcNonce, errCode, response) = abi.decode(entry,(address, uint256, uint32, bytes)); + uint192 nonceKey = uint192(srcNonce >> 64); + + INonceManager NM = INonceManager(entryPoint); + uint256 actualNonce = NM.getNonce(srcAddr, nonceKey); + + if (srcNonce + 1 != actualNonce) { + // stale entry + found = false; + errCode = 0; + response = "0x"; + } + } + return (found, errCode, response); + } + + // Make an offchain call to a pre-registered endpoint. + function TryCallOffchain(bytes32 userKey, bytes memory req) public returns (uint32, bytes memory) { + bool found; + uint32 errCode; + bytes memory ret; + + require(RegisteredCallers[msg.sender].owner != address(0), "Calling contract not registered"); + require(req.length % 32 == 4, "Request must be ABI-encoded with selector"); + + if (RegisteredCallers[msg.sender].credits == 0) { + return (5, "Insufficient credit"); + } + RegisteredCallers[msg.sender].credits -= 1; + + bytes32 subKey = keccak256(abi.encodePacked(userKey, req)); + bytes32 mapKey = keccak256(abi.encodePacked(msg.sender, subKey)); + + (found, errCode, ret) = getEntry(mapKey); + + if (found) { + return (errCode, ret); + } else { + // If no off-chain response, check for a system error response. + bytes32 errKey = keccak256(abi.encodePacked(address(this), subKey)); + + (found, errCode, ret) = getEntry(errKey); + if (found) { + require(errCode >= 2, "invalid errCode"); + return (errCode, ret); + } else { + // Nothing found, so trigger a new request. + bytes memory prefix = "_HC_TRIG"; + bytes memory r2 = bytes.concat(prefix, abi.encodePacked(msg.sender, userKey, req)); + assembly { + revert(add(r2, 32), mload(r2)) + } + } + } + } + + // Returns the slot index of ResponseCache, needed by the bundler. + // This index is affected by the OpenZeppelin libraries like Ownable. + function ResponseSlot() public pure returns (uint256 ret) { + assembly { + ret := ResponseCache.slot + } + } +} diff --git a/crates/types/contracts/src/hc0_7/HybridAccount.sol b/crates/types/contracts/src/hc0_7/HybridAccount.sol new file mode 100644 index 00000000..0faaf5cc --- /dev/null +++ b/crates/types/contracts/src/hc0_7/HybridAccount.sol @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; + +/* solhint-disable avoid-low-level-calls */ +/* solhint-disable no-inline-assembly */ +/* solhint-disable reason-string */ + +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; +import "account-abstraction/v0_7/core/BaseAccount.sol"; +import "account-abstraction/v0_7/core/Helpers.sol"; +import "account-abstraction/v0_7/samples/callback/TokenCallbackHandler.sol"; + +interface IHCHelper { + function TryCallOffchain(bytes32, bytes memory) external returns (uint32, bytes memory); +} + +/** + * minimal account. + * this is sample minimal account. + * has execute, eth handling methods + * has a single signer that can send requests through the entryPoint. + */ +contract HybridAccount is BaseAccount, TokenCallbackHandler, UUPSUpgradeable, Initializable { + mapping(address=>bool) public PermittedCallers; + + address public owner; + + IEntryPoint private immutable _entryPoint; + address public immutable _helperAddr; + + event HybridAccountInitialized(IEntryPoint indexed entryPoint, address indexed owner); + + modifier onlyOwner() { + _onlyOwner(); + _; + } + + /// @inheritdoc BaseAccount + function entryPoint() public view virtual override returns (IEntryPoint) { + return _entryPoint; + } + + // solhint-disable-next-line no-empty-blocks + receive() external payable {} + + constructor(IEntryPoint anEntryPoint, address helperAddr) { + _entryPoint = anEntryPoint; + _helperAddr = helperAddr; + _disableInitializers(); + } + + function _onlyOwner() internal view { + //directly from EOA owner, or through the account itself (which gets redirected through execute()) + require(msg.sender == owner || msg.sender == address(this), "only owner"); + } + + /** + * execute a transaction (called directly from owner, or by entryPoint) + * @param dest destination address to call + * @param value the value to pass in this call + * @param func the calldata to pass in this call + */ + function execute(address dest, uint256 value, bytes calldata func) external { + _requireFromEntryPointOrOwner(); + _call(dest, value, func); + } + + /** + * execute a sequence of transactions + * @dev to reduce gas consumption for trivial case (no value), use a zero-length array to mean zero value + * @param dest an array of destination addresses + * @param value an array of values to pass to each call. can be zero-length for no-value calls + * @param func an array of calldata to pass to each call + */ + function executeBatch(address[] calldata dest, uint256[] calldata value, bytes[] calldata func) external { + _requireFromEntryPointOrOwner(); + require(dest.length == func.length && (value.length == 0 || value.length == func.length), "wrong array lengths"); + if (value.length == 0) { + for (uint256 i = 0; i < dest.length; i++) { + _call(dest[i], 0, func[i]); + } + } else { + for (uint256 i = 0; i < dest.length; i++) { + _call(dest[i], value[i], func[i]); + } + } + } + + /** + * @dev The _entryPoint member is immutable, to reduce gas consumption. To upgrade EntryPoint, + * a new implementation of HybridAccount must be deployed with the new EntryPoint address, then upgrading + * the implementation by calling `upgradeTo()` + * @param anOwner the owner (signer) of this account + */ + function initialize(address anOwner) public virtual initializer { + _initialize(anOwner); + } + + function _initialize(address anOwner) internal virtual { + owner = anOwner; + emit HybridAccountInitialized(_entryPoint, owner); + } + + // Require the function call went through EntryPoint or owner + function _requireFromEntryPointOrOwner() internal view { + require(msg.sender == address(entryPoint()) || msg.sender == owner, "account: not Owner or EntryPoint"); + } + + /// implement template method of BaseAccount + function _validateSignature(PackedUserOperation calldata userOp, bytes32 userOpHash) + internal override virtual returns (uint256 validationData) { + bytes32 hash = MessageHashUtils.toEthSignedMessageHash(userOpHash); + if (owner != ECDSA.recover(hash, userOp.signature)) + return SIG_VALIDATION_FAILED; + return SIG_VALIDATION_SUCCESS; + } + + function _call(address target, uint256 value, bytes memory data) internal { + (bool success, bytes memory result) = target.call{value: value}(data); + if (!success) { + assembly { + revert(add(result, 32), mload(result)) + } + } + } + + /** + * check current account deposit in the entryPoint + */ + function getDeposit() public view returns (uint256) { + return entryPoint().balanceOf(address(this)); + } + + /** + * deposit more funds for this account in the entryPoint + */ + function addDeposit() public payable { + entryPoint().depositTo{value: msg.value}(address(this)); + } + + /** + * withdraw value from the account's deposit + * @param withdrawAddress target to send to + * @param amount to withdraw + */ + function withdrawDepositTo(address payable withdrawAddress, uint256 amount) public onlyOwner { + entryPoint().withdrawTo(withdrawAddress, amount); + } + + function _authorizeUpgrade(address newImplementation) internal view override { + (newImplementation); + _onlyOwner(); + } + + function PermitCaller(address caller, bool allowed) public { + _requireFromEntryPointOrOwner(); + PermittedCallers[caller] = allowed; + } + + function CallOffchain(bytes32 userKey, bytes memory req) public returns (uint32, bytes memory) { + /* By default a simple whitelist is used. Endpoint implementations may choose to allow + unrestricted access, to use a custom permission model, to charge fees, etc. */ + require(_helperAddr != address(0), "Helper address not set"); + require(PermittedCallers[msg.sender], "Permission denied"); + IHCHelper HC = IHCHelper(_helperAddr); + + userKey = keccak256(abi.encodePacked(userKey, msg.sender)); + return HC.TryCallOffchain(userKey, req); + } +} diff --git a/crates/types/contracts/src/hc0_7/HybridAccountFactory.sol b/crates/types/contracts/src/hc0_7/HybridAccountFactory.sol new file mode 100644 index 00000000..3c0cf280 --- /dev/null +++ b/crates/types/contracts/src/hc0_7/HybridAccountFactory.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "@openzeppelin/contracts/utils/Create2.sol"; +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +import "./HybridAccount.sol"; + +/** + * A sample factory contract for HybridAccount + * A UserOperations "initCode" holds the address of the factory, and a method call (to createAccount, in this sample factory). + * The factory's createAccount returns the target account address even if it is already installed. + * This way, the entryPoint.getSenderAddress() can be called either before or after the account is created. + */ +contract HybridAccountFactory { + HybridAccount public immutable accountImplementation; + address public Helper; + + constructor(IEntryPoint _entryPoint, address _helper) { + accountImplementation = new HybridAccount(_entryPoint, _helper); + Helper = _helper; + } + + /** + * create an account, and return its address. + * returns the address even if the account is already deployed. + * Note that during UserOperation execution, this method is called only if the account is not deployed. + * This method returns an existing account address so that entryPoint.getSenderAddress() would work even after account creation + */ + function createAccount(address owner,uint256 salt) public returns (HybridAccount ret) { + address addr = getAddress(owner, salt); + uint codeSize = addr.code.length; + if (codeSize > 0) { + return HybridAccount(payable(addr)); + } + ret = HybridAccount(payable(new ERC1967Proxy{salt : bytes32(salt)}( + address(accountImplementation), + abi.encodeCall(HybridAccount.initialize, (owner)) + ))); + } + + /** + * calculate the counterfactual address of this account as it would be returned by createAccount() + */ + function getAddress(address owner,uint256 salt) public view returns (address) { + return Create2.computeAddress(bytes32(salt), keccak256(abi.encodePacked( + type(ERC1967Proxy).creationCode, + abi.encode( + address(accountImplementation), + abi.encodeCall(HybridAccount.initialize, (owner)) + ) + ))); + } +} diff --git a/crates/types/contracts/src/hc0_7/TestAuctionSystem.sol b/crates/types/contracts/src/hc0_7/TestAuctionSystem.sol new file mode 100644 index 00000000..15a446f3 --- /dev/null +++ b/crates/types/contracts/src/hc0_7/TestAuctionSystem.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./HybridAccount.sol"; + +contract AuctionFactory { + uint256 public auctionCount = 0; + mapping(uint256 => Auction) public auctions; + + event AuctionCreated(uint256 auctionId, address auctionAddress); + event AuctionEnded(uint256 auctionId, address winner, uint256 amount); + + struct Auction { + address highestBidder; + uint256 highestBid; + uint256 endTime; + address payable beneficiary; + bool ended; + } + + address payable immutable helperAddr; + + constructor(address payable _helperAddr) { + helperAddr = _helperAddr; + } + + modifier auctionExists(uint256 auctionId) { + require(auctions[auctionId].beneficiary != address(0), "Auction does not exist"); + _; + } + + function createAuction(uint256 _biddingTime, address payable _beneficiary) public { + auctionCount++; + auctions[auctionCount] = Auction({ + highestBidder: address(0), + highestBid: 0, + endTime: block.timestamp + _biddingTime, + beneficiary: _beneficiary, + ended: false + }); + emit AuctionCreated(auctionCount, address(this)); + } + + function bid(uint256 auctionId) public payable auctionExists(auctionId) { + Auction storage auction = auctions[auctionId]; + require(block.timestamp < auction.endTime, "Auction already ended."); + require(msg.value > auction.highestBid, "There already is a higher bid."); + require(verifyBidder(), "Bidder not verified."); + + if (auction.highestBidder != address(0)) { + payable(auction.highestBidder).transfer(auction.highestBid); + } + + auction.highestBidder = msg.sender; + auction.highestBid = msg.value; + } + + function verifyBidder() private returns (bool) { + HybridAccount ha = HybridAccount(helperAddr); + + bytes memory req = abi.encodeWithSignature( + "verifyBidder(address)", + msg.sender + ); + bytes32 userKey = bytes32(abi.encode(msg.sender)); + (uint32 error, bytes memory ret) = ha.CallOffchain(userKey, req); + + if (error != 0) { + revert(string(ret)); + } + + bool isVerified; + (isVerified) = abi.decode(ret, (bool)); + return isVerified; + } + + function endAuction(uint256 auctionId) public auctionExists(auctionId) { + Auction storage auction = auctions[auctionId]; + require(block.timestamp >= auction.endTime, "Auction not yet ended."); + require(!auction.ended, "Auction end already called."); + + auction.ended = true; + emit AuctionEnded(auctionId, auction.highestBidder, auction.highestBid); + + auction.beneficiary.transfer(auction.highestBid); + } + + function getHighestBid(uint256 auctionId) public view auctionExists(auctionId) returns (uint256) { + return auctions[auctionId].highestBid; + } + + function getHighestBidder(uint256 auctionId) public view auctionExists(auctionId) returns (address) { + return auctions[auctionId].highestBidder; + } + + function getAuctionEndTime(uint256 auctionId) public view auctionExists(auctionId) returns (uint256) { + return auctions[auctionId].endTime; + } + + function isAuctionEnded(uint256 auctionId) public view auctionExists(auctionId) returns (bool) { + return auctions[auctionId].ended; + } +} diff --git a/crates/types/contracts/src/hc0_7/TestCaptcha.sol b/crates/types/contracts/src/hc0_7/TestCaptcha.sol new file mode 100644 index 00000000..69868406 --- /dev/null +++ b/crates/types/contracts/src/hc0_7/TestCaptcha.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "./HybridAccount.sol"; + +contract TestCaptcha is Ownable { + address payable immutable helperAddr; + uint256 constant public nativeFaucetAmount = 0.01 ether; + uint256 constant public waitingPeriod = 1 days; + IERC20 public token; + + mapping(address => uint256) public claimRecords; + + uint256 private constant SAFE_GAS_STIPEND = 6000; + + constructor(address payable _helperAddr) Ownable(msg.sender) { + helperAddr = _helperAddr; + } + + event Withdraw(address receiver, uint256 nativeAmount); + + receive() external payable {} + + function withdraw(uint256 _nativeAmount) public onlyOwner { + (bool sent, ) = msg.sender.call{ + gas: SAFE_GAS_STIPEND, + value: _nativeAmount + }(""); + require(sent, "Failed to send native Ether"); + + emit Withdraw(msg.sender, _nativeAmount); + } + + function verifyCaptcha( + address _to, + bytes32 _uuid, + string memory _key + ) private returns (bool) { + HybridAccount ha = HybridAccount(helperAddr); + + bytes memory req = abi.encodeWithSignature( + "verifyCaptcha(string,string,string)", + _to, + _uuid, + _key + ); + bytes32 userKey = bytes32(abi.encode(msg.sender)); + (uint32 error, bytes memory ret) = ha.CallOffchain(userKey, req); + + if (error != 0) { + revert(string(ret)); + } + + bool isVerified; + (isVerified) = abi.decode(ret, (bool)); + return isVerified; + } + + function getTestnetETH( + bytes32 _uuid, + string memory _key, + address _to) external { + require(claimRecords[_to] + waitingPeriod <= block.timestamp, 'Invalid request'); + require(verifyCaptcha(_to, _uuid, _key), "Invalid captcha"); + claimRecords[_to] = block.timestamp; + + (bool sent,) = (_to).call{gas: SAFE_GAS_STIPEND, value: nativeFaucetAmount}(""); + require(sent, "Failed to send native"); + } +} diff --git a/crates/types/contracts/src/hc0_7/TestHybrid.sol b/crates/types/contracts/src/hc0_7/TestHybrid.sol new file mode 100644 index 00000000..85ecc36a --- /dev/null +++ b/crates/types/contracts/src/hc0_7/TestHybrid.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; + +import "./HybridAccount.sol"; + +contract TestHybrid { + mapping(address => uint256) public counters; + + address payable immutable hcAccount; + + event CalledFrom(address sender); + + constructor(address payable _hcAccount) { + hcAccount = _hcAccount; + } + + // helper method to waste gas + // repeat - waste gas on writing storage in a loop + // junk - dynamic buffer to stress the function size. + mapping(uint256 => uint256) public xxx; + uint256 public offset; + + function gasWaster(uint256 repeat, string calldata /*junk*/) external { + for (uint256 i = 1; i <= repeat; i++) { + offset++; + xxx[offset] = i; + } + } + + function count(uint32 a, uint32 b) public { + HybridAccount HA = HybridAccount(hcAccount); + uint256 x; + uint256 y; + if (b == 0) { + counters[msg.sender] = counters[msg.sender] + a; + return; + } + bytes memory req = abi.encodeWithSignature("addsub2(uint32,uint32)", a, b); + bytes32 userKey = bytes32(abi.encode(msg.sender)); + (uint32 error, bytes memory ret) = HA.CallOffchain(userKey, req); + + if (error == 0) { + (x,y) = abi.decode(ret,(uint256,uint256)); // x=(a+b), y=(a-b) + + this.gasWaster(x, "abcd1234"); + counters[msg.sender] = counters[msg.sender] + y; + } else if (b >= 10) { + revert(string(ret)); + } else if (error == 1) { + counters[msg.sender] = counters[msg.sender] + 100; + } else { + //revert(string(ret)); + counters[msg.sender] = counters[msg.sender] + 1000; + } + } + + function countFail() public pure { + revert("count failed"); + } + + function justemit() public { + emit CalledFrom(msg.sender); + } + + /* This example is a word-guessing game. The user picks a four-letter word as their guess, + and pays for the number of entries they wish to purchase. This wager is added to a pool. + The offchain provider generates a random array of words and returns it as a string[]. If + the user's guess appears in the list returned from the server then they win the entire pool. + + A boolean flag allows the user to cheat by guaranteeing that the word "frog" will appear + in the list. + */ + event GameResult(address indexed caller,uint256 indexed win, uint256 indexed Pool); + uint256 public constant EntryCost = 2 gwei; + uint256 public Pool = 0; + + function wordGuess(string calldata myGuess, bool cheat) public payable { + HybridAccount HA = HybridAccount(payable(hcAccount)); + uint256 entries = msg.value / EntryCost; + require(entries > 0, "No entries purchased"); + require(entries <= 100, "Excess payment"); + Pool += msg.value; + require(bytes(myGuess).length == 4, "Game uses 4-letter words"); + + bytes memory req = abi.encodeWithSignature("ramble(uint256,bool)", entries, cheat); + bytes32 userKey = bytes32(abi.encode(msg.sender)); + (uint32 error, bytes memory ret) = HA.CallOffchain(userKey, req); + if (error != 0) { + revert(string(ret)); + } + + uint256 win = 0; + string[] memory words = abi.decode(ret,(string[])); + + for (uint256 i=0; i Policy) public policies; + mapping(string => Rainfall) public currentRainfall; + uint256 public constant MULTIPLIER = 3; + address payable immutable helperAddr; + + uint256 private nonce; + + event PolicyCreated(uint256 indexed policyId, address indexed policyholder, string city, uint256 premium, uint256 payoutAmount); + + + constructor(address payable _helperAddr) { + helperAddr = _helperAddr; + } + + function generatePolicyId(address policyHolder, string memory city) internal returns (uint256) { + nonce++; + return uint256(keccak256(abi.encodePacked(policyHolder, city, block.timestamp, nonce))); + } + + function buyInsurance( + uint256 triggerRainfall, + string memory city + ) public payable returns (uint256){ + require(msg.value > 0, "Premium must be greater than zero"); + uint256 payoutAmount = msg.value * MULTIPLIER; + uint256 policyId = generatePolicyId(msg.sender, city); + + policies[policyId] = Policy( + msg.sender, + msg.value, + payoutAmount, + triggerRainfall, + city, + block.timestamp, + PolicyState.Active + ); + + emit PolicyCreated(policyId, msg.sender, city, msg.value, payoutAmount); + } + + function updateRainfall( + string memory city + ) internal returns (Rainfall storage) { + HybridAccount ha = HybridAccount(helperAddr); + + bytes memory req = abi.encodeWithSignature( + "get_rainfall(string)", + city + ); + bytes32 userKey = bytes32(abi.encode(msg.sender)); + (uint32 error, bytes memory ret) = ha.CallOffchain(userKey, req); + + if (error != 0) { + revert(string(ret)); + } + + uint256 rainfallInMm; + (rainfallInMm) = abi.decode(ret, (uint256)); + currentRainfall[city] = Rainfall(rainfallInMm, block.timestamp); + + return currentRainfall[city]; + } + + function checkAndPayout(uint256 policyId) public { + Policy storage policy = policies[policyId]; + require(policy.state == PolicyState.Active, "Policy is not active"); + + if (policy.timestamp + 365 days < block.timestamp) { + policy.state = PolicyState.Expired; + revert("Policy expired"); + } + + Rainfall storage rainfall = currentRainfall[policy.city]; + + if ( + rainfall.updatedAt == 0 || + rainfall.updatedAt + 24 hours < block.timestamp + ) { + rainfall = updateRainfall(policy.city); + } + + + require( + rainfall.rainfallInMm <= policy.triggerRainfall, + "Trigger condition not met" + ); + + policy.state = PolicyState.Claimed; + payable(policy.policyholder).transfer(policy.payoutAmount); + } +} diff --git a/crates/types/contracts/src/hc0_7/TestSportsBetting.sol b/crates/types/contracts/src/hc0_7/TestSportsBetting.sol new file mode 100644 index 00000000..a515e6c9 --- /dev/null +++ b/crates/types/contracts/src/hc0_7/TestSportsBetting.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./HybridAccount.sol"; + +contract SportsBetting { + address payable immutable helperAddr; + + constructor(address payable _helperAddr) { + helperAddr = _helperAddr; + } + + struct Bet { + address bettor; + uint256 amount; + uint256 outcome; // 1 for Team A win, 2 for Team B win, 3 for Draw + bool settled; + } + + struct Game { + uint256 gameId; + bool exists; + } + + mapping(uint256 => Game) public games; + mapping(uint256 => Bet[]) public bets; + mapping(uint256 => uint256) public gameScores; // 1 for Team A win, 2 for Team B win, 3 for Draw + + event GameCreated(uint256 indexed gameId); + event BetPlaced( + address indexed bettor, + uint256 indexed gameId, + uint256 amount, + uint256 outcome + ); + event BetSettled( + address indexed bettor, + uint256 indexed gameId, + uint256 outcome, + uint256 winnings + ); + event GameScoreUpdated(uint256 indexed gameId, uint256 score); + + function createGame(uint256 gameId) external returns (uint256) { + games[gameId] = Game({gameId: gameId, exists: true}); + + emit GameCreated(gameId); + return gameId; + } + + function placeBet(uint256 _gameId, uint256 _outcome) external payable { + require(msg.value > 0, "Bet amount must be greater than zero"); + require(_outcome >= 1 && _outcome <= 3, "Invalid outcome"); + require(games[_gameId].exists, "Game does not exist"); + + bets[_gameId].push( + Bet({ + bettor: msg.sender, + amount: msg.value, + outcome: _outcome, + settled: false + }) + ); + + emit BetPlaced(msg.sender, _gameId, msg.value, _outcome); + } + + function settleBet(uint256 _gameId) external { + require(games[_gameId].exists, "Game does not exist"); + + uint256 actualOutcome = updateGameScore(_gameId); + //uint256 actualOutcome = gameScores[_gameId]; + + for (uint256 i = 0; i < bets[_gameId].length; i++) { + Bet storage bet = bets[_gameId][i]; + if (!bet.settled) { + if (bet.outcome == actualOutcome) { + uint256 winnings = bet.amount * 2; // Here you could fetch the winning ratio from offchain to calculate the user's win. + payable(bet.bettor).transfer(winnings); + emit BetSettled( + bet.bettor, + _gameId, + actualOutcome, + winnings + ); + } + bet.settled = true; + } + } + } + + function updateGameScore(uint256 _gameId) internal returns (uint256) { + require(games[_gameId].exists, "Game does not exist"); + + HybridAccount ha = HybridAccount(helperAddr); + + bytes memory req = abi.encodeWithSignature( + "get_score(uint256)", + _gameId + ); + bytes32 userKey = bytes32(abi.encode(msg.sender)); + (uint32 error, bytes memory ret) = ha.CallOffchain(userKey, req); + + if (error != 0) { + revert(string(ret)); + } + + uint256 result; + (result) = abi.decode(ret, (uint256)); + gameScores[_gameId] = result; + emit GameScoreUpdated(_gameId, result); + return result; + } +} diff --git a/crates/types/contracts/src/hc0_7/TestTokenPrice.sol b/crates/types/contracts/src/hc0_7/TestTokenPrice.sol new file mode 100644 index 00000000..9dcbf397 --- /dev/null +++ b/crates/types/contracts/src/hc0_7/TestTokenPrice.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "./HybridAccount.sol"; + +contract TestTokenPrice { + mapping(uint256 => uint256) public counters; + address payable immutable helperAddr; + + event PriceQuote(string, string); + + constructor(address payable _helperAddr) { + helperAddr = _helperAddr; + counters[0] = 100; + } + + function fetchPrice( + string calldata token + ) public returns (string memory) { + HybridAccount ha = HybridAccount(payable(helperAddr)); + string memory price; + + bytes memory req = abi.encodeWithSignature("getprice(string)", token); + bytes32 userKey = bytes32(abi.encode(msg.sender)); + (uint32 error, bytes memory ret) = ha.CallOffchain(userKey, req); + + if (error != 0) { + revert(string(ret)); + } + + (price) = abi.decode(ret, (string)); + emit PriceQuote(token, price); + return price; + } +} diff --git a/crates/types/contracts/src/v0_7/imports.sol b/crates/types/contracts/src/v0_7/imports.sol index 8b094c63..d6c6f5af 100644 --- a/crates/types/contracts/src/v0_7/imports.sol +++ b/crates/types/contracts/src/v0_7/imports.sol @@ -10,3 +10,4 @@ import "account-abstraction/v0_7/interfaces/IAggregator.sol"; import "account-abstraction/v0_7/interfaces/IStakeManager.sol"; import "account-abstraction/v0_7/core/EntryPointSimulations.sol"; import "account-abstraction/v0_7/core/SenderCreator.sol"; +import "src/hc0_7/HCHelper.sol"; diff --git a/crates/types/src/hybrid_compute.rs b/crates/types/src/hybrid_compute.rs index 6129ff6a..07c473af 100644 --- a/crates/types/src/hybrid_compute.rs +++ b/crates/types/src/hybrid_compute.rs @@ -21,12 +21,19 @@ use std::{ use ethers::{ abi::{AbiDecode, AbiEncode}, signers::{LocalWallet, Signer}, - types::{Address, BigEndianHash, Bytes, RecoveryMessage::Data, H256, U256}, + types::{Address, BigEndianHash, Bytes, RecoveryMessage::Data, H256, U128, U256}, utils::keccak256, }; use once_cell::sync::Lazy; -use crate::{user_operation::UserOperation, v0_6::UserOperation as UserOperationV0_6}; +use crate::{ + chain::ChainSpec, + user_operation::{ + v0_6::UserOperationOptionalGas as UserOperationOptionalGasV0_6, + v0_7::UserOperationOptionalGas as UserOperationOptionalGasV0_7, UserOperation, + UserOperationOptionalGas, + }, +}; #[derive(Clone, Debug)] /// Error code @@ -44,16 +51,18 @@ pub struct HcEntry { pub sub_key: H256, /// Merged key, used for end-of-bundle cleanup pub map_key: H256, - /// Extracted calldata - //pub call_data: Bytes, + /// Extracted calldata (also in user_op; duplicated for easier access) + pub call_data: Bytes, /// Full operation - pub user_op: UserOperationV0_6, + pub user_op: UserOperationOptionalGas, /// Creation timestamp, used to prune expired entries pub ts: SystemTime, /// The total computed offchain gas (all 3 phases) pub oc_gas: U256, /// The required preVerificationGas incl. HC overhead (set during successful gas estimation) pub needed_pvg: U256, + /// Version flag + pub is_v7: bool, } const EXPIRE_SECS: std::time::Duration = Duration::new(120, 0); @@ -63,11 +72,12 @@ impl Clone for HcEntry { HcEntry { sub_key: self.sub_key, map_key: self.map_key, - //call_data: self.call_data.clone(), + call_data: self.call_data.clone(), user_op: self.user_op.clone(), ts: self.ts, oc_gas: self.oc_gas, needed_pvg: self.needed_pvg, + is_v7: self.is_v7, } } } @@ -77,7 +87,7 @@ static HC_MAP: Lazy>> = Lazy::new(|| Mutex::new(m) }); -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] /// Parameters needed for Hybrid Compute, accessed from various modules. pub struct HcCfg { /// Helper contract address @@ -88,14 +98,14 @@ pub struct HcCfg { pub sys_owner: Address, /// Private key for sys_account pub sys_privkey: H256, - /// EntryPoint contract address (currently only 1 EP is supported) - pub entry_point: Address, - /// Chain ID - pub chain_id: u64, + /// Chain spec + pub chain_spec: ChainSpec, /// Temporary workaround; would be better to use an existing Provider. pub node_http: String, /// Temporary workaround pub from_addr: Address, + /// Index of ResponseCache slot in HCHelper + pub slot_idx: U256, } //pub static mut HC_CONFIG: HcCfg = HcCfg { helper_addr:Address::zero(), sys_account:Address::zero(), sys_owner:Address::zero(), sys_privkey:H256::zero(), entry_point: Address::zero(), chain_id: 0, node_http:String::new(), from_addr: Address::zero()}; @@ -107,10 +117,10 @@ pub static HC_CONFIG: Lazy> = Lazy::new(|| { sys_account: Address::zero(), sys_owner: Address::zero(), sys_privkey: H256::zero(), - entry_point: Address::zero(), - chain_id: 0, + chain_spec: ChainSpec::default(), node_http: String::new(), from_addr: Address::zero(), + slot_idx: U256::from(0), }; Mutex::new(c) }); @@ -121,9 +131,9 @@ pub fn init( sys_account: Address, sys_owner: Address, sys_privkey: H256, - entry_point: Address, - chain_id: u64, + chain_spec: ChainSpec, node_http: String, + slot_idx: U256, ) { let mut cfg = HC_CONFIG.lock().unwrap(); @@ -131,9 +141,9 @@ pub fn init( cfg.sys_account = sys_account; cfg.sys_owner = sys_owner; cfg.sys_privkey = sys_privkey; - cfg.entry_point = entry_point; - cfg.chain_id = chain_id; + cfg.chain_spec = chain_spec; cfg.node_http.clone_from(&node_http); + cfg.slot_idx = slot_idx; } /// Set the EOA address which the bundler is using. Erigon, but not geth, needs this for tx simulation @@ -205,11 +215,11 @@ pub fn hc_map_key(revert_data: &Bytes) -> H256 { /// Calculates the HCHelper storage slot key for a ResponseCache entry pub fn hc_storage_key(map_key: H256) -> H256 { - let slot_idx = "0x0000000000000000000000000000000000000000000000000000000000000000" - .parse::() - .unwrap(); + let cfg = HC_CONFIG.lock().unwrap(); + let slot_idx_bytes: Bytes = cfg.slot_idx.encode().into(); + let storage_key: H256 = - keccak256([Bytes::from(map_key.to_fixed_bytes()), slot_idx].concat()).into(); + keccak256([Bytes::from(map_key.to_fixed_bytes()), slot_idx_bytes].concat()).into(); storage_key } @@ -220,7 +230,7 @@ pub fn hc_sub_key(revert_data: &Bytes) -> H256 { } /// Endpoint address (address of HybridAccount which called HCHelper) -pub fn hc_ep_addr(revert_data: &Bytes) -> Address { +pub fn hc_ha_addr(revert_data: &Bytes) -> Address { Address::from_slice(&revert_data[8..28]) } @@ -243,11 +253,12 @@ fn make_external_op( op_success: bool, response_payload: &Bytes, sub_key: H256, - ep_addr: Address, + ha_addr: Address, sig_hex: String, oo_nonce: U256, cfg: &HcCfg, -) -> UserOperationV0_6 { + is_v7: bool, +) -> (UserOperationOptionalGas, Bytes) { let tmp_bytes: Bytes = Bytes::from(response_payload.to_vec()); let err_code: u32 = if op_success { 0 } else { 1 }; @@ -267,56 +278,83 @@ fn make_external_op( call_gas, call_data ); + if is_v7 { + let mut new_op = UserOperationOptionalGasV0_7 { + sender: ha_addr, + nonce: oo_nonce, + call_data: call_data.clone(), + call_gas_limit: Some(U128::from(call_gas)), + verification_gas_limit: Some(U128::from(0x10000)), + pre_verification_gas: Some(U256::from(0x10000)), + max_fee_per_gas: Some(U128::zero()), + max_priority_fee_per_gas: Some(U128::zero()), + paymaster_data: Bytes::new(), + signature: Bytes::new(), + paymaster: None, + factory: None, + factory_data: Bytes::new(), + paymaster_verification_gas_limit: None, + paymaster_post_op_gas_limit: None, + }; - let mut new_op: UserOperationV0_6 = UserOperationV0_6 { - sender: ep_addr, - nonce: oo_nonce, - init_code: Bytes::new(), - call_data: call_data.clone(), - call_gas_limit: U256::from(call_gas), - verification_gas_limit: U256::from(0x10000), - pre_verification_gas: U256::from(0x10000), - max_fee_per_gas: U256::zero(), - max_priority_fee_per_gas: U256::zero(), - paymaster_and_data: Bytes::new(), - signature: Bytes::new(), - }; + new_op.signature = sig_hex.parse::().unwrap(); + (UserOperationOptionalGas::V0_7(new_op), call_data) + } else { + let mut new_op = UserOperationOptionalGasV0_6 { + sender: ha_addr, + nonce: oo_nonce, + init_code: Bytes::new(), + call_data: call_data.clone(), + call_gas_limit: Some(U256::from(call_gas)), + verification_gas_limit: Some(U256::from(0x10000)), + pre_verification_gas: Some(U256::from(0x10000)), + max_fee_per_gas: Some(U256::zero()), + max_priority_fee_per_gas: Some(U256::zero()), + paymaster_and_data: Bytes::new(), + signature: Bytes::new(), + }; - new_op.signature = sig_hex.parse::().unwrap(); + new_op.signature = sig_hex.parse::().unwrap(); - new_op + (UserOperationOptionalGas::V0_6(new_op), call_data) + } } /// Processes an external hybrid compute op. #[allow(clippy::too_many_arguments)] // FIXME later pub async fn external_op( + entry_point: Address, op_key: H256, src_addr: Address, nonce: U256, op_success: bool, response_payload: &Bytes, sub_key: H256, - ep_addr: Address, + ha_addr: Address, sig_hex: String, oo_nonce: U256, map_key: H256, cfg: &HcCfg, ha_owner: Address, nn: U256, + is_v7: bool, ) -> HcErr { - let mut new_op = make_external_op( + let (mut new_op, mut new_cd) = make_external_op( src_addr, nonce, op_success, response_payload, sub_key, - ep_addr, + ha_addr, sig_hex.clone(), oo_nonce, cfg, + is_v7, ); - let check_hash = new_op.hash(cfg.entry_point, cfg.chain_id); + let check_hash = new_op + .into_variant(&cfg.chain_spec) + .hash(entry_point, cfg.chain_spec.id); let check_sig: ethers::types::Signature = ethers::types::Signature::from_str(&sig_hex).expect("Signature decode"); let check_msg: ethers::types::RecoveryMessage = Data(check_hash.to_fixed_bytes().to_vec()); @@ -325,37 +363,56 @@ pub async fn external_op( code: 0, message: "".to_string(), }; - if check_sig.verify(check_msg, ha_owner).is_err() { println!("HC Bad offchain signature"); hc_err = HcErr { code: 3, message: "HC03: Bad offchain signature".to_string(), }; - new_op = make_err_op(hc_err.clone(), sub_key, src_addr, nn, oo_nonce, cfg); + let key_bytes: Bytes = cfg.sys_privkey.as_fixed_bytes().into(); + let wallet = LocalWallet::from_bytes(&key_bytes).unwrap(); + + (new_op, new_cd) = make_err_op( + hc_err.clone(), + sub_key, + src_addr, + nn, + oo_nonce, + cfg, + entry_point, + wallet, + is_v7, + ) + .await; } let ent: HcEntry = HcEntry { sub_key, map_key, + call_data: new_cd.clone(), user_op: new_op.clone(), ts: SystemTime::now(), oc_gas: U256::zero(), needed_pvg: U256::zero(), + is_v7, }; HC_MAP.lock().unwrap().insert(op_key, ent); hc_err } -fn make_err_op( +#[allow(clippy::too_many_arguments)] // FIXME later +async fn make_err_op( err_hc: HcErr, sub_key: H256, src_addr: Address, nn: U256, oo_nonce: U256, cfg: &HcCfg, -) -> UserOperationV0_6 { + entry_point: Address, + wallet: LocalWallet, + is_v7: bool, +) -> (UserOperationOptionalGas, Bytes) { let response_payload: Bytes = AbiEncode::encode((src_addr, nn, err_hc.code, err_hc.message)).into(); @@ -364,23 +421,58 @@ fn make_err_op( sub_key, Bytes::from(response_payload.to_vec()), ); - println!("HC err_op call_data {:?}", call_data); - - let new_op: UserOperationV0_6 = UserOperationV0_6 { - sender: cfg.sys_account, - nonce: oo_nonce, - init_code: Bytes::new(), - call_data: call_data.clone(), - call_gas_limit: U256::from(0x40000), - verification_gas_limit: U256::from(0x10000), - pre_verification_gas: U256::from(0x10000), - max_fee_per_gas: U256::zero(), - max_priority_fee_per_gas: U256::zero(), - paymaster_and_data: Bytes::new(), - signature: Bytes::new(), - }; - new_op + if is_v7 { + let mut new_op = UserOperationOptionalGasV0_7 { + sender: cfg.sys_account, + nonce: oo_nonce, + call_data: call_data.clone(), + call_gas_limit: Some(U128::from(0x40000)), + verification_gas_limit: Some(U128::from(0x10000)), + pre_verification_gas: Some(U256::from(0x10000)), + max_fee_per_gas: Some(U128::zero()), + max_priority_fee_per_gas: Some(U128::zero()), + paymaster_data: Bytes::new(), + signature: Bytes::new(), + paymaster: None, + factory: None, + factory_data: Bytes::new(), + paymaster_verification_gas_limit: None, + paymaster_post_op_gas_limit: None, + }; + let hh = UserOperationOptionalGas::V0_7(new_op.clone()) + .into_variant(&cfg.chain_spec) + .hash(entry_point, cfg.chain_spec.id); + let signature = wallet.sign_message(hh).await; + let sig_bytes: Bytes = signature.as_ref().unwrap().to_vec().into(); + println!("HC err_op signed {:?} {:?}", signature, sig_bytes); + new_op.signature = sig_bytes; + + (UserOperationOptionalGas::V0_7(new_op), call_data) + } else { + let mut new_op = UserOperationOptionalGasV0_6 { + sender: cfg.sys_account, + nonce: oo_nonce, + init_code: Bytes::new(), + call_data: call_data.clone(), + call_gas_limit: Some(U256::from(0x40000)), + verification_gas_limit: Some(U256::from(0x10000)), + pre_verification_gas: Some(U256::from(0x10000)), + max_fee_per_gas: Some(U256::zero()), + max_priority_fee_per_gas: Some(U256::zero()), + paymaster_and_data: Bytes::new(), + signature: Bytes::new(), + }; + let hh = UserOperationOptionalGas::V0_6(new_op.clone()) + .into_variant(&cfg.chain_spec) + .hash(entry_point, cfg.chain_spec.id); + let signature = wallet.sign_message(hh).await; + let sig_bytes: Bytes = signature.as_ref().unwrap().to_vec().into(); + println!("HC err_op signed {:?} {:?}", signature, sig_bytes); + new_op.signature = sig_bytes; + + (UserOperationOptionalGas::V0_6(new_op), call_data) + } } /// Encapsulate an error code into a UserOperation @@ -395,63 +487,113 @@ pub async fn err_op( oo_nonce: U256, map_key: H256, cfg: &HcCfg, + is_v7: bool, ) { println!( "HC hybrid_compute err_op op_key {:?} err_str {:?}", op_key, err_hc.message ); assert!(err_hc.code >= 2); - let mut new_op = make_err_op(err_hc, sub_key, src_addr, nn, oo_nonce, cfg); let key_bytes: Bytes = cfg.sys_privkey.as_fixed_bytes().into(); let wallet = LocalWallet::from_bytes(&key_bytes).unwrap(); - let hh = new_op.hash(entry_point, cfg.chain_id); - - let signature = wallet.sign_message(hh).await; - new_op.signature = signature.as_ref().unwrap().to_vec().into(); - println!("HC err_op signed {:?} {:?}", signature, new_op.signature); + let (new_op, new_cd) = make_err_op( + err_hc, + sub_key, + src_addr, + nn, + oo_nonce, + cfg, + entry_point, + wallet, + is_v7, + ) + .await; let ent: HcEntry = HcEntry { sub_key, map_key, + call_data: new_cd.clone(), user_op: new_op.clone(), ts: SystemTime::now(), oc_gas: U256::zero(), needed_pvg: U256::zero(), + is_v7, }; HC_MAP.lock().unwrap().insert(op_key, ent); } /// Encapsulate a RemoveResponses into a UserOperation -pub async fn rr_op(cfg: &HcCfg, oo_nonce: U256, keys: Vec) -> UserOperationV0_6 { +pub async fn rr_op( + cfg: &HcCfg, + entry_point: Address, + oo_nonce: U256, + keys: Vec, + is_v7: bool, +) -> UserOperationOptionalGas { let call_data = make_rr_calldata(keys); println!("HC rr_op call_data {:?}", call_data); - let mut new_op: UserOperationV0_6 = UserOperationV0_6 { - sender: cfg.sys_account, - nonce: oo_nonce, - init_code: Bytes::new(), - call_data: call_data.clone(), - call_gas_limit: U256::from(0x6000), - verification_gas_limit: U256::from(0x10000), - pre_verification_gas: U256::from(0x10000), - max_fee_per_gas: U256::zero(), - max_priority_fee_per_gas: U256::zero(), - paymaster_and_data: Bytes::new(), - signature: Bytes::new(), - }; + if is_v7 { + let mut new_op = UserOperationOptionalGasV0_7 { + sender: cfg.sys_account, + nonce: oo_nonce, + call_data: call_data.clone(), + call_gas_limit: Some(U128::from(0x6000)), + verification_gas_limit: Some(U128::from(0x10000)), + pre_verification_gas: Some(U256::from(0x10000)), + max_fee_per_gas: Some(U128::zero()), + max_priority_fee_per_gas: Some(U128::zero()), + paymaster_data: Bytes::new(), + signature: Bytes::new(), + paymaster: None, + factory: None, + factory_data: Bytes::new(), + paymaster_verification_gas_limit: None, + paymaster_post_op_gas_limit: None, + }; - let key_bytes: Bytes = cfg.sys_privkey.as_fixed_bytes().into(); - let wallet = LocalWallet::from_bytes(&key_bytes).unwrap(); + let key_bytes: Bytes = cfg.sys_privkey.as_fixed_bytes().into(); + let wallet = LocalWallet::from_bytes(&key_bytes).unwrap(); + + let hh = UserOperationOptionalGas::V0_7(new_op.clone()) + .into_variant(&cfg.chain_spec) + .hash(entry_point, cfg.chain_spec.id); - let hh = new_op.hash(cfg.entry_point, cfg.chain_id); - println!("HC pre_sign hash {:?}", hh); + let signature = wallet.sign_message(hh).await; + new_op.signature = signature.as_ref().unwrap().to_vec().into(); + println!("HC rr_op signed {:?} {:?}", signature, new_op.signature); - let signature = wallet.sign_message(hh).await; - new_op.signature = signature.as_ref().unwrap().to_vec().into(); - println!("HC rr_op signed {:?} {:?}", signature, new_op.signature); + UserOperationOptionalGas::V0_7(new_op) + } else { + let mut new_op = UserOperationOptionalGasV0_6 { + sender: cfg.sys_account, + nonce: oo_nonce, + init_code: Bytes::new(), + call_data: call_data.clone(), + call_gas_limit: Some(U256::from(0x6000)), + verification_gas_limit: Some(U256::from(0x10000)), + pre_verification_gas: Some(U256::from(0x10000)), + max_fee_per_gas: Some(U256::zero()), + max_priority_fee_per_gas: Some(U256::zero()), + paymaster_and_data: Bytes::new(), + signature: Bytes::new(), + }; - new_op + let key_bytes: Bytes = cfg.sys_privkey.as_fixed_bytes().into(); + let wallet = LocalWallet::from_bytes(&key_bytes).unwrap(); + + let hh = UserOperationOptionalGas::V0_6(new_op.clone()) + .into_variant(&cfg.chain_spec) + .hash(entry_point, cfg.chain_spec.id); + println!("HC pre_sign hash {:?}", hh); + + let signature = wallet.sign_message(hh).await; + new_op.signature = signature.as_ref().unwrap().to_vec().into(); + println!("HC rr_op signed {:?} {:?}", signature, new_op.signature); + + UserOperationOptionalGas::V0_6(new_op) + } } /// Retrieve a cached HC operation @@ -467,7 +609,7 @@ pub fn del_hc_ent(key: H256) { /// Retrieve the PutResponse() payload from a cached HC operation pub fn get_hc_op_payload(key: H256) -> Bytes { let op = HC_MAP.lock().unwrap().get(&key).cloned().unwrap(); - let cd1 = &op.user_op.call_data[4..]; + let cd1 = &op.call_data[4..]; let dec1 = <(Address, U256, Bytes) as AbiDecode>::decode(cd1).unwrap(); let cd2 = &dec1.2[4..]; let dec2 = <(H256, Bytes) as AbiDecode>::decode(cd2).unwrap(); @@ -520,10 +662,12 @@ pub fn hc_set_pvg(key: H256, needed_pvg: U256, oc_gas: U256) { let new_ent = HcEntry { sub_key: ent.sub_key, map_key: ent.map_key, + call_data: ent.call_data.clone(), user_op: ent.user_op.clone(), ts: ent.ts, needed_pvg, oc_gas, + is_v7: ent.is_v7, }; map.remove(&key); map.insert(key, new_ent); @@ -563,11 +707,9 @@ mod test { "0x1111111111111111111111111111111111111111111111111111111111111111" .parse::() .unwrap(), - "0x0000000000000000000000000000000000000004" - .parse::
() - .unwrap(), - 123, + ChainSpec::default(), "http://test.local/rpc".to_string(), + U256::from(2), ); set_signer( "0x0000000000000000000000000000000000000005" @@ -588,17 +730,22 @@ mod test { sys_privkey: "0x1111111111111111111111111111111111111111111111111111111111111111" .parse::() .unwrap(), - entry_point: "0x0000000000000000000000000000000000000004" - .parse::
() - .unwrap(), - chain_id: 123, + chain_spec: ChainSpec::default(), node_http: "http://test.local/rpc".to_string(), from_addr: "0x0000000000000000000000000000000000000005" .parse::
() .unwrap(), + slot_idx: U256::from(2), }; let cfg: HcCfg = HC_CONFIG.lock().unwrap().clone(); - assert_eq!(expected, cfg); + // chain_spec doesn't support PartialEq + assert_eq!(expected.helper_addr, cfg.helper_addr); + assert_eq!(expected.sys_account, cfg.sys_account); + assert_eq!(expected.sys_owner, cfg.sys_owner); + assert_eq!(expected.sys_privkey, cfg.sys_privkey); + assert_eq!(expected.chain_spec.id, cfg.chain_spec.id); + assert_eq!(expected.node_http, cfg.node_http); + assert_eq!(expected.from_addr, cfg.from_addr); } #[test] @@ -620,7 +767,7 @@ mod test { let e_sub_key = "0x16d7f606293dca5dbbe97735b2913e6dade6e3f216310b12148cb67a6fd86947" .parse::() .unwrap(); - let e_ep_addr = "0x9c6df0d4c9d8f527221b59c66ad5279c16a1dbc2" + let e_ha_addr = "0x9c6df0d4c9d8f527221b59c66ad5279c16a1dbc2" .parse::
() .unwrap(); let e_sel = [151, 224, 215, 186]; @@ -630,8 +777,8 @@ mod test { assert_eq!(e_map_key, map_key); let sub_key = hc_sub_key(&rev_data); assert_eq!(e_sub_key, sub_key); - let ep_addr = hc_ep_addr(&rev_data); - assert_eq!(e_ep_addr, ep_addr); + let ha_addr = hc_ha_addr(&rev_data); + assert_eq!(e_ha_addr, ha_addr); let sel = hc_selector(&rev_data); assert_eq!(e_sel, sel); let payload = hc_req_payload(&rev_data); @@ -645,7 +792,7 @@ mod test { let payload = "0x0000000000000000000000000000000000000000000000000000000000000002" .parse::() .unwrap(); - let op = make_external_op( + let (op, _) = make_external_op( "0x1000000000000000000000000000000000000001".parse::
().unwrap(), U256::from(100), true, @@ -655,27 +802,36 @@ mod test { "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c".to_string(), U256::from(222), &cfg, + true, ); let e_calldata = "0xb61d27f60000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000124dfc98ae82222222222222222222222222222222222222222222222222222222222222222000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000010000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000".parse::().unwrap(); - let expected:UserOperationV0_6 = UserOperationV0_6{ + let expected = UserOperationOptionalGasV0_7 { sender: "0x2000000000000000000000000000000000000002".parse::
().unwrap(), nonce: U256::from(222), - init_code: Bytes::new(), call_data: e_calldata, - call_gas_limit: U256::from(192560), - verification_gas_limit: U256::from(65536), - pre_verification_gas: U256::from(65536), - max_fee_per_gas: U256::from(0), - max_priority_fee_per_gas: U256::from(0), - paymaster_and_data: Bytes::new(), + call_gas_limit: Some(U128::from(192560)), + verification_gas_limit: Some(U128::from(65536)), + pre_verification_gas: Some(U256::from(65536)), + max_fee_per_gas: Some(U128::from(0)), + max_priority_fee_per_gas: Some(U128::from(0)), + paymaster_data: Bytes::new(), signature: "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c".parse::().unwrap(), + paymaster: None, + factory: None, + factory_data: Bytes::new(), + paymaster_verification_gas_limit: None, + paymaster_post_op_gas_limit: None, }; - assert_eq!(expected, op); + if let UserOperationOptionalGas::V0_7(op7) = op { + assert_eq!(expected, op7); + } else { + panic!("Invalud UserOperation variant"); + } } - #[test] - fn test_op_gen_error() { + #[tokio::test] + async fn test_op_gen_error() { let cfg = HcCfg { helper_addr: "0x0000000000000000000000000000000000000001" .parse::
() @@ -689,17 +845,16 @@ mod test { sys_privkey: "0x1111111111111111111111111111111111111111111111111111111111111111" .parse::() .unwrap(), - entry_point: "0x0000000000000000000000000000000000000004" - .parse::
() - .unwrap(), - chain_id: 123, + chain_spec: ChainSpec::default(), node_http: "http://test.local/rpc".to_string(), from_addr: "0x0000000000000000000000000000000000000005" .parse::
() .unwrap(), + slot_idx: U256::from(0), }; - let op = make_err_op( + let wallet = LocalWallet::from_bytes(&cfg.sys_privkey.to_fixed_bytes()).unwrap(); + let op_future = make_err_op( HcErr { code: 4, message: "unit test".to_string(), @@ -713,24 +868,40 @@ mod test { U256::from(100), U256::from(222), &cfg, + "0x0000000000000000000000000000000000000004" + .parse::
() + .unwrap(), + wallet, + true, ); let e_calldata = "0xb61d27f60000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000124fde89b642222222222222222222222222222222222222222222222222222222222222222000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000020000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000009756e69742074657374000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000".parse::().unwrap(); - let expected: UserOperationV0_6 = UserOperationV0_6 { + let expected = UserOperationOptionalGasV0_7 { sender: "0x0000000000000000000000000000000000000002" .parse::
() .unwrap(), nonce: U256::from(222), - init_code: Bytes::new(), call_data: e_calldata, - call_gas_limit: U256::from(262144), - verification_gas_limit: U256::from(65536), - pre_verification_gas: U256::from(65536), - max_fee_per_gas: U256::from(0), - max_priority_fee_per_gas: U256::from(0), - paymaster_and_data: Bytes::new(), - signature: Bytes::new(), + call_gas_limit: Some(U128::from(262144)), + verification_gas_limit: Some(U128::from(65536)), + pre_verification_gas: Some(U256::from(65536)), + max_fee_per_gas: Some(U128::from(0)), + max_priority_fee_per_gas: Some(U128::from(0)), + paymaster_data: Bytes::new(), + signature:"0xfe10d7b74cc08f195b44caa8ce3dbd084941c5b26231f456d54cd101efe4e1ea37793d70c6732bbea5f10e9214ea2596fd5dfaf3f7c162b0c4d41a5d5eca64af1b" + .parse::() + .unwrap(), + paymaster: None, + factory: None, + factory_data: Bytes::new(), + paymaster_verification_gas_limit: None, + paymaster_post_op_gas_limit: None, }; - assert_eq!(expected, op); + let (op, _) = op_future.await; + if let UserOperationOptionalGas::V0_7(op7) = op { + assert_eq!(expected, op7); + } else { + panic!("Invalud UserOperation variant"); + } } } diff --git a/crates/types/src/user_operation/mod.rs b/crates/types/src/user_operation/mod.rs index 5e1c0ea4..d46d46af 100644 --- a/crates/types/src/user_operation/mod.rs +++ b/crates/types/src/user_operation/mod.rs @@ -15,7 +15,7 @@ use std::{fmt::Debug, time::Duration}; use ethers::{ abi::AbiEncode, - types::{Address, Bytes, H256, U256}, + types::{Address, Bytes, H256, U128, U256}, }; /// User Operation types for Entry Point v0.6 @@ -375,6 +375,28 @@ impl UserOperationOptionalGas { }; abi_size + BUNDLE_BYTE_OVERHEAD + USER_OP_OFFSET_WORD_SIZE } + + /// Hash fields relevant to Hybrid Compute + pub fn hc_hash(&self) -> H256 { + match self { + UserOperationOptionalGas::V0_6(op) => op.hc_hash(), + UserOperationOptionalGas::V0_7(op) => op.hc_hash(), + } + } + + /// Convert into UserOperationVariant type - needed for Hybrid Compute + pub fn into_variant(&self, cs: &ChainSpec) -> UserOperationVariant { + match self { + UserOperationOptionalGas::V0_6(op) => UserOperationVariant::V0_6( + op.clone().into_user_operation(U256::from(0), U256::from(0)), + ), + UserOperationOptionalGas::V0_7(op) => UserOperationVariant::V0_7( + op.clone() + .into_user_operation_builder(cs, U128::from(0), U128::from(0), U128::from(0)) + .build(), + ), + } + } } /// Gas estimate diff --git a/crates/types/src/user_operation/v0_6.rs b/crates/types/src/user_operation/v0_6.rs index 8a502118..408d11c6 100644 --- a/crates/types/src/user_operation/v0_6.rs +++ b/crates/types/src/user_operation/v0_6.rs @@ -284,7 +284,7 @@ impl AsMut for super::UserOperationVariant { } /// User operation with optional gas fields for gas estimation -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] // PartialEq for hybrid_compute #[serde(rename_all = "camelCase")] pub struct UserOperationOptionalGas { /// Sender (required) @@ -401,6 +401,13 @@ impl UserOperationOptionalGas { rand::thread_rng().fill_bytes(&mut bytes); bytes.into() } + + /// Hash fields relevant to Hybrid Compute + pub fn hc_hash(&self) -> H256 { + self.clone() + .into_user_operation(U256::from(0), U256::from(0)) + .hc_hash() + } } impl From for UserOperationOptionalGas { diff --git a/crates/types/src/user_operation/v0_7.rs b/crates/types/src/user_operation/v0_7.rs index d9bf8b86..b77f1250 100644 --- a/crates/types/src/user_operation/v0_7.rs +++ b/crates/types/src/user_operation/v0_7.rs @@ -111,7 +111,10 @@ impl UserOperationTrait for UserOperation { } fn hc_hash(&self) -> H256 { - H256::zero() // Not yet implemented + keccak256(encode(&[Token::FixedBytes( + keccak256(self.pack_for_hc_hash()).to_vec(), + )])) + .into() } fn id(&self) -> UserOperationId { @@ -254,6 +257,22 @@ impl UserOperation { pub fn packed(&self) -> &PackedUserOperation { &self.packed } + + /// Gets the byte array representation of the user operation to be used as HC key + pub fn pack_for_hc_hash(&self) -> Bytes { + let hash_init_code = keccak256(self.packed.init_code.clone()); + let hash_call_data = keccak256(self.call_data.clone()); + let hash_paymaster_and_data = keccak256(self.packed.paymaster_and_data.clone()); + + encode(&[ + Token::Address(self.sender), + Token::Uint(self.nonce), + Token::FixedBytes(hash_init_code.to_vec()), + Token::FixedBytes(hash_call_data.to_vec()), + Token::FixedBytes(hash_paymaster_and_data.to_vec()), // ??? + ]) + .into() + } } impl From for UserOperation { @@ -504,6 +523,15 @@ impl UserOperationOptionalGas { rand::thread_rng().fill_bytes(&mut bytes); bytes.into() } + + /// Hash fields relevant to Hybrid Compute + pub fn hc_hash(&self) -> H256 { + let cs = ChainSpec::default(); + self.clone() + .into_user_operation_builder(&cs, U128::from(0), U128::from(0), U128::from(0)) + .build() + .hc_hash() + } } impl From for UserOperationOptionalGas { diff --git a/hybrid-compute/aa-client.py b/hybrid-compute/aa-client.py index 13edb195..fd571079 100644 --- a/hybrid-compute/aa-client.py +++ b/hybrid-compute/aa-client.py @@ -50,7 +50,7 @@ def build_op(to_contract, value_in_wei, initcode_hex, calldata_hex): 'preVerificationGas': "0x0", 'maxFeePerGas': Web3.to_hex(w3.eth.gas_price), 'maxPriorityFeePerGas': Web3.to_hex(w3.eth.max_priority_fee), - 'paymasterAndData':"0x", +# 'paymasterAndData':"0x", 'signature': '0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c' } return p @@ -187,7 +187,7 @@ def ParseReceipt(opReceipt): EP_addr = response.json()['result'][0] vprint("Detected EntryPoint address", EP_addr) -aa = aa_utils(EP_addr, w3.eth.chain_id) +aa = aa_rpc(EP_addr, w3, args.bundler_rpc) vprint("gasPrices", w3.eth.gas_price, w3.eth.max_priority_fee) diff --git a/hybrid-compute/aa_utils/__init__.py b/hybrid-compute/aa_utils/__init__.py index 3f68561f..31fc3ffe 100644 --- a/hybrid-compute/aa_utils/__init__.py +++ b/hybrid-compute/aa_utils/__init__.py @@ -35,6 +35,46 @@ def sign_op(self, op, signer_key): op['signature'] = Web3.to_hex(sig.signature) return op + def sign_v7_op(self, user_op, signer_key): + """Signs a UserOperation, returning a modified op containing a 'signature' field.""" + op = dict(user_op) # Derived fields are added to 'op' prior to hashing + + assert 'paymaster' not in op # not yet implemented + + # The deploy-local script supplies the packed values prior to signature, as it bypasses the bundler. + # For normal UserOperations the fields are derived here + if 'accountGasLimits' not in op: + account_gas_limits = ethabi.encode(['uint128'],[Web3.to_int(hexstr=op['verificationGasLimit'])])[16:32] \ + + ethabi.encode(['uint128'],[Web3.to_int(hexstr=op['callGasLimit'])])[16:32] + else: + account_gas_limits = Web3.to_bytes(hexstr=op['accountGasLimits']) + + if 'gasFees' not in op: + gas_fees = ethabi.encode(['uint128'],[Web3.to_int(hexstr=op['maxPriorityFeePerGas'])])[16:32] \ + + ethabi.encode(['uint128'],[Web3.to_int(hexstr=op['maxFeePerGas'])])[16:32] + else: + gas_fees = Web3.to_bytes(hexstr=op['gasFees']) + + if 'paymasterAndData' not in op: + op['paymasterAndData'] = "0x" + + pack1 = ethabi.encode(['address','uint256','bytes32','bytes32','bytes32','uint256','bytes32','bytes32'], \ + [op['sender'], + Web3.to_int(hexstr=op['nonce']), + Web3.keccak(hexstr="0x"), # initcode + Web3.keccak(hexstr=op['callData']), + account_gas_limits, + Web3.to_int(hexstr=op['preVerificationGas']), + gas_fees, + Web3.keccak(hexstr=op['paymasterAndData']), + ]) + pack2 = ethabi.encode(['bytes32','address','uint256'], [Web3.keccak(pack1), self.EP_addr, self.chain_id]) + e_msg = eth_account.messages.encode_defunct(Web3.keccak(pack2)) + signer_acct = eth_account.account.Account.from_key(signer_key) + sig = signer_acct.sign_message(e_msg) + user_op['signature'] = Web3.to_hex(sig.signature) + return user_op + class aa_rpc(aa_utils): """Provides AA helper methods which talk to an ETH node and/or a Bundler""" def __init__(self, _EP_addr, _eth_rpc, _bundler_url): @@ -69,22 +109,24 @@ def build_op(self, sender, target, value, calldata, nonce_key=0): op = { 'sender': sender, 'nonce': self.aa_nonce(sender,nonce_key), - 'initCode': '0x', + #factory - none + #factoryData - none 'callData': Web3.to_hex(ex_calldata), 'callGasLimit': "0x0", 'verificationGasLimit': Web3.to_hex(0), 'preVerificationGas': "0x0", 'maxFeePerGas': Web3.to_hex(fee), 'maxPriorityFeePerGas': Web3.to_hex(tip), - 'paymasterAndData': '0x', + #paymaster - none + #paymasterVerificationGasLimit - none + #paymasterPostOpGasLimit - none + #paymasterData - none # Dummy signature, per Alchemy AA documentation - # A future update may require a valid signature on gas estimation ops. This should be safe because the gas - # limits in the signed request are set to zero, therefore it would be rejected if a third party attempted to - # submit it as a real transaction. 'signature': '0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c' } print("Built userOperation", op) return op + def estimate_op_gas(self, op, extra_pvg=0, extra_vg=0, extra_cg=0): """ Wrapper to call eth_estimateUserOperationGas() and update the op. Allows limits to be increased in cases where a bundler is @@ -100,21 +142,31 @@ def estimate_op_gas(self, op, extra_pvg=0, extra_vg=0, extra_cg=0): print("*** eth_estimateUserOperationGas failed") time.sleep(2) return False, op - else: - est_result = response.json()['result'] - - op['preVerificationGas'] = Web3.to_hex(Web3.to_int( - hexstr=est_result['preVerificationGas']) + extra_pvg) - op['verificationGasLimit'] = Web3.to_hex(Web3.to_int( - hexstr=est_result['verificationGasLimit']) + extra_vg) - op['callGasLimit'] = Web3.to_hex(Web3.to_int( - hexstr=est_result['callGasLimit']) + extra_cg) + + est_result = response.json()['result'] + + op['preVerificationGas'] = Web3.to_hex(Web3.to_int( + hexstr=est_result['preVerificationGas']) + extra_pvg) + op['verificationGasLimit'] = Web3.to_hex(Web3.to_int( + hexstr=est_result['verificationGasLimit']) + extra_vg) + op['callGasLimit'] = Web3.to_hex(Web3.to_int( + hexstr=est_result['callGasLimit']) + extra_cg) return True, op def sign_submit_op(self, op, owner_key): """Sign and submit a UserOperation to the Bundler""" - op = self.sign_op(op, owner_key) + is_v7 = False + if self.EP_addr == "0x0000000071727De22E5E9d8BAf0edAc6f37da032": + is_v7 = True + else: + assert self.EP_addr == "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789" + + if is_v7: + op = self.sign_v7_op(op, owner_key) + else: + op = self.sign_op(op, owner_key) + while True: response = requests.post(self.bundler_url, json=request( "eth_sendUserOperation", params=[op, self.EP_addr])) @@ -184,13 +236,13 @@ def sign_and_submit(self, tx, key): def approve_token(self, token, spender, deploy_addr, deploy_key): """Perform an unlimited ERC20 token approval""" - approveCD = selector("approve(address,uint256)") + ethabi.encode( + approve_calldata = selector("approve(address,uint256)") + ethabi.encode( ['address','uint256'], [spender, Web3.to_int(hexstr="0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")]) tx = { 'from': deploy_addr, - 'data': approveCD, + 'data': approve_calldata, 'to': token, } print("ERC20 approval of", token, "for", spender) diff --git a/hybrid-compute/deploy-local.py b/hybrid-compute/deploy-local.py index 26b53e16..3a1bcbe2 100644 --- a/hybrid-compute/deploy-local.py +++ b/hybrid-compute/deploy-local.py @@ -18,9 +18,17 @@ parser = argparse.ArgumentParser() parser.add_argument("--boba-path", required=True, help="Path to your local Boba/Optimism repository") parser.add_argument("--deploy-salt", required=False, help="Salt value for contract deployment", default="0") +parser.add_argument("--ep-version", required=False, help="EntryPoint contract version (0.6|0.7)", default="0.7") cli_args = parser.parse_args() +ep7 = False + +if cli_args.ep_version == "0.7": + ep7 = True +elif cli_args.ep_version != "0.6": + assert "Invalid EntryPoint version (0.6 or 0.7 are supported)" == False + with open(cli_args.boba_path + "/.devnet/addresses.json", "r", encoding="ascii") as f: jj = json.load(f) boba_l1_addr = Web3.to_checksum_address(jj['BOBA']) @@ -66,7 +74,10 @@ l2_util = eth_utils(w3) contract_info = {} -OUT_PREFIX = "../crates/types/contracts/out/" +if ep7: + OUT_PREFIX = "../crates/types/contracts/out/hc0_7/" +else: + OUT_PREFIX = "../crates/types/contracts/out/hc0_6/" def load_contract(w, name, path, address): """Loads a contract's JSON ABI""" @@ -84,7 +95,7 @@ def load_contract(w, name, path, address): return w.eth.contract(abi=contract_info[name]['abi'], address=address) -def submit_as_op(addr, calldata, signer_key): +def submit_as_v6_op(addr, calldata, signer_key): """Wrapper to build and submit a UserOperation directly to the int. We don't have a Bundler to run gas estimation so the values are hard-coded. It might be necessary to change these values e.g. if simulating different L1 prices on the local devnet""" @@ -125,6 +136,47 @@ def submit_as_op(addr, calldata, signer_key): return l2_util.sign_and_submit(ho, deploy_key) +def submit_as_v7_op(addr, calldata, signer_key): + """Wrapper to build and submit a UserOperation directly to the EntryPoint. We don't + have a Bundler to run gas estimation so the values are hard-coded. It might be + necessary to change these values e.g. if simulating different L1 prices on the local devnet""" + + gasLimits = "0x00000000000000000000000000016ed900000000000000000000000000053652" + gasFees = "0x00000000000000000000000039d106800000000000000000000000025b9c274c" + + op = { + 'sender':addr, + 'nonce': aa.aa_nonce(addr, 1235), + 'initCode':"0x", + 'callData': Web3.to_hex(calldata), + 'accountGasLimits': gasLimits, + 'preVerificationGas': "0xF0000", + 'gasFees': gasFees, + 'paymasterAndData':"0x", + 'signature': '0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c' + } + + op = aa.sign_v7_op(op, signer_key) + + # Because the bundler is not running yet we must call the EntryPoint directly. + ho = EP.functions.handleOps([( + op['sender'], + Web3.to_int(hexstr=op['nonce']), + op['initCode'], + op['callData'], + Web3.to_bytes(hexstr=op['accountGasLimits']), + Web3.to_int(hexstr=op['preVerificationGas']), + Web3.to_bytes(hexstr=op['gasFees']), + op['paymasterAndData'], + op['signature'], + )], deploy_addr).build_transaction({ + 'from': deploy_addr, + 'value': 0, + }) + ho['gas'] = int(w3.eth.estimate_gas(ho) * 1.2) + + return l2_util.sign_and_submit(ho, deploy_key) + def permit_caller(acct, caller): """Whitelist a contract to call a HybridAccount. Now implemented as a UserOperation rather than requiring the Owner to be an EOA.""" @@ -134,7 +186,10 @@ def permit_caller(acct, caller): calldata = selector("PermitCaller(address,bool)") + \ ethabi.encode(['address','bool'], [caller, True]) - submit_as_op(acct.address, calldata, env_vars['OC_PRIVKEY']) + if ep7: + submit_as_v7_op(acct.address, calldata, env_vars['OC_PRIVKEY']) + else: + submit_as_v6_op(acct.address, calldata, env_vars['OC_PRIVKEY']) def register_url(caller, url): """Associates a URL with the address of a HybridAccount contract""" @@ -168,18 +223,6 @@ def fund_addr(addr): tx['gasPrice'] = Web3.to_wei(1, 'gwei') l2_util.sign_and_submit(tx, deploy_key) -def fund_addr_ep(EP, addr): - """Deposit funds for an address into the EntryPoint""" - if EP.functions.deposits(addr).call()[0] < Web3.to_wei(0.005, 'ether'): - print("Funding acct (depositTo)", addr) - tx = EP.functions.depositTo(addr).build_transaction({ - 'from': deploy_addr, - 'value': Web3.to_wei(0.01, "ether") - }) - l2_util.sign_and_submit(tx, deploy_key) - print("Balances for", addr, Web3.from_wei(w3.eth.get_balance(addr), 'ether'), - Web3.from_wei(EP.functions.deposits(addr).call()[0], 'ether')) - def deploy_account(factory, owner): """Deploy an account using a Factory contract""" calldata = selector("createAccount(address,uint256)") + ethabi.encode(['address','uint256'],[owner,0]) @@ -199,9 +242,15 @@ def deploy_forge(script, cmd_env): args = ["/home/enya/.foundry/bin/forge", "script", "--silent", "--json", "--broadcast"] args.append("--rpc-url=http://127.0.0.1:9545") args.append("--contracts") - args.append("src/hc0_6") - args.append("--remappings") - args.append("@openzeppelin/=lib/openzeppelin-contracts-versions/v4_9") + if ep7: + args.append("src/hc0_7") + args.append("--remappings") + args.append("@openzeppelin/=lib/openzeppelin-contracts-versions/v5_0") + else: + args.append("src/hc0_6") + args.append("--remappings") + args.append("@openzeppelin/=lib/openzeppelin-contracts-versions/v4_9") + args.append(script) sys_env = os.environ.copy() @@ -209,7 +258,14 @@ def deploy_forge(script, cmd_env): cmd_env['PRIVATE_KEY'] = deploy_key cmd_env['DEPLOY_ADDR'] = deploy_addr cmd_env['DEPLOY_SALT'] = cli_args.deploy_salt # Update to force redeployment - cmd_env['ENTRY_POINTS'] = env_vars['ENTRY_POINTS'] + + if 'ENTRY_POINTS' in env_vars: + cmd_env['ENTRY_POINTS'] = env_vars['ENTRY_POINTS'] + elif ep7: + cmd_env['ENTRY_POINTS'] = "0x0000000071727De22E5E9d8BAf0edAc6f37da032" + else: + cmd_env['ENTRY_POINTS'] = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789" + print("Using EntryPoint address:", cmd_env['ENTRY_POINTS']) out = subprocess.run(args, cwd="../crates/types/contracts", env=cmd_env, capture_output=True, check=True) @@ -232,14 +288,22 @@ def deploy_base(): cmd_env = {} cmd_env['HC_SYS_OWNER'] = env_vars['HC_SYS_OWNER'] cmd_env['BOBA_TOKEN'] = boba_token - addrs = deploy_forge("hc_scripts/LocalDeploy.s.sol", cmd_env) + if ep7: + addrs = deploy_forge("hc_scripts/LocalDeploy_v7.s.sol", cmd_env) + else: + addrs = deploy_forge("hc_scripts/LocalDeploy_v6.s.sol", cmd_env) + print("Deployed base contracts:", addrs) return addrs.split(',') def deploy_examples(hybrid_acct_addr): cmd_env = {} cmd_env['OC_HYBRID_ACCOUNT'] = hybrid_acct_addr - addrs = deploy_forge("hc_scripts/ExampleDeploy.s.sol", cmd_env) + if ep7: + addrs = deploy_forge("hc_scripts/ExampleDeploy_v7.s.sol", cmd_env) + else: + addrs = deploy_forge("hc_scripts/ExampleDeploy_v6.s.sol", cmd_env) + print("Deployed example contracts:", addrs) return addrs.split(',') @@ -259,7 +323,10 @@ def boba_balance(addr): bal = w3.eth.call({'to':boba_token, 'data':bal_calldata}) return Web3.to_int(bal) -EP = load_contract(w3, "EntryPoint", "../crates/types/contracts/lib/account-abstraction-versions/v0_6/deployments/optimism/EntryPoint.json", "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789") +if ep7: + EP = load_contract(w3, "EntryPoint", "../crates/types/contracts/out/v0_7/EntryPoint.sol/EntryPoint.json", "0x0000000071727De22E5E9d8BAf0edAc6f37da032") +else: + EP = load_contract(w3, "EntryPoint", "../crates/types/contracts/lib/account-abstraction-versions/v0_6/deployments/optimism/EntryPoint.json", "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789") assert l1.eth.get_balance(deploy_addr) > Web3.to_wei(1000, 'ether') @@ -325,10 +392,10 @@ def boba_balance(addr): fund_addr(client_addr) ha1_addr = deploy_account(haf_addr, env_vars['OC_OWNER']) -fund_addr_ep(EP, ha1_addr) +fund_addr(ha1_addr) HA = load_contract(w3, 'HybridAccount', OUT_PREFIX + "HybridAccount.sol/HybridAccount.json", ha1_addr) -SA = load_contract(w3, 'SimpleAccount', OUT_PREFIX + "SimpleAccount.sol/SimpleAccount.json", client_addr) +#SA = load_contract(w3, 'SimpleAccount', OUT_PREFIX + "SimpleAccount.sol/SimpleAccount.json", client_addr) example_addrs = deploy_examples(ha1_addr) diff --git a/hybrid-compute/local.env b/hybrid-compute/local.env index e57e1e32..6dd0c11e 100644 --- a/hybrid-compute/local.env +++ b/hybrid-compute/local.env @@ -12,4 +12,3 @@ OC_OWNER=0xE073fC0ff8122389F6e693DD94CcDc5AF637448e OC_PRIVKEY=0x7c0c629efc797f8c5f658919b7efbae01275470d59d03fdeb0fca1e6bd11d7fa CLIENT_OWNER=0x77Fe14A710E33De68855b0eA93Ed8128025328a9 CLIENT_PRIVKEY=0x541b3e3b20b8bb0e5bae310b2d4db4c8b7912ba09750e6ff161b7e67a26a9bf7 -ENTRY_POINTS=0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789 diff --git a/hybrid-compute/offchain/add_sub_2/add_sub_2_offchain.py b/hybrid-compute/offchain/add_sub_2/add_sub_2_offchain.py index c427b71e..431154b1 100644 --- a/hybrid-compute/offchain/add_sub_2/add_sub_2_offchain.py +++ b/hybrid-compute/offchain/add_sub_2/add_sub_2_offchain.py @@ -1,13 +1,13 @@ from web3 import Web3 from eth_abi import abi as ethabi -from offchain_utils import gen_response, parse_req +from offchain_utils import gen_response_v7, parse_req def offchain_addsub2(ver, sk, src_addr, src_nonce, oo_nonce, payload, *args): print(" -> offchain_addsub2 handler called with ver={} subkey={} src_addr={} src_nonce={} oo_nonce={} payload={} extra_args={}".format( ver, sk, src_addr, src_nonce, oo_nonce, payload, args)) err_code = 1 resp = Web3.to_bytes(text="unknown error") - assert ver == "0.2" + assert ver == "0.3" try: req = parse_req(sk, src_addr, src_nonce, oo_nonce, payload) @@ -24,4 +24,4 @@ def offchain_addsub2(ver, sk, src_addr, src_nonce, oo_nonce, payload, *args): except Exception as e: print("DECODE FAILED", e) - return gen_response(req, err_code, resp) + return gen_response_v7(req, err_code, resp) diff --git a/hybrid-compute/offchain/add_sub_2/add_sub_2_test.py b/hybrid-compute/offchain/add_sub_2/add_sub_2_test.py index ee2d1e48..128dde2d 100644 --- a/hybrid-compute/offchain/add_sub_2/add_sub_2_test.py +++ b/hybrid-compute/offchain/add_sub_2/add_sub_2_test.py @@ -3,12 +3,12 @@ def TestAddSub2(aa, a, b): print(f"\n - - - - TestAddSub2({a},{b}) - - - -") - print("TestCount(begin)=", TC.functions.counters(SA.address).call()) + print("TestCount(begin)=", TC.functions.counters(u_account).call()) count_call = selector("count(uint32,uint32)") + \ ethabi.encode(['uint32', 'uint32'], [a, b]) - op = aa.build_op(SA.address, TC.address, 0, count_call, nKey) + op = aa.build_op(u_account, TC.address, 0, count_call, nKey) (success, op) = estimateOp(aa, op) if not success: @@ -17,4 +17,4 @@ def TestAddSub2(aa, a, b): rcpt = aa.sign_submit_op(op, u_key) ParseReceipt(rcpt) - print("TestCount(end)=", TC.functions.counters(SA.address).call()) + print("TestCount(end)=", TC.functions.counters(u_account).call()) diff --git a/hybrid-compute/offchain/auction_system/auction_system_offchain.py b/hybrid-compute/offchain/auction_system/auction_system_offchain.py index 046f3157..ba3781a9 100644 --- a/hybrid-compute/offchain/auction_system/auction_system_offchain.py +++ b/hybrid-compute/offchain/auction_system/auction_system_offchain.py @@ -1,6 +1,6 @@ from web3 import Web3 from eth_abi import abi as ethabi -from offchain_utils import gen_response, parse_req +from offchain_utils import gen_response_v7, parse_req blacklist = ["0x123"] @@ -9,7 +9,7 @@ def offchain_auction(ver, sk, src_addr, src_nonce, oo_nonce, payload, *args): src_addr, src_nonce, oo_nonce, payload, args)) err_code = 0 resp = Web3.to_bytes(text="unknown error") - assert(ver == "0.2") + assert(ver == "0.3") try: req = parse_req(sk, src_addr, src_nonce, oo_nonce, payload) @@ -26,4 +26,4 @@ def offchain_auction(ver, sk, src_addr, src_nonce, oo_nonce, payload, *args): err_code = 1 print("DECODE FAILED", e) - return gen_response(req, err_code, resp) + return gen_response_v7(req, err_code, resp) diff --git a/hybrid-compute/offchain/auction_system/auction_system_test.py b/hybrid-compute/offchain/auction_system/auction_system_test.py index ffb4aa66..b79abc65 100644 --- a/hybrid-compute/offchain/auction_system/auction_system_test.py +++ b/hybrid-compute/offchain/auction_system/auction_system_test.py @@ -4,9 +4,9 @@ def TestAuction(aa): print("\n - - - - TestAuction() - - - -") - start_auction_call = selector("createAuction(uint256,address)") + ethabi.encode(['uint256', 'address'], [300, SA.address]) + start_auction_call = selector("createAuction(uint256,address)") + ethabi.encode(['uint256', 'address'], [300, u_account]) - op = aa.build_op(SA.address, TEST_AUCTION.address, 0, start_auction_call, nKey) + op = aa.build_op(u_account, TEST_AUCTION.address, 0, start_auction_call, nKey) (success, op) = estimateOp(aa, op) assert success @@ -22,7 +22,7 @@ def bid(aa, auctionId): print("\n - - - - bid() - - - -") bid_call = selector("bid(uint256)") + ethabi.encode(['uint256'], [auctionId]) - op = aa.build_op(SA.address, TEST_AUCTION.address, 6, bid_call, nKey) + op = aa.build_op(u_account, TEST_AUCTION.address, 6, bid_call, nKey) (success, op) = estimateOp(aa, op) assert success diff --git a/hybrid-compute/offchain/check_kyc/check_kyc_offchain.py b/hybrid-compute/offchain/check_kyc/check_kyc_offchain.py index 222c5e45..f9a7b910 100644 --- a/hybrid-compute/offchain/check_kyc/check_kyc_offchain.py +++ b/hybrid-compute/offchain/check_kyc/check_kyc_offchain.py @@ -1,6 +1,6 @@ from web3 import Web3 from eth_abi import abi as ethabi -from offchain_utils import gen_response, parse_req +from offchain_utils import gen_response_v7, parse_req validWallets = ["0x123"] @@ -9,7 +9,7 @@ def offchain_checkkyc(ver, sk, src_addr, src_nonce, oo_nonce, payload, *args): src_addr, src_nonce, oo_nonce, payload, args)) err_code = 0 resp = Web3.to_bytes(text="unknown error") - assert(ver == "0.2") + assert(ver == "0.3") try: req = parse_req(sk, src_addr, src_nonce, oo_nonce, payload) @@ -26,4 +26,4 @@ def offchain_checkkyc(ver, sk, src_addr, src_nonce, oo_nonce, payload, *args): err_code = 1 print("DECODE FAILED", e) - return gen_response(req, err_code, resp) + return gen_response_v7(req, err_code, resp) diff --git a/hybrid-compute/offchain/check_kyc/check_kyc_test.py b/hybrid-compute/offchain/check_kyc/check_kyc_test.py index c8fa129a..0a919bc3 100644 --- a/hybrid-compute/offchain/check_kyc/check_kyc_test.py +++ b/hybrid-compute/offchain/check_kyc/check_kyc_test.py @@ -14,7 +14,7 @@ def TestKyc(aa, isValid: bool): print("\n - - - - TestKyc({}) - - - -".format(isValid)) - print("SA ADDRESS {}".format(SA.address)) + print("SA ADDRESS {}".format(u_account)) print("TestKyc begin") kycCall = None @@ -24,7 +24,7 @@ def TestKyc(aa, isValid: bool): else: kycCall = selector("openForKyced(string)") + ethabi.encode(['string'], [""]) - op = aa.build_op(SA.address, KYC.address, 0, kycCall, nKey) + op = aa.build_op(u_account, KYC.address, 0, kycCall, nKey) (success, op) = estimateOp(aa, op) assert success == isValid diff --git a/hybrid-compute/offchain/get_token_price/get_token_price_offchain.py b/hybrid-compute/offchain/get_token_price/get_token_price_offchain.py index 5c3f8d2e..714feb1b 100644 --- a/hybrid-compute/offchain/get_token_price/get_token_price_offchain.py +++ b/hybrid-compute/offchain/get_token_price/get_token_price_offchain.py @@ -1,7 +1,7 @@ from web3 import Web3 import requests from eth_abi import abi as ethabi -from offchain_utils import gen_response, parse_req +from offchain_utils import gen_response_v7, parse_req def offchain_getprice(ver, sk, src_addr, src_nonce, oo_nonce, payload, *args): @@ -9,7 +9,7 @@ def offchain_getprice(ver, sk, src_addr, src_nonce, oo_nonce, payload, *args): src_addr, src_nonce, oo_nonce, payload, args)) err_code = 0 resp = Web3.to_bytes(text="unknown error") - assert(ver == "0.2") + assert(ver == "0.3") try: req = parse_req(sk, src_addr, src_nonce, oo_nonce, payload) @@ -66,4 +66,4 @@ def offchain_getprice(ver, sk, src_addr, src_nonce, oo_nonce, payload, *args): except Exception as e: print("DECODE FAILED", e) - return gen_response(req, err_code, resp) + return gen_response_v7(req, err_code, resp) diff --git a/hybrid-compute/offchain/get_token_price/get_token_price_test.py b/hybrid-compute/offchain/get_token_price/get_token_price_test.py index e7f73977..97aa227a 100644 --- a/hybrid-compute/offchain/get_token_price/get_token_price_test.py +++ b/hybrid-compute/offchain/get_token_price/get_token_price_test.py @@ -18,7 +18,7 @@ def TestTokenPrice(aa, tokenSymbol): calldata = selector("fetchPrice(string)") + \ ethabi.encode(['string'], [tokenSymbol]) - op = aa.build_op(SA.address, TFP.address, 0, calldata, nKey) + op = aa.build_op(u_account, TFP.address, 0, calldata, nKey) (success, op) = estimateOp(aa, op) assert success diff --git a/hybrid-compute/offchain/offchain_utils.py b/hybrid-compute/offchain/offchain_utils.py index a2a4780e..c1b5283f 100644 --- a/hybrid-compute/offchain/offchain_utils.py +++ b/hybrid-compute/offchain/offchain_utils.py @@ -81,7 +81,72 @@ def gen_response(req, err_code, resp_payload): e_msg = eth_account.messages.encode_defunct(oo_hash) sig = signer_acct.sign_message(e_msg) - success = (err_code == 0) + success = err_code == 0 + print("Method returning success={} response={} signature={}".format( + success, Web3.to_hex(resp_payload), Web3.to_hex(sig.signature))) + return ({ + "success": success, + "response": Web3.to_hex(resp_payload), + "signature": Web3.to_hex(sig.signature) + }) + +def gen_response_v7(req, err_code, resp_payload): + resp2 = ethabi.encode(['address', 'uint256', 'uint32', 'bytes'], [ + req['srcAddr'], req['srcNonce'], err_code, resp_payload]) + p_enc1 = selector("PutResponse(bytes32,bytes)") + \ + ethabi.encode(['bytes32', 'bytes'], [req['skey'], resp2]) # dfc98ae8 + + p_enc2 = selector("execute(address,uint256,bytes)") + \ + ethabi.encode(['address', 'uint256', 'bytes'], [ + Web3.to_checksum_address(HelperAddr), 0, p_enc1]) # b61d27f6 + + limits = { + 'verificationGasLimit': "0x10000", + 'preVerificationGas': "0x10000", + } + + # This call_gas formula is a "close enough" estimate for the initial implementation. + # A more accurate model, or a protocol enhancement to run an actual simulation, may + # be required in the future. + call_gas = 705*len(resp_payload) + 170000 + + print("call_gas calculation", len(resp_payload), 4+len(p_enc2), call_gas) + + account_gas_limits = \ + ethabi.encode(['uint128'],[Web3.to_int(hexstr=limits['verificationGasLimit'])])[16:32] + \ + ethabi.encode(['uint128'],[call_gas])[16:32] + + gas_fees = Web3.to_bytes( + hexstr="0x0000000000000000000000000000000000000000000000000000000000000000" + ) + + packed = ethabi.encode([ + 'address', + 'uint256', + 'bytes32', + 'bytes32', + 'bytes32', + 'uint256', + 'bytes32', + 'bytes32', + ], [ + HybridAcctAddr, + req['opNonce'], + Web3.keccak(Web3.to_bytes(hexstr='0x')), # initCode + Web3.keccak(p_enc2), + account_gas_limits, + Web3.to_int(hexstr=limits['preVerificationGas']), + gas_fees, + Web3.keccak(Web3.to_bytes(hexstr='0x')), # paymasterAndData + ]) + oo_hash = Web3.keccak(ethabi.encode(['bytes32', 'address', 'uint256'], [ + Web3.keccak(packed), EntryPointAddr, HC_CHAIN])) + + signer_acct = eth_account.account.Account.from_key(hc1_key) + e_msg = eth_account.messages.encode_defunct(oo_hash) + sig = signer_acct.sign_message(e_msg) + + success = err_code == 0 print("Method returning success={} response={} signature={}".format( success, Web3.to_hex(resp_payload), Web3.to_hex(sig.signature))) return ({ diff --git a/hybrid-compute/offchain/rainfall_insurance/rainfall_insurance_offchain.py b/hybrid-compute/offchain/rainfall_insurance/rainfall_insurance_offchain.py index ff329b2b..2c586a47 100644 --- a/hybrid-compute/offchain/rainfall_insurance/rainfall_insurance_offchain.py +++ b/hybrid-compute/offchain/rainfall_insurance/rainfall_insurance_offchain.py @@ -3,7 +3,7 @@ from web3 import Web3 import requests from eth_abi import abi as ethabi -from offchain_utils import gen_response, parse_req +from offchain_utils import gen_response_v7, parse_req load_dotenv() @@ -16,7 +16,7 @@ def offchain_getrainfall(ver, sk, src_addr, src_nonce, oo_nonce, payload, *args) src_addr, src_nonce, oo_nonce, payload, args)) err_code = 0 resp = Web3.to_bytes(text="unknown error") - assert(ver == "0.2") + assert(ver == "0.3") try: req = parse_req(sk, src_addr, src_nonce, oo_nonce, payload) @@ -53,6 +53,6 @@ def offchain_getrainfall(ver, sk, src_addr, src_nonce, oo_nonce, payload, *args) except Exception as e: print("DECODE FAILED", e) - return gen_response(req, err_code, resp) + return gen_response_v7(req, err_code, resp) diff --git a/hybrid-compute/offchain/rainfall_insurance/rainfall_insurance_test.py b/hybrid-compute/offchain/rainfall_insurance/rainfall_insurance_test.py index efc4da5b..854f86cd 100644 --- a/hybrid-compute/offchain/rainfall_insurance/rainfall_insurance_test.py +++ b/hybrid-compute/offchain/rainfall_insurance/rainfall_insurance_test.py @@ -15,7 +15,7 @@ def test_rainfall_insurance_purchase(aa): calldata = selector("buyInsurance(uint256,string)") + \ ethabi.encode(['uint256','string'],[trigger_rainfall, city]) - op = aa.build_op(SA.address, TEST_RAINFALL_INSURANCE.address, premium, calldata, nKey) + op = aa.build_op(u_account, TEST_RAINFALL_INSURANCE.address, premium, calldata, nKey) (success, op) = estimateOp(aa, op) assert success @@ -38,7 +38,7 @@ def test_rainfall_insurance_payout(aa, policy_id): ethabi.encode(['address', 'uint256', 'bytes'], [ TEST_RAINFALL_INSURANCE.address, 0, payout_call]) - op = aa.build_op(SA.address, TEST_RAINFALL_INSURANCE.address, 0, payout_call, nKey) + op = aa.build_op(u_account, TEST_RAINFALL_INSURANCE.address, 0, payout_call, nKey) (success, op) = estimateOp(aa, op) assert success diff --git a/hybrid-compute/offchain/ramble/ramble_offchain.py b/hybrid-compute/offchain/ramble/ramble_offchain.py index cee138ac..a1c8b6c4 100644 --- a/hybrid-compute/offchain/ramble/ramble_offchain.py +++ b/hybrid-compute/offchain/ramble/ramble_offchain.py @@ -2,7 +2,7 @@ import random from web3 import Web3 from eth_abi import abi as ethabi -from offchain_utils import gen_response, parse_req +from offchain_utils import gen_response_v7, parse_req def load_words(): """Loads a list of dictionary words, assumes a standard file path""" @@ -24,7 +24,7 @@ def offchain_ramble(ver, sk, src_addr, src_nonce, oo_nonce, payload, *args): src_addr, src_nonce, oo_nonce, payload, args)) err_code = 1 resp = Web3.to_bytes(text="unknown error") - assert ver == "0.2" + assert ver == "0.3" try: req = parse_req(sk, src_addr, src_nonce, oo_nonce, payload) @@ -51,4 +51,4 @@ def offchain_ramble(ver, sk, src_addr, src_nonce, oo_nonce, payload, *args): except Exception as e: print("DECODE FAILED", e) - return gen_response(req, err_code, resp) + return gen_response_v7(req, err_code, resp) diff --git a/hybrid-compute/offchain/ramble/ramble_test.py b/hybrid-compute/offchain/ramble/ramble_test.py index 609d5eb0..937a7285 100644 --- a/hybrid-compute/offchain/ramble/ramble_test.py +++ b/hybrid-compute/offchain/ramble/ramble_test.py @@ -11,7 +11,7 @@ def TestWordGuess(aa, n, cheat): print("Pool balance before playing =", Web3.from_wei( TC.functions.Pool().call(), 'gwei')) - op = aa.build_op(SA.address, TC.address, n * per_entry, game_call, nKey) + op = aa.build_op(u_account, TC.address, n * per_entry, game_call, nKey) (success, op) = estimateOp(aa, op) assert success diff --git a/hybrid-compute/offchain/sports_betting/sports_betting_offchain.py b/hybrid-compute/offchain/sports_betting/sports_betting_offchain.py index 56b15742..6be3f42c 100644 --- a/hybrid-compute/offchain/sports_betting/sports_betting_offchain.py +++ b/hybrid-compute/offchain/sports_betting/sports_betting_offchain.py @@ -1,12 +1,13 @@ from web3 import Web3 from eth_abi import abi as ethabi -from offchain_utils import gen_response, parse_req +from offchain_utils import gen_response_v7, parse_req def offchain_sports_betting(ver, sk, src_addr, src_nonce, oo_nonce, payload, *args): print(" -> offchain_sport_betting handler called with subkey={} src_addr={} src_nonce={} oo_nonce={} payload={} extra_args={}".format(sk, src_addr, src_nonce, oo_nonce, payload, args)) err_code = 0 resp = Web3.to_bytes(text="unknown error") + assert ver == "0.3" try: req = parse_req(sk, src_addr, src_nonce, oo_nonce, payload) @@ -30,7 +31,7 @@ def offchain_sports_betting(ver, sk, src_addr, src_nonce, oo_nonce, payload, *ar err_code = 1 print("DECODE FAILED", e) - return gen_response(req, err_code, resp) + return gen_response_v7(req, err_code, resp) def get_game_score(game_id): # This is a dummy function to simulate the offchain data retrieval diff --git a/hybrid-compute/offchain/sports_betting/sports_betting_test.py b/hybrid-compute/offchain/sports_betting/sports_betting_test.py index 0b133478..a1fc1645 100644 --- a/hybrid-compute/offchain/sports_betting/sports_betting_test.py +++ b/hybrid-compute/offchain/sports_betting/sports_betting_test.py @@ -3,7 +3,7 @@ import time def TestSportsBetting(aa): print("\n - - - - SportBetting() - - - -") - print("SA ADDRESS {}".format(SA.address)) + print("SA ADDRESS {}".format(u_account)) game_id = 456 create_bet(aa, game_id) @@ -21,7 +21,7 @@ def create_bet(aa, game_id): print("--------------------Create Bet--------------------") create_call = selector("createGame(uint256)") + ethabi.encode(['uint256'], [game_id]) - op = aa.build_op(SA.address, TEST_SPORTS_BETTING.address, 0, create_call, nKey) + op = aa.build_op(u_account, TEST_SPORTS_BETTING.address, 0, create_call, nKey) (success, op) = estimateOp(aa, op) assert success @@ -38,7 +38,7 @@ def place_bet(aa, game_id): [game_id, outcome]) amount_to_bet = 2 - op = aa.build_op(SA.address, TEST_SPORTS_BETTING.address, amount_to_bet, place_bet, nKey) + op = aa.build_op(u_account, TEST_SPORTS_BETTING.address, amount_to_bet, place_bet, nKey) (success, op) = estimateOp(aa, op) assert success @@ -52,7 +52,7 @@ def settle_bet(aa, game_id): print("--------------------Settle Bet--------------------") settle_bet = selector("settleBet(uint256)") + ethabi.encode(['uint256'], [game_id]) - op = aa.build_op(SA.address, TEST_SPORTS_BETTING.address, 0, settle_bet, nKey) + op = aa.build_op(u_account, TEST_SPORTS_BETTING.address, 0, settle_bet, nKey) time.sleep(5) (success, op) = estimateOp(aa, op) assert success diff --git a/hybrid-compute/offchain/userop.py b/hybrid-compute/offchain/userop.py index ee6fae8f..ad3f9a6d 100644 --- a/hybrid-compute/offchain/userop.py +++ b/hybrid-compute/offchain/userop.py @@ -18,9 +18,9 @@ print("Starting Balances:") showBalances() balStart_bnd = w3.eth.get_balance(bundler_addr) -balStart_sa = EP.functions.getDepositInfo(SA.address).call()[0] + w3.eth.get_balance(SA.address) +balStart_sa = EP.functions.getDepositInfo(u_account).call()[0] + w3.eth.get_balance(u_account) -print("TestCount(start)=", TC.functions.counters(SA.address).call()) +print("TestCount(start)=", TC.functions.counters(u_account).call()) #print("TestFetchPrice(start)=", TFP.functions.counters(0).call()) # =============================================== @@ -28,6 +28,7 @@ aa = aa_rpc(EP.address, w3, bundler_rpc) TestAddSub2(aa, 2, 1) # Success + TestAddSub2(aa, 2, 10) # Underflow error, asserted TestAddSub2(aa, 2, 3) # Underflow error, handled internally TestAddSub2(aa, 7, 0) # Not HC @@ -54,13 +55,13 @@ # =============================================== -print("TestCount(final)=", TC.functions.counters(SA.address).call()) +print("TestCount(final)=", TC.functions.counters(u_account).call()) #print("TestFetchPrice(final)=", TFP.functions.counters(0).call()) print("\nFinal Balances:") showBalances() balFinal_bnd = w3.eth.get_balance(bundler_addr) -balFinal_sa = EP.functions.getDepositInfo(SA.address).call()[0] + w3.eth.get_balance(SA.address) +balFinal_sa = EP.functions.getDepositInfo(u_account).call()[0] + w3.eth.get_balance(u_account) print("Net balance changes", balFinal_bnd - balStart_bnd, balFinal_sa - balStart_sa, (balFinal_bnd + balFinal_sa) - (balStart_bnd + balStart_sa), (gasFees['l1Fees'] + gasFees['l2Fees'])) diff --git a/hybrid-compute/offchain/userop_utils.py b/hybrid-compute/offchain/userop_utils.py index 47f16856..5bc15427 100644 --- a/hybrid-compute/offchain/userop_utils.py +++ b/hybrid-compute/offchain/userop_utils.py @@ -62,8 +62,9 @@ HH = w3.eth.contract( address=deployed['HCHelper']['address'], abi=deployed['HCHelper']['abi']) # This address is unique for each user, who deploys their own wallet account -SA = w3.eth.contract( - address=u_account, abi=deployed['SimpleAccount']['abi']) +#SA = w3.eth.contract( +# address=u_account, abi=deployed['SimpleAccount']['abi']) + HA = w3.eth.contract(address=deployed['HybridAccount'] ['address'], abi=deployed['HybridAccount']['abi']) TC = w3.eth.contract( @@ -90,7 +91,7 @@ def showBalances(): print("bnd", EP.functions.getDepositInfo( bundler_addr).call(), w3.eth.get_balance(bundler_addr)) print("SA ", EP.functions.getDepositInfo( - SA.address).call(), w3.eth.get_balance(SA.address)) + u_account).call(), w3.eth.get_balance(u_account)) print("HA ", EP.functions.getDepositInfo( HA.address).call(), w3.eth.get_balance(HA.address)) print("TC ", EP.functions.getDepositInfo( diff --git a/hybrid-compute/offchain/verify_captcha/captcha_offchain.py b/hybrid-compute/offchain/verify_captcha/captcha_offchain.py index da65a04e..d9f050af 100644 --- a/hybrid-compute/offchain/verify_captcha/captcha_offchain.py +++ b/hybrid-compute/offchain/verify_captcha/captcha_offchain.py @@ -1,12 +1,13 @@ from web3 import Web3 import redis from eth_abi import abi as ethabi -from offchain_utils import gen_response, parse_req +from offchain_utils import gen_response_v7, parse_req def offchain_verifycaptcha(sk, src_addr, src_nonce, oo_nonce, payload, *args): print(" -> offchain_verifycaptcha handler called with subkey={} src_addr={} src_nonce={} oo_nonce={} payload={} extra_args={}".format(sk, src_addr, src_nonce, oo_nonce, payload, args)) + assert ver == "0.3" try: req = parse_req(sk, src_addr, src_nonce, oo_nonce, payload) @@ -24,9 +25,9 @@ def offchain_verifycaptcha(sk, src_addr, src_nonce, oo_nonce, payload, *args): print("ismatch ", is_match) print('captcha input ', captcha_input) print("key decoded ", key_in_redis.decode('utf-8')) - return gen_response(req, 0, ethabi.encode(["bool"], [is_match])) + return gen_response_v7(req, 0, ethabi.encode(["bool"], [is_match])) else: - return gen_response(req, 1, Web3.to_bytes(text="Error: uuid or to not found")) + return gen_response_v7(req, 1, Web3.to_bytes(text="Error: uuid or to not found")) except Exception as e: print("Error:", e) diff --git a/hybrid-compute/offchain/verify_captcha/captcha_test.py b/hybrid-compute/offchain/verify_captcha/captcha_test.py index 8bfb1e91..308b25fe 100644 --- a/hybrid-compute/offchain/verify_captcha/captcha_test.py +++ b/hybrid-compute/offchain/verify_captcha/captcha_test.py @@ -20,7 +20,7 @@ def TestCaptcha(user_addr): global estGas print("\n - - - - TestCaptcha({}) - - - -".format(user_addr)) - print("SA ADDRESS {}".format(SA.address)) + print("SA ADDRESS {}".format(u_account)) print("TestCaptcha begin") captcha = get_captcha(user_addr) diff --git a/hybrid-compute/runit.sh b/hybrid-compute/runit.sh index ffd1d596..f1d8cdde 100755 --- a/hybrid-compute/runit.sh +++ b/hybrid-compute/runit.sh @@ -6,6 +6,6 @@ RUST_BACKTRACE=1 ETH_POLL_INTERVAL_MILLIS=5000 \ --rpc.port 3300 \ --metrics.port 8380 \ --builder.private_keys $BUILDER_PRIVKEY \ - --disable_entry_point_v0_7 \ + --disable_entry_point_v0_6 \ --builder.dropped_status_unsupported \ $@ 2>&1