diff --git a/Cargo.lock b/Cargo.lock index afd7b900b34..df38390d74d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7549,7 +7549,7 @@ dependencies = [ "indexmap 2.13.0", "ipnet", "itertools 0.10.5", - "itertools 0.11.0", + "itertools 0.13.0", "js-sys", "jsonrpsee", "jsonrpsee-client-transport", @@ -8169,6 +8169,10 @@ dependencies = [ "demo-piggy-bank", "demo-ping", "etc", + "ethexe-common", + "ethexe-db", + "ethexe-processor", + "ethexe-runtime-common", "gear-common", "gear-core", "gear-core-errors", @@ -8179,6 +8183,7 @@ dependencies = [ "gear-utils", "gear-workspace-hack", "gprimitives", + "gsigner", "gsys", "log", "parity-scale-codec", @@ -8188,6 +8193,7 @@ dependencies = [ "sha2 0.10.9", "sp-core", "thiserror 2.0.17", + "tokio", "tracing-subscriber", ] diff --git a/gtest/Cargo.toml b/gtest/Cargo.toml index 88cec179e11..fa665c66a33 100644 --- a/gtest/Cargo.toml +++ b/gtest/Cargo.toml @@ -23,6 +23,12 @@ gear-lazy-pages-common.workspace = true gear-lazy-pages-native-interface.workspace = true gear-utils.workspace = true gsys.workspace = true +ethexe-common = { workspace = true, features = ["std"] } +ethexe-db.workspace = true +ethexe-processor.workspace = true +ethexe-runtime-common = { workspace = true, features = ["std"] } +gsigner.workspace = true +tokio = { workspace = true, features = ["rt-multi-thread"] } # General dependencies parity-scale-codec = { workspace = true, features = ["derive"] } @@ -43,7 +49,7 @@ sp-core.workspace = true sha2.workspace = true demo-custom.workspace = true demo-piggy-bank.workspace = true -demo-ping.workspace = true +demo-ping = { workspace = true, features = ["debug", "ethexe"] } demo-futures-unordered.workspace = true demo-constructor = { workspace = true, features = ["std"] } demo-delayed-sender.workspace = true diff --git a/gtest/src/ethexe.rs b/gtest/src/ethexe.rs new file mode 100644 index 00000000000..780ac0d3d04 --- /dev/null +++ b/gtest/src/ethexe.rs @@ -0,0 +1,634 @@ +// This file is part of Gear. + +// Copyright (C) 2026 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use crate::{BLOCK_DURATION_IN_MSECS, Gas, Value, error::usage_panic, log::BlockRunResult}; +use core_processor::configs::BlockInfo; +use ethexe_common::{ + CodeAndIdUnchecked, ProgramStates, Schedule, SimpleBlockData, StateHashWithQueueSize, + db::{CodesStorageRO, CodesStorageRW}, + ecdsa::VerifiedData, + events::{BlockRequestEvent, MirrorRequestEvent, mirror::MessageQueueingRequestedEvent}, + gear::StateTransition, + injected::InjectedTransaction, +}; +use ethexe_db::Database; +use ethexe_processor::{ExecutableData, ProcessedCodeInfo, Processor, ValidCodeInfo}; +use ethexe_runtime_common::{ + RUNTIME_ID, + state::{Program, ProgramState, Storage}, +}; +use gear_core::{ + ids::{ActorId, CodeId, MessageId, prelude::MessageIdExt as _}, + message::ReplyCode, + rpc::ReplyInfo, +}; +use gsigner::secp256k1::Secp256k1SignerExt as _; +use std::{ + collections::{BTreeMap, BTreeSet}, + future::Future, + sync::OnceLock, +}; + +fn block_on(future: F) -> T +where + F: Future + Send, + T: Send, +{ + static RUNTIME: OnceLock = OnceLock::new(); + let runtime = RUNTIME.get_or_init(|| { + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .expect("failed to build ethexe gtest runtime") + }); + + if tokio::runtime::Handle::try_current().is_ok() { + std::thread::scope(|scope| { + scope + .spawn(|| runtime.block_on(future)) + .join() + .expect("ethexe gtest runtime thread panicked") + }) + } else { + runtime.block_on(future) + } +} + +pub(crate) struct EthexeManager { + db: Database, + processor: Processor, + program_states: ProgramStates, + schedule: Schedule, + pending_events: Vec, + pending_injected: Vec>, + block: SimpleBlockData, + message_nonce: u64, + last_executable_balance_burned: Value, +} + +impl EthexeManager { + pub(crate) fn new() -> Self { + // gtest owns this in-memory database for the lifetime of one System. + #[allow(unused_unsafe)] + let db = unsafe { Database::memory() }; + let processor = Processor::new(db.clone()).expect("failed to create ethexe processor"); + + log::debug!(target: "gtest::ethexe", "Initialized ethexe gtest manager"); + + Self { + db, + processor, + program_states: Default::default(), + schedule: Default::default(), + pending_events: Default::default(), + pending_injected: Default::default(), + block: Default::default(), + message_nonce: 0, + last_executable_balance_burned: 0, + } + } + + pub(crate) fn queue_len(&self) -> usize { + let queue_len = self + .program_states + .values() + .map(|state| state.canonical_queue_size as usize + state.injected_queue_size as usize) + .sum::() + + self + .pending_events + .iter() + .filter(|event| { + matches!( + event, + BlockRequestEvent::Mirror { + event: MirrorRequestEvent::MessageQueueingRequested(_), + .. + } + ) + }) + .count() + + self.pending_injected.len(); + + log::trace!(target: "gtest::ethexe", "Ethexe queue length requested: {queue_len}"); + + queue_len + } + + pub(crate) fn program_ids(&self) -> Vec { + self.program_states.keys().copied().collect() + } + + pub(crate) fn block_height(&self) -> u32 { + self.block.header.height + } + + pub(crate) fn block_timestamp(&self) -> u64 { + self.block.header.timestamp + } + + pub(crate) fn store_code(&mut self, code_id: CodeId, code: Vec) { + let original_len = code.len(); + log::debug!( + target: "gtest::ethexe", + "Processing ethexe code {code_id} ({original_len} bytes)" + ); + + let ProcessedCodeInfo { valid, .. } = + block_on(self.processor.process_code(CodeAndIdUnchecked { + code: code.clone(), + code_id, + })) + .expect("failed to process ethexe code"); + + let ValidCodeInfo { + code, + instrumented_code, + code_metadata, + } = valid.expect("provided ethexe code is invalid"); + let instrumented_len = instrumented_code.bytes().len(); + + self.db.set_original_code(&code); + self.db + .set_instrumented_code(RUNTIME_ID, code_id, instrumented_code); + self.db.set_code_metadata(code_id, code_metadata); + self.db.set_code_valid(code_id, true); + + log::debug!( + target: "gtest::ethexe", + "Stored ethexe code {code_id}: original={original_len} bytes, instrumented={} bytes", + instrumented_len + ); + } + + pub(crate) fn original_code(&self, code_id: CodeId) -> Option> { + self.db.original_code(code_id) + } + + pub(crate) fn register_program(&mut self, actor_id: ActorId, code_id: CodeId) { + if self.is_program(actor_id) { + usage_panic!( + "Can't create program with id {actor_id}, because Program with this id already exists. \ + Please, use another id." + ); + } + + self.db.set_program_code_id(actor_id, code_id); + let state = ProgramState::zero(); + let state_hash = self.db.write_program_state(state); + self.program_states.insert( + actor_id, + StateHashWithQueueSize { + hash: state_hash, + canonical_queue_size: 0, + injected_queue_size: 0, + }, + ); + + log::debug!( + target: "gtest::ethexe", + "Registered ethexe program {actor_id} with code {code_id}" + ); + } + + pub(crate) fn is_program(&self, actor_id: ActorId) -> bool { + self.program_states.contains_key(&actor_id) + } + + pub(crate) fn is_active_program(&self, actor_id: ActorId) -> bool { + self.program_states + .get(&actor_id) + .and_then(|entry| self.db.program_state(entry.hash)) + .is_some_and(|state| matches!(state.program, Program::Active(_))) + } + + pub(crate) fn state(&self, actor_id: ActorId) -> ProgramState { + let state = self + .program_states + .get(&actor_id) + .unwrap_or_else(|| panic!("ethexe program {actor_id} not found")); + self.db + .program_state(state.hash) + .expect("ethexe program state missing from database") + } + + pub(crate) fn balance_of(&self, actor_id: ActorId) -> Value { + self.state(actor_id).balance + } + + pub(crate) fn executable_balance_of(&self, actor_id: ActorId) -> Value { + self.state(actor_id).executable_balance + } + + pub(crate) fn top_up_executable_balance(&mut self, actor_id: ActorId, value: Value) { + self.modify_state(actor_id, |state| { + state.executable_balance = state + .executable_balance + .checked_add(value) + .expect("executable balance overflow"); + }); + + log::debug!( + target: "gtest::ethexe", + "Topped up ethexe executable balance for {actor_id}: +{value}, total={}", + self.executable_balance_of(actor_id) + ); + } + + pub(crate) fn top_up_owned_balance(&mut self, actor_id: ActorId, value: Value) { + self.modify_state(actor_id, |state| { + state.balance = state + .balance + .checked_add(value) + .expect("owned balance overflow"); + }); + + log::debug!( + target: "gtest::ethexe", + "Topped up ethexe owned balance for {actor_id}: +{value}, total={}", + self.balance_of(actor_id) + ); + } + + fn modify_state(&mut self, actor_id: ActorId, f: impl FnOnce(&mut ProgramState)) { + let entry = self + .program_states + .get_mut(&actor_id) + .unwrap_or_else(|| panic!("ethexe program {actor_id} not found")); + let mut state = self + .db + .program_state(entry.hash) + .expect("ethexe program state missing from database"); + + f(&mut state); + + entry.canonical_queue_size = state.canonical_queue.cached_queue_size; + entry.injected_queue_size = state.injected_queue.cached_queue_size; + entry.hash = self.db.write_program_state(state); + } + + pub(crate) fn send( + &mut self, + source: ActorId, + destination: ActorId, + payload: Vec, + value: Value, + ) -> MessageId { + if !self.is_active_program(destination) { + usage_panic!("User message can't be sent to non active ethexe program"); + } + let payload_len = payload.len(); + + let message_id = MessageId::generate_from_user( + self.block.header.height.saturating_add(1), + source, + self.message_nonce as u128, + ); + self.message_nonce = self.message_nonce.saturating_add(1); + + self.pending_events.push(BlockRequestEvent::Mirror { + actor_id: destination, + event: MirrorRequestEvent::MessageQueueingRequested(MessageQueueingRequestedEvent { + id: message_id, + source, + payload, + value, + call_reply: false, + }), + }); + + log::debug!( + target: "gtest::ethexe", + "Queued ethexe message {message_id}: source={source}, destination={destination}, payload_len={}, value={value}", + payload_len + ); + + message_id + } + + pub(crate) fn push_event(&mut self, event: BlockRequestEvent) { + log::debug!(target: "gtest::ethexe", "Queued raw ethexe event: {event:?}"); + self.pending_events.push(event); + } + + pub(crate) fn push_injected_transaction(&mut self, tx: InjectedTransaction) { + log::debug!( + target: "gtest::ethexe", + "Queued raw ethexe injected transaction: destination={}, payload_len={}, value={}", + tx.destination, + tx.payload.len(), + tx.value + ); + + let signer = gsigner::secp256k1::Signer::memory(); + let public_key = signer + .generate() + .expect("failed to generate gtest ethexe injected transaction key"); + let tx = signer + .signed_data(public_key, tx, None) + .expect("failed to sign gtest ethexe injected transaction") + .into_verified(); + + self.pending_injected.push(tx); + } + + pub(crate) fn run_new_block( + &mut self, + allowance: Gas, + block_info: BlockInfo, + ) -> BlockRunResult { + let balances_before = self.executable_balances(); + let pending_events = self.pending_events.len(); + let pending_injected = self.pending_injected.len(); + + self.block.header.height = block_info.height; + self.block.header.timestamp = block_info.timestamp; + self.block.hash = gear_core::utils::hash(&self.block.header.height.to_le_bytes()).into(); + + log::debug!( + target: "gtest::ethexe", + "Running ethexe block #{}: allowance={allowance}, pending_events={pending_events}, pending_injected={pending_injected}, programs={}", + self.block.header.height, + self.program_states.len() + ); + + let executable = ExecutableData { + block: self.block, + program_states: self.program_states.clone(), + schedule: self.schedule.clone(), + injected_transactions: core::mem::take(&mut self.pending_injected), + gas_allowance: Some(allowance), + events: core::mem::take(&mut self.pending_events), + }; + + let finalized = block_on(self.processor.process_programs(executable, None)) + .expect("failed to run ethexe block in gtest"); + + self.program_states = finalized.states; + self.schedule = finalized.schedule; + self.last_executable_balance_burned = + executable_balance_burned(balances_before, self.executable_balances()); + + let log = transitions_to_logs(&finalized.transitions); + let succeed = infer_successes(&finalized.transitions); + let failed = infer_failures(&finalized.transitions); + let total_processed = u32::try_from(succeed.len() + failed.len()) + .expect("processed ethexe message count exceeds u32"); + let gas_burned = approximate_gas_burned_from_ethexe( + self.last_executable_balance_burned, + &succeed, + &failed, + ); + let gas_allowance_spent = gas_burned + .values() + .copied() + .fold(0u64, |acc, gas| acc.saturating_add(gas)); + + log::debug!( + target: "gtest::ethexe", + "Finished ethexe block #{}: transitions={}, logs={}, succeed={}, failed={}, executable_balance_burned={}", + self.block.header.height, + finalized.transitions.len(), + log.len(), + succeed.len(), + failed.len(), + self.last_executable_balance_burned + ); + log::trace!( + target: "gtest::ethexe", + "Ethexe block #{} transitions: {:#?}", + self.block.header.height, + finalized.transitions + ); + + BlockRunResult { + block_info: BlockInfo { + height: self.block.header.height, + timestamp: self.block.header.timestamp, + }, + gas_allowance_spent, + succeed, + failed, + not_executed: Default::default(), + total_processed, + log, + gas_burned, + ethexe_executable_balance_burned: self.last_executable_balance_burned, + } + } + + pub(crate) fn calculate_reply_for_handle( + &self, + source: ActorId, + program_id: ActorId, + payload: Vec, + value: Value, + gas_allowance: Gas, + ) -> Result { + let state = self + .program_states + .get(&program_id) + .ok_or_else(|| format!("Program state hash for {program_id} not found")) + .and_then(|state| { + self.db + .program_state(state.hash) + .ok_or_else(|| format!("Program state for {program_id} not found")) + })?; + + if state.requires_init_message() { + return Err(format!("Program {program_id} is not initialized")); + } + + log::debug!( + target: "gtest::ethexe", + "Calculating ethexe reply: source={source}, program_id={program_id}, payload_len={}, value={value}, gas_allowance={gas_allowance}", + payload.len() + ); + + // gtest reply calculation must not commit storage writes or queued dispatches. + let db = unsafe { self.db.clone().overlaid() }; + let mut processor = Processor::new(db.clone()).map_err(|err| err.to_string())?; + let program_states = self.program_states_without_queues(&db); + let mut block = self.block; + block.header.height = block.header.height.saturating_add(1); + block.header.timestamp = block + .header + .timestamp + .saturating_add(BLOCK_DURATION_IN_MSECS); + block.hash = gear_core::utils::hash(&block.header.height.to_le_bytes()).into(); + let message_id = MessageId::generate_from_user(block.header.height, source, u128::MAX); + + let finalized = block_on(processor.process_programs( + ExecutableData { + block, + program_states, + schedule: Default::default(), + injected_transactions: Default::default(), + gas_allowance: Some(gas_allowance), + events: vec![BlockRequestEvent::Mirror { + actor_id: program_id, + event: MirrorRequestEvent::MessageQueueingRequested( + MessageQueueingRequestedEvent { + id: message_id, + source, + payload, + value, + call_reply: false, + }, + ), + }], + }, + None, + )) + .map_err(|err| err.to_string())?; + + let reply = finalized + .transitions + .iter() + .flat_map(|transition| transition.messages.iter()) + .find_map(|message| { + message.reply_details.and_then(|details| { + (details.to_message_id() == message_id).then(|| ReplyInfo { + payload: message.payload.clone(), + value: message.value, + code: details.to_reply_code(), + }) + }) + }) + .ok_or_else(|| "Reply not found".to_string())?; + + log::debug!( + target: "gtest::ethexe", + "Calculated ethexe reply for {message_id}: code={:?}, payload_len={}, value={}", + reply.code, + reply.payload.len(), + reply.value + ); + + Ok(reply) + } + + fn executable_balances(&self) -> BTreeMap { + self.program_states + .keys() + .copied() + .map(|actor_id| (actor_id, self.executable_balance_of(actor_id))) + .collect() + } + + fn program_states_without_queues(&self, db: &Database) -> ProgramStates { + let mut program_states = self.program_states.clone(); + + for state_hash in program_states.values_mut() { + let mut state = db + .program_state(state_hash.hash) + .expect("ethexe program state missing from database"); + let empty = ProgramState::zero(); + state.canonical_queue = empty.canonical_queue; + state.injected_queue = empty.injected_queue; + + state_hash.hash = db.write_program_state(state); + state_hash.canonical_queue_size = 0; + state_hash.injected_queue_size = 0; + } + + program_states + } +} + +fn transitions_to_logs(transitions: &[StateTransition]) -> Vec { + transitions + .iter() + .flat_map(|transition| { + transition.messages.iter().map(|message| { + let reply_code = message.reply_details.map(|details| details.to_reply_code()); + let reply_to = message.reply_details.map(|details| details.to_message_id()); + crate::log::CoreLog::new( + message.id, + transition.actor_id, + message.destination, + message.payload.clone(), + reply_code, + reply_to, + ) + }) + }) + .collect() +} + +fn infer_successes(transitions: &[StateTransition]) -> BTreeSet { + transitions + .iter() + .flat_map(|transition| transition.messages.iter()) + .filter_map(|message| { + message + .reply_details + .filter(|details| matches!(details.to_reply_code(), ReplyCode::Success(_))) + .map(|details| details.to_message_id()) + }) + .collect() +} + +fn infer_failures(transitions: &[StateTransition]) -> BTreeSet { + transitions + .iter() + .flat_map(|transition| transition.messages.iter()) + .filter_map(|message| { + message + .reply_details + .filter(|details| matches!(details.to_reply_code(), ReplyCode::Error(_))) + .map(|details| details.to_message_id()) + }) + .collect() +} + +fn executable_balance_burned( + before: BTreeMap, + after: BTreeMap, +) -> Value { + before + .into_iter() + .map(|(actor_id, before)| { + before.saturating_sub(after.get(&actor_id).copied().unwrap_or_default()) + }) + .sum() +} + +/// Ethexe reports execution cost via executable balance burn, not per-message gas. We map that +/// value to an equivalent gas total (using gtest's [`crate::VALUE_PER_GAS`]) and split it across +/// processed message ids so [`BlockRunResult::spent_value`] stays meaningful in ethexe mode. +fn approximate_gas_burned_from_ethexe( + burned: Value, + succeed: &BTreeSet, + failed: &BTreeSet, +) -> BTreeMap { + let total_u128 = burned / crate::VALUE_PER_GAS; + let Ok(total_gas) = Gas::try_from(total_u128.min(u128::from(u64::MAX))) else { + return BTreeMap::new(); + }; + if total_gas == 0 { + return BTreeMap::new(); + } + + let targets: Vec = succeed.iter().chain(failed.iter()).copied().collect(); + let Ok(n) = u64::try_from(targets.len()) else { + return BTreeMap::new(); + }; + if n == 0 { + return BTreeMap::new(); + } + + let per = total_gas / n; + let mut remainder = total_gas % n; + let mut map = BTreeMap::new(); + for id in targets { + let mut g = per; + if remainder > 0 { + g = g.saturating_add(1); + remainder -= 1; + } + map.insert(id, g); + } + map +} diff --git a/gtest/src/lib.rs b/gtest/src/lib.rs index 01caf9c1e81..650c491d1a5 100644 --- a/gtest/src/lib.rs +++ b/gtest/src/lib.rs @@ -164,6 +164,36 @@ //! cargo test //! ``` //! +//! ## Ethexe execution model +//! +//! [`System`] uses the Vara-compatible execution model by default. Tests that +//! need the ethexe processor can opt in with [`System::builder`] and +//! [`ExecutionModel::Ethexe`]: +//! +//! ```no_run +//! # use gtest::{ExecutionModel, Program, System}; +//! # const WASM_BINARY: &[u8] = &[]; +//! let sys = System::builder() +//! .execution_model(ExecutionModel::Ethexe) +//! .build(); +//! let program = Program::from_binary_with_id(&sys, 0x10000, WASM_BINARY); +//! +//! sys.top_up_executable_balance(program.id(), 200_000_000_000); +//! let message_id = program.send_bytes(10, b"PING"); +//! let result = sys.run_next_block(); +//! # let _ = (message_id, result); +//! ``` +//! +//! In ethexe mode, `gtest` keeps the usual [`System`] and [`Program`] shape but +//! routes code processing and block execution through the real ethexe processor +//! with an in-memory database. Ethexe executable balance is funded with +//! [`System::top_up_executable_balance`], and the value burned in a block is +//! exposed as [`BlockRunResult::ethexe_executable_balance_burned`]. +//! +//! Tests that need raw ethexe inputs can queue block request events with +//! [`System::push_ethexe_event`] or injected transactions with +//! [`System::push_injected_transaction`]. +//! //! # `gtest` capabilities //! //! Let's take a closer look at the `gtest` capabilities. @@ -496,6 +526,7 @@ mod builtins; mod error; +mod ethexe; mod log; mod manager; mod program; @@ -514,7 +545,7 @@ pub use program::{ gbuild::ensure_gbuild, }; pub use state::mailbox::ActorMailbox; -pub use system::System; +pub use system::{ExecutionModel, System, SystemBuilder}; pub use constants::Value; pub(crate) use constants::*; diff --git a/gtest/src/log.rs b/gtest/src/log.rs index 156bbd576c0..b762573e331 100644 --- a/gtest/src/log.rs +++ b/gtest/src/log.rs @@ -73,6 +73,26 @@ impl CoreLog { pub fn reply_to(&self) -> Option { self.reply_to } + + pub(crate) fn new( + id: MessageId, + source: ActorId, + destination: ActorId, + payload: Vec, + reply_code: Option, + reply_to: Option, + ) -> Self { + Self { + id, + source, + destination, + payload: payload.try_into().unwrap_or_else(|_| { + usage_panic!("Log payload exceeds maximum supported gtest payload size") + }), + reply_code, + reply_to, + } + } } impl From for CoreLog { @@ -413,7 +433,13 @@ pub struct BlockRunResult { pub log: Vec, /// Mapping gas burned for each message during /// the current block execution. + /// + /// In ethexe mode this is an approximation derived from + /// [`BlockRunResult::ethexe_executable_balance_burned`] (see source in `gtest::ethexe`), not + /// per-message gas metering from the processor. pub gas_burned: BTreeMap, + /// Value burned from ethexe executable balances during the current block. + pub ethexe_executable_balance_burned: Value, } impl BlockRunResult { diff --git a/gtest/src/manager/block_exec.rs b/gtest/src/manager/block_exec.rs index 5bd43889e3b..40af53eb4b3 100644 --- a/gtest/src/manager/block_exec.rs +++ b/gtest/src/manager/block_exec.rs @@ -172,6 +172,7 @@ impl ExtManager { .map(CoreLog::from) .collect(), gas_burned: mem::take(&mut self.gas_burned), + ethexe_executable_balance_burned: 0, } } diff --git a/gtest/src/program.rs b/gtest/src/program.rs index e9b88d42042..34685539667 100644 --- a/gtest/src/program.rs +++ b/gtest/src/program.rs @@ -234,6 +234,26 @@ impl ProgramBuilder { .unwrap_or_else(|| system.0.borrow_mut().free_id_nonce().into()); let code_id = CodeId::generate(&self.code); + + if let Some(ethexe) = system.ethexe() { + let program_id = id.0; + if default_users_list().contains(&(program_id.into_bytes()[0] as u64)) { + usage_panic!( + "Can't create program with id {id:?}, because it's reserved for default users.\ + Please, use another id." + ) + } + + ethexe.borrow_mut().store_code(code_id, self.code); + ethexe.borrow_mut().register_program(program_id, code_id); + + return Program { + manager: &system.0, + ethexe_manager: system.ethexe(), + id: program_id, + }; + } + system.0.borrow_mut().store_code(code_id, self.code); if let Some(metadata) = self.meta { system @@ -301,6 +321,7 @@ impl ProgramBuilder { /// ``` pub struct Program<'a> { pub(crate) manager: &'a RefCell, + pub(crate) ethexe_manager: Option<&'a RefCell>, pub(crate) id: ActorId, } @@ -329,6 +350,7 @@ impl<'a> Program<'a> { Self { manager: &system.0, + ethexe_manager: system.ethexe(), id: program_id, } } @@ -396,6 +418,10 @@ impl<'a> Program<'a> { T: WasmProgram + 'static, ID: Into + Clone + Debug, { + if system.ethexe().is_some() { + usage_panic!("Mock programs are not available in ethexe execution mode"); + } + // Create a default active program for the mock let primary_program = PrimaryProgram::Active(ActiveProgram { allocations_tree_len: 0, @@ -495,10 +521,23 @@ impl<'a> Program<'a> { ID: Into, T: Into>, { - let mut system = self.manager.borrow_mut(); - let source = from.into().0; + if let Some(ethexe) = self.ethexe_manager { + if gas_limit != MAX_USER_GAS_LIMIT { + usage_panic!( + "Per-message gas_limit is not enforced in ethexe execution mode; only \ + block-level gas allowance applies. Use send_bytes / send_bytes_with_value \ + (which use MAX_USER_GAS_LIMIT) or pass MAX_USER_GAS_LIMIT explicitly." + ); + } + return ethexe + .borrow_mut() + .send(source, self.id, payload.into(), value); + } + + let mut system = self.manager.borrow_mut(); + // The current block number is always a block number of the "executed" block. // So before sending any messages and triggering a block run the block number // equals to 0 (curr). So any new message sent by user goes to a new block, @@ -552,6 +591,10 @@ impl Program<'_> { /// Reads the program’s state as a byte vector. pub fn read_state_bytes(&self, payload: Vec) -> Result> { + if self.ethexe_manager.is_some() { + usage_panic!("State reads are not available in ethexe execution mode yet"); + } + self.manager.borrow_mut().read_state_bytes(payload, self.id) } @@ -563,11 +606,19 @@ impl Program<'_> { /// Returns the balance of the account. pub fn balance(&self) -> Value { + if let Some(ethexe) = self.ethexe_manager { + return ethexe.borrow().balance_of(self.id()); + } + self.manager.borrow().balance_of(self.id()) } /// Save the program's memory to path. pub fn save_memory_dump(&self, path: impl AsRef) { + if self.ethexe_manager.is_some() { + usage_panic!("Memory dumps are not available in ethexe execution mode yet"); + } + let manager = self.manager.borrow(); let mem = manager.read_memory_pages(self.id); let balance = manager.balance_of(self.id); @@ -587,6 +638,10 @@ impl Program<'_> { /// Load the program's memory from path. pub fn load_memory_dump(&mut self, path: impl AsRef) { + if self.ethexe_manager.is_some() { + usage_panic!("Memory dumps are not available in ethexe execution mode yet"); + } + let memory_dump = ProgramMemoryDump::load_from_file(path); let mem = memory_dump .pages diff --git a/gtest/src/system.rs b/gtest/src/system.rs index 0e4d7cb078d..8b4f4ea037c 100644 --- a/gtest/src/system.rs +++ b/gtest/src/system.rs @@ -19,6 +19,7 @@ use crate::{ GAS_ALLOWANCE, Gas, Value, error::usage_panic, + ethexe::EthexeManager, log::{BlockRunResult, CoreLog}, manager::ExtManager, program::{Program, ProgramIdWrapper}, @@ -54,6 +55,42 @@ thread_local! { static SYSTEM_INITIALIZED: RefCell = const { RefCell::new(false) }; } +/// Execution model used by [`System`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ExecutionModel { + /// Default Vara/Substrate-compatible gtest execution. + Vara, + /// Ethexe execution through the real ethexe processor. + Ethexe, +} + +/// Builder for [`System`]. +#[derive(Clone, Copy, Debug)] +pub struct SystemBuilder { + execution_model: ExecutionModel, +} + +impl Default for SystemBuilder { + fn default() -> Self { + Self { + execution_model: ExecutionModel::Vara, + } + } +} + +impl SystemBuilder { + /// Select the execution model for the new [`System`]. + pub fn execution_model(mut self, execution_model: ExecutionModel) -> Self { + self.execution_model = execution_model; + self + } + + /// Build the [`System`]. + pub fn build(self) -> System { + System::with_execution_model(self.execution_model) + } +} + #[derive(Decode)] struct PageKey { _page_storage_prefix: [u8; 32], @@ -99,7 +136,10 @@ impl LazyPagesStorage for PagesStorage { /// // Init logger with "gwasm" target set to `debug` level. /// system.init_logger(); /// ``` -pub struct System(pub(crate) RefCell); +pub struct System( + pub(crate) RefCell, + pub(crate) Option>, +); impl System { /// Prefix for lazy pages. @@ -112,6 +152,15 @@ impl System { /// create. Instantiation of the other one leads to runtime panic. #[allow(clippy::new_without_default)] pub fn new() -> Self { + Self::with_execution_model(ExecutionModel::Vara) + } + + /// Create a [`SystemBuilder`]. + pub fn builder() -> SystemBuilder { + SystemBuilder::default() + } + + fn with_execution_model(execution_model: ExecutionModel) -> Self { SYSTEM_INITIALIZED.with_borrow_mut(|initialized| { if *initialized { panic!("Impossible to have multiple instances of the `System`."); @@ -127,10 +176,26 @@ impl System { *initialized = true; - Self(RefCell::new(ext_manager)) + let ethexe = matches!(execution_model, ExecutionModel::Ethexe) + .then(|| RefCell::new(EthexeManager::new())); + + Self(RefCell::new(ext_manager), ethexe) }) } + /// Return the execution model used by this system. + pub fn execution_model(&self) -> ExecutionModel { + if self.1.is_some() { + ExecutionModel::Ethexe + } else { + ExecutionModel::Vara + } + } + + pub(crate) fn ethexe(&self) -> Option<&RefCell> { + self.1.as_ref() + } + /// Init logger with "gwasm" target set to `debug` level. pub fn init_logger(&self) { self.init_logger_with_default_filter("gwasm=debug"); @@ -157,6 +222,10 @@ impl System { /// Returns amount of dispatches in the queue. pub fn queue_len(&self) -> usize { + if let Some(ethexe) = self.ethexe() { + return ethexe.borrow().queue_len(); + } + self.0.borrow().dispatches.len() } @@ -199,12 +268,35 @@ impl System { ); } + if let Some(ethexe) = self.ethexe() { + let block_info = { + let ext_manager = self.0.borrow_mut(); + ext_manager.blocks_manager.next_block() + }; + return ethexe.borrow_mut().run_new_block(allowance, block_info); + } + self.0.borrow_mut().run_new_block(allowance) } /// Runs blocks same as [`Self::run_next_block`], but executes blocks to /// block number `bn` including it. pub fn run_to_block(&self, bn: u32) -> Vec { + if self.ethexe().is_some() { + let mut current_block = self.block_height(); + if current_block > bn { + usage_panic!("Can't run blocks until bn {bn}, as current bn is {current_block}"); + } + + let mut ret = Vec::with_capacity((bn - current_block) as usize); + while current_block != bn { + ret.push(self.run_next_block_with_allowance(GAS_ALLOWANCE)); + current_block = self.block_height(); + } + + return ret; + } + let mut manager = self.0.borrow_mut(); let mut current_block = manager.block_height(); @@ -226,6 +318,12 @@ impl System { /// Runs `amount` of blocks only with processing task pool, without /// processing the message queue. pub fn run_scheduled_tasks(&self, amount: u32) -> Vec { + if self.ethexe().is_some() { + usage_panic!( + "`run_scheduled_tasks` is not supported in ethexe execution mode; use `run_next_block` instead." + ); + } + let mut manager = self.0.borrow_mut(); let block_height = manager.block_height(); @@ -251,21 +349,40 @@ impl System { /// Return the current block height of the testing environment. pub fn block_height(&self) -> u32 { + if let Some(ethexe) = self.ethexe() { + return ethexe.borrow().block_height(); + } + self.0.borrow().block_height() } /// Return the current block timestamp of the testing environment. pub fn block_timestamp(&self) -> u64 { + if let Some(ethexe) = self.ethexe() { + return ethexe.borrow().block_timestamp(); + } + self.0.borrow().blocks_manager.get().timestamp } /// Returns a [`Program`] by `id`. pub fn get_program>(&self, id: ID) -> Option> { let id = id.into().0; + if let Some(ethexe) = self.ethexe() + && ethexe.borrow().is_program(id) + { + return Some(Program { + id, + manager: &self.0, + ethexe_manager: self.ethexe(), + }); + } + if ProgramsStorageManager::is_program(id) { Some(Program { id, manager: &self.0, + ethexe_manager: None, }) } else { None @@ -279,11 +396,25 @@ impl System { /// Returns a list of programs. pub fn programs(&self) -> Vec> { + if let Some(ethexe) = self.ethexe() { + return ethexe + .borrow() + .program_ids() + .into_iter() + .map(|id| Program { + id, + manager: &self.0, + ethexe_manager: self.ethexe(), + }) + .collect(); + } + ProgramsStorageManager::program_ids() .into_iter() .map(|id| Program { id, manager: &self.0, + ethexe_manager: None, }) .collect() } @@ -295,6 +426,10 @@ impl System { /// exited or terminated that it can't be called anymore. pub fn is_active_program>(&self, id: ID) -> bool { let program_id = id.into().0; + if let Some(ethexe) = self.ethexe() { + return ethexe.borrow().is_active_program(program_id); + } + ProgramsStorageManager::is_active_program(program_id) } @@ -302,6 +437,10 @@ impl System { /// /// Returns [`None`] otherwise. pub fn inheritor_of>(&self, id: ID) -> Option { + if self.ethexe().is_some() { + usage_panic!("`inheritor_of` is not supported in ethexe execution mode yet"); + } + let program_id = id.into().0; ProgramsStorageManager::access_primary_program(program_id, |program| { program.and_then(|program| { @@ -355,6 +494,11 @@ impl System { let code = binary.into(); let code_id = CodeId::generate(code.as_ref()); + if let Some(ethexe) = self.ethexe() { + ethexe.borrow_mut().store_code(code_id, code); + return code_id; + } + // Save original code self.0.borrow_mut().store_code(code_id, code); @@ -363,6 +507,10 @@ impl System { /// Returns previously submitted original code by its code hash. pub fn submitted_code(&self, code_id: CodeId) -> Option> { + if let Some(ethexe) = self.ethexe() { + return ethexe.borrow().original_code(code_id); + } + self.0 .borrow() .original_code(code_id) @@ -374,6 +522,10 @@ impl System { /// The mailbox contains messages from the program that are waiting /// for user action. pub fn get_mailbox>(&self, id: ID) -> ActorMailbox<'_> { + if self.ethexe().is_some() { + usage_panic!("Mailbox helper is not available in ethexe execution mode yet"); + } + let program_id = id.into().0; if !ProgramsStorageManager::is_user(program_id) { usage_panic!("Mailbox available only for users. Please, provide a user id."); @@ -383,6 +535,13 @@ impl System { /// Mint balance to user with given `id` and `value`. pub fn mint_to>(&self, id: ID, value: Value) { + if self.ethexe().is_some() { + usage_panic!( + "`mint_to` is not supported in ethexe execution mode; use `top_up_owned_balance` \ + or `top_up_executable_balance` for programs instead." + ); + } + let id = id.into().0; if ProgramsStorageManager::is_program(id) { @@ -403,6 +562,13 @@ impl System { value: Value, keep_alive: bool, ) { + if self.ethexe().is_some() { + usage_panic!( + "`transfer` is not supported in ethexe execution mode; use runtime message \ + value flow and balance top-up helpers instead." + ); + } + let from = from.into().0; let to = to.into().0; @@ -418,9 +584,69 @@ impl System { /// Returns balance of user with given `id`. pub fn balance_of>(&self, id: ID) -> Value { let actor_id = id.into().0; + if let Some(ethexe) = self.ethexe() { + return if ethexe.borrow().is_program(actor_id) { + ethexe.borrow().balance_of(actor_id) + } else { + 0 + }; + } + self.0.borrow().balance_of(actor_id) } + /// Return ethexe executable balance for `id`. + pub fn executable_balance_of>(&self, id: ID) -> Value { + let id = id.into().0; + if let Some(ethexe) = self.ethexe() { + return if ethexe.borrow().is_program(id) { + ethexe.borrow().executable_balance_of(id) + } else { + 0 + }; + } else { + 0 + } + } + + /// Increase ethexe executable balance for `id`. + pub fn top_up_executable_balance>(&self, id: ID, value: Value) { + let id = id.into().0; + if let Some(ethexe) = self.ethexe() { + ethexe.borrow_mut().top_up_executable_balance(id, value); + } else { + usage_panic!("Executable balance is only available in ethexe execution mode"); + } + } + + /// Increase ethexe owned balance for `id`. + pub fn top_up_owned_balance>(&self, id: ID, value: Value) { + let id = id.into().0; + if let Some(ethexe) = self.ethexe() { + ethexe.borrow_mut().top_up_owned_balance(id, value); + } else { + usage_panic!("Owned balance top-up is only available in ethexe execution mode"); + } + } + + /// Queue a raw ethexe block request event. + pub fn push_ethexe_event(&self, event: ethexe_common::events::BlockRequestEvent) { + if let Some(ethexe) = self.ethexe() { + ethexe.borrow_mut().push_event(event); + } else { + usage_panic!("Raw ethexe events are only available in ethexe execution mode"); + } + } + + /// Queue an injected transaction for ethexe execution. + pub fn push_injected_transaction(&self, tx: ethexe_common::injected::InjectedTransaction) { + if let Some(ethexe) = self.ethexe() { + ethexe.borrow_mut().push_injected_transaction(tx); + } else { + usage_panic!("Injected transactions are only available in ethexe execution mode"); + } + } + /// Calculate reply that would be received when sending /// message to initialized program with any of `Program::send*` methods. pub fn calculate_reply_for_handle( @@ -431,6 +657,20 @@ impl System { gas_limit: u64, value: Value, ) -> Result { + let origin = origin.into().0; + let destination = destination.into().0; + let payload: Vec = payload.into(); + + if let Some(ethexe) = self.ethexe() { + return ethexe.borrow().calculate_reply_for_handle( + origin, + destination, + payload, + value, + gas_limit, + ); + } + let mut manager_mut = self.0.borrow_mut(); // Enter the overlay mode @@ -439,10 +679,7 @@ impl System { // Clear the queue manager_mut.dispatches.clear(); - let origin = origin.into().0; - let destination = destination.into().0; let payload = payload - .into() .try_into() .expect("failed to convert payload to limited payload"); @@ -522,6 +759,8 @@ impl System { impl Drop for System { fn drop(&mut self) { + drop(self.1.take()); + // Uninitialize SYSTEM_INITIALIZED.with_borrow_mut(|initialized| *initialized = false); let manager = self.0.borrow(); @@ -547,9 +786,184 @@ impl Drop for System { #[cfg(test)] mod tests { use super::*; - use crate::{DEFAULT_USER_ALICE, EXISTENTIAL_DEPOSIT, Log, MAX_USER_GAS_LIMIT}; + use crate::{DEFAULT_USER_ALICE, EXISTENTIAL_DEPOSIT, GAS_ALLOWANCE, Log, MAX_USER_GAS_LIMIT}; + use ethexe_common::events::{ + BlockRequestEvent, MirrorRequestEvent, mirror::ExecutableBalanceTopUpRequestedEvent, + }; use gear_core_errors::{ReplyCode, SuccessReplyReason}; + fn init_ethexe_test_logger(sys: &System, test_name: &str) { + sys.init_logger_with_default_filter("gtest=debug,ethexe=debug,gwasm=debug"); + log::debug!(target: "gtest::ethexe", "Running ethexe gtest: {test_name}"); + } + + #[test] + fn system_new_uses_vara_model() { + let sys = System::new(); + + assert_eq!(sys.execution_model(), ExecutionModel::Vara); + } + + #[test] + fn system_builder_can_create_ethexe_model() { + let sys = System::builder() + .execution_model(ExecutionModel::Ethexe) + .build(); + init_ethexe_test_logger(&sys, "system_builder_can_create_ethexe_model"); + + assert_eq!(sys.execution_model(), ExecutionModel::Ethexe); + } + + #[test] + fn ethexe_program_creation_registers_state_and_code() { + let sys = System::builder() + .execution_model(ExecutionModel::Ethexe) + .build(); + init_ethexe_test_logger(&sys, "ethexe_program_creation_registers_state_and_code"); + let program = Program::from_binary_with_id(&sys, 0x10000, demo_ping::WASM_BINARY); + + assert!(sys.is_active_program(program.id())); + assert_eq!(sys.executable_balance_of(program.id()), 0); + + sys.top_up_executable_balance(program.id(), 80_000_000_000); + assert_eq!(sys.executable_balance_of(program.id()), 80_000_000_000); + } + + #[test] + fn ethexe_raw_events_are_processed() { + let sys = System::builder() + .execution_model(ExecutionModel::Ethexe) + .build(); + init_ethexe_test_logger(&sys, "ethexe_raw_events_are_processed"); + let program = Program::from_binary_with_id(&sys, 0x10000, demo_ping::WASM_BINARY); + + sys.push_ethexe_event(BlockRequestEvent::Mirror { + actor_id: program.id(), + event: MirrorRequestEvent::ExecutableBalanceTopUpRequested( + ExecutableBalanceTopUpRequestedEvent { value: 42 }, + ), + }); + sys.run_next_block(); + + assert_eq!(sys.executable_balance_of(program.id()), 42); + } + + #[test] + fn ethexe_executable_balance_of_non_program_is_zero() { + let sys = System::builder() + .execution_model(ExecutionModel::Ethexe) + .build(); + init_ethexe_test_logger(&sys, "ethexe_executable_balance_of_non_program_is_zero"); + + assert_eq!(sys.executable_balance_of(10), 0); + } + + #[test] + #[should_panic(expected = "`inheritor_of` is not supported in ethexe execution mode yet")] + fn ethexe_inheritor_of_panics_as_unsupported() { + let sys = System::builder() + .execution_model(ExecutionModel::Ethexe) + .build(); + init_ethexe_test_logger(&sys, "ethexe_inheritor_of_panics_as_unsupported"); + + let _ = sys.inheritor_of(10); + } + + #[test] + #[should_panic(expected = "`mint_to` is not supported in ethexe execution mode")] + fn ethexe_mint_to_panics_as_unsupported() { + let sys = System::builder() + .execution_model(ExecutionModel::Ethexe) + .build(); + init_ethexe_test_logger(&sys, "ethexe_mint_to_panics_as_unsupported"); + + sys.mint_to(10, EXISTENTIAL_DEPOSIT); + } + + #[test] + #[should_panic(expected = "`transfer` is not supported in ethexe execution mode")] + fn ethexe_transfer_panics_as_unsupported() { + let sys = System::builder() + .execution_model(ExecutionModel::Ethexe) + .build(); + init_ethexe_test_logger(&sys, "ethexe_transfer_panics_as_unsupported"); + + sys.transfer(10, 11, EXISTENTIAL_DEPOSIT, true); + } + + #[test] + fn ethexe_canonical_message_replies_and_charges_executable_balance() { + let sys = System::builder() + .execution_model(ExecutionModel::Ethexe) + .build(); + init_ethexe_test_logger( + &sys, + "ethexe_canonical_message_replies_and_charges_executable_balance", + ); + let program = Program::from_binary_with_id(&sys, 0x10000, demo_ping::WASM_BINARY); + let user = 10; + + sys.top_up_executable_balance(program.id(), 200_000_000_000); + let before = sys.executable_balance_of(program.id()); + + assert_eq!(sys.queue_len(), 0); + let message_id = program.send_bytes(user, b"PING"); + assert_eq!(sys.queue_len(), 1); + let result = sys.run_next_block(); + + assert!(result.succeed.contains(&message_id)); + assert!( + result.contains( + &Log::builder() + .source(program.id()) + .dest(user) + .payload_bytes(b"PONG") + ) + ); + assert!(sys.executable_balance_of(program.id()) < before); + assert!(result.ethexe_executable_balance_burned > 0); + assert_eq!(result.total_processed, 1); + assert_eq!(sys.queue_len(), 0); + + let message_id = program.send_bytes(user, b"PING"); + assert_eq!(sys.queue_len(), 1); + let result = sys.run_next_block(); + + assert!(result.succeed.contains(&message_id)); + assert!( + result.contains( + &Log::builder() + .source(program.id()) + .dest(user) + .payload_bytes(b"PONG") + ) + ); + assert_eq!(result.total_processed, 1); + assert_eq!(sys.queue_len(), 0); + } + + #[test] + fn ethexe_calculate_reply_does_not_commit_state() { + let sys = System::builder() + .execution_model(ExecutionModel::Ethexe) + .build(); + init_ethexe_test_logger(&sys, "ethexe_calculate_reply_does_not_commit_state"); + let program = Program::from_binary_with_id(&sys, 0x10000, demo_ping::WASM_BINARY); + let user = 10; + + sys.top_up_executable_balance(program.id(), 200_000_000_000); + let init = program.send_bytes(user, b"PING"); + assert!(sys.run_next_block().succeed.contains(&init)); + + let before = sys.executable_balance_of(program.id()); + let reply = sys + .calculate_reply_for_handle(user, program.id(), b"PING", GAS_ALLOWANCE, 0) + .expect("reply"); + + assert_eq!(reply.payload, b"PONG"); + assert_eq!(sys.executable_balance_of(program.id()), before); + } + #[test] #[should_panic(expected = "Impossible to have multiple instances of the `System`.")] fn test_system_being_singleton() { diff --git a/utils/gear-workspace-hack/Cargo.toml b/utils/gear-workspace-hack/Cargo.toml index d148da49993..9b18b02b08c 100644 --- a/utils/gear-workspace-hack/Cargo.toml +++ b/utils/gear-workspace-hack/Cargo.toml @@ -779,7 +779,7 @@ errno = { version = "0.3" } gimli = { version = "0.28" } hyper-rustls = { version = "0.27", default-features = false, features = ["aws-lc-rs", "http1", "http2", "logging", "ring", "tls12", "webpki-tokio"] } hyper-util = { version = "0.1", default-features = false, features = ["client-proxy"] } -itertools-a6292c17cd707f01 = { package = "itertools", version = "0.11" } +itertools-594e8ee84c453af0 = { package = "itertools", version = "0.13", default-features = false, features = ["use_std"] } libc = { version = "0.2", default-features = false, features = ["extra_traits"] } miniz_oxide = { version = "0.8", default-features = false, features = ["simd", "with-alloc"] } mio = { version = "1", features = ["net", "os-ext"] } @@ -803,7 +803,7 @@ errno = { version = "0.3" } gimli = { version = "0.28" } hyper-rustls = { version = "0.27", default-features = false, features = ["aws-lc-rs", "http1", "http2", "logging", "ring", "tls12", "webpki-tokio"] } hyper-util = { version = "0.1", default-features = false, features = ["client-proxy"] } -itertools-a6292c17cd707f01 = { package = "itertools", version = "0.11" } +itertools-594e8ee84c453af0 = { package = "itertools", version = "0.13", default-features = false, features = ["use_std"] } libc = { version = "0.2", default-features = false, features = ["extra_traits"] } miniz_oxide = { version = "0.8", default-features = false, features = ["simd", "with-alloc"] } mio = { version = "1", features = ["net", "os-ext"] } @@ -828,7 +828,7 @@ errno = { version = "0.3" } gimli = { version = "0.28" } hyper-rustls = { version = "0.27", default-features = false, features = ["aws-lc-rs", "http1", "http2", "logging", "ring", "tls12", "webpki-tokio"] } hyper-util = { version = "0.1", default-features = false, features = ["client-proxy"] } -itertools-a6292c17cd707f01 = { package = "itertools", version = "0.11" } +itertools-594e8ee84c453af0 = { package = "itertools", version = "0.13", default-features = false, features = ["use_std"] } libc = { version = "0.2", default-features = false, features = ["extra_traits"] } miniz_oxide = { version = "0.8", default-features = false, features = ["simd", "with-alloc"] } mio = { version = "1", features = ["net", "os-ext"] } @@ -851,7 +851,7 @@ errno = { version = "0.3" } gimli = { version = "0.28" } hyper-rustls = { version = "0.27", default-features = false, features = ["aws-lc-rs", "http1", "http2", "logging", "ring", "tls12", "webpki-tokio"] } hyper-util = { version = "0.1", default-features = false, features = ["client-proxy"] } -itertools-a6292c17cd707f01 = { package = "itertools", version = "0.11" } +itertools-594e8ee84c453af0 = { package = "itertools", version = "0.13", default-features = false, features = ["use_std"] } libc = { version = "0.2", default-features = false, features = ["extra_traits"] } miniz_oxide = { version = "0.8", default-features = false, features = ["simd", "with-alloc"] } mio = { version = "1", features = ["net", "os-ext"] } @@ -875,7 +875,7 @@ errno = { version = "0.3" } gimli = { version = "0.28" } hyper-rustls = { version = "0.27", default-features = false, features = ["aws-lc-rs", "http1", "http2", "logging", "ring", "tls12", "webpki-tokio"] } hyper-util = { version = "0.1", default-features = false, features = ["client-proxy"] } -itertools-a6292c17cd707f01 = { package = "itertools", version = "0.11" } +itertools-594e8ee84c453af0 = { package = "itertools", version = "0.13", default-features = false, features = ["use_std"] } libc = { version = "0.2", default-features = false, features = ["extra_traits"] } miniz_oxide = { version = "0.8", default-features = false, features = ["simd", "with-alloc"] } nom = { version = "7" } @@ -897,7 +897,7 @@ errno = { version = "0.3" } gimli = { version = "0.28" } hyper-rustls = { version = "0.27", default-features = false, features = ["aws-lc-rs", "http1", "http2", "logging", "ring", "tls12", "webpki-tokio"] } hyper-util = { version = "0.1", default-features = false, features = ["client-proxy"] } -itertools-a6292c17cd707f01 = { package = "itertools", version = "0.11" } +itertools-594e8ee84c453af0 = { package = "itertools", version = "0.13", default-features = false, features = ["use_std"] } libc = { version = "0.2", default-features = false, features = ["extra_traits"] } miniz_oxide = { version = "0.8", default-features = false, features = ["simd", "with-alloc"] } nom = { version = "7" }