diff --git a/Cargo.lock b/Cargo.lock index 97ecb67b8bcd7..5866ce02e13eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7556,6 +7556,15 @@ dependencies = [ "libc", ] +[[package]] +name = "lz4_flex" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b8c72594ac26bfd34f2d99dfced2edfaddfe8a476e3ff2ca0eb293d925c4f83" +dependencies = [ + "twox-hash", +] + [[package]] name = "lz4_flex" version = "0.11.2" @@ -9900,7 +9909,7 @@ dependencies = [ "flate2", "half 2.3.1", "hashbrown 0.15.2", - "lz4_flex", + "lz4_flex 0.11.2", "num", "num-bigint 0.4.4", "paste", @@ -13508,8 +13517,10 @@ version = "0.1.0" dependencies = [ "anyhow", "bcs", + "csv", "indexmap 2.8.0", "leb128", + "lz4_flex 0.10.0", "move-binary-format", "move-bytecode-utils", "move-bytecode-verifier", @@ -13524,9 +13535,11 @@ dependencies = [ "mysten-common", "mysten-metrics", "nonempty", + "once_cell", "parking_lot 0.12.3", "serde", "serde_json", + "similar", "sui-macros", "sui-move-natives-latest", "sui-protocol-config", diff --git a/Cargo.toml b/Cargo.toml index c4cb7b9697bab..9c12aaafcae46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -611,6 +611,7 @@ versions = "4.1.0" linked-hash-map = "0.5.6" shlex = "1.3.0" starlark = "0.13.0" +lz4_flex = "0.10.0" # Move dependencies move-binary-format = { path = "external-crates/move/crates/move-binary-format" } diff --git a/crates/sui-protocol-config/src/lib.rs b/crates/sui-protocol-config/src/lib.rs index 7136a8939f47b..17c9c0e2a6d92 100644 --- a/crates/sui-protocol-config/src/lib.rs +++ b/crates/sui-protocol-config/src/lib.rs @@ -4560,6 +4560,36 @@ impl ProtocolConfig { self.feature_flags.consensus_batched_block_sync = val; } + /// NB: We are setting a number of feature flags and protocol config fields here to to + /// facilitate testing of PTB execution v2. These feature flags and config fields should be set + /// with or before enabling PTB execution v2 in a real protocol upgrade. + pub fn set_enable_ptb_execution_v2_for_testing(&mut self, val: bool) { + self.feature_flags.enable_ptb_execution_v2 = val; + // Remove this and set these fields when we move this to be set for a specific protocol + // version. + if val { + self.translation_per_command_base_charge = Some(1); + self.translation_per_input_base_charge = Some(1); + self.translation_pure_input_per_byte_charge = Some(1); + self.translation_per_type_node_charge = Some(1); + self.translation_per_reference_node_charge = Some(1); + self.translation_per_linkage_entry_charge = Some(10); + self.gas_model_version = Some(11); + self.feature_flags.abstract_size_in_object_runtime = true; + self.feature_flags.object_runtime_charge_cache_load_gas = true; + self.dynamic_field_hash_type_and_key_cost_base = Some(52); + self.dynamic_field_add_child_object_cost_base = Some(52); + self.dynamic_field_add_child_object_value_cost_per_byte = Some(1); + self.dynamic_field_borrow_child_object_cost_base = Some(52); + self.dynamic_field_borrow_child_object_child_ref_cost_per_byte = Some(1); + self.dynamic_field_remove_child_object_cost_base = Some(52); + self.dynamic_field_remove_child_object_child_cost_per_byte = Some(1); + self.dynamic_field_has_child_object_cost_base = Some(52); + self.dynamic_field_has_child_object_with_ty_cost_base = Some(52); + self.feature_flags.enable_ptb_execution_v2 = true; + } + } + pub fn set_record_time_estimate_processed_for_testing(&mut self, val: bool) { self.feature_flags.record_time_estimate_processed = val; } diff --git a/crates/sui-types/src/gas.rs b/crates/sui-types/src/gas.rs index f57e391b92255..03ca174bba9e7 100644 --- a/crates/sui-types/src/gas.rs +++ b/crates/sui-types/src/gas.rs @@ -58,7 +58,7 @@ pub mod checked { /// Version aware enum for gas status. #[enum_dispatch(SuiGasStatusAPI)] - #[derive(Debug)] + #[derive(Debug, Clone)] pub enum SuiGasStatus { // V1 does not exists any longer as it was a pre mainnet version. // So we start the enum from V2 @@ -121,6 +121,12 @@ pub mod checked { Self::V2(status) => status.gas_price(), } } + + pub fn set_inner_gas_model_version(&mut self, version: u64) { + match self { + Self::V2(status) => status.gas_status.gas_model_version = version, + } + } } /// Summary of the charges in a transaction. diff --git a/crates/sui-types/src/gas_model/gas_v2.rs b/crates/sui-types/src/gas_model/gas_v2.rs index ccbb12b9b6f27..437853311c5f8 100644 --- a/crates/sui-types/src/gas_model/gas_v2.rs +++ b/crates/sui-types/src/gas_model/gas_v2.rs @@ -24,6 +24,7 @@ mod checked { /// After execution a call to `GasStatus::bucketize` will round the computation /// cost to `cost` for the bucket ([`min`, `max`]) the gas used falls into. #[allow(dead_code)] + #[derive(Clone)] pub(crate) struct ComputationBucket { min: u64, max: u64, @@ -81,6 +82,7 @@ mod checked { } /// A list of constant costs of various operations in Sui. + #[derive(Clone)] pub struct SuiCostTable { /// A flat fee charged for every transaction. This is also the minimum amount of /// gas charged for a transaction. @@ -167,7 +169,7 @@ mod checked { } #[allow(dead_code)] - #[derive(Debug)] + #[derive(Debug, Clone)] pub struct SuiGasStatus { // GasStatus as used by the VM, that is all the VM sees pub gas_status: GasStatus, diff --git a/crates/sui-types/src/gas_model/tables.rs b/crates/sui-types/src/gas_model/tables.rs index 7b27a49be5716..f7f45c57cc271 100644 --- a/crates/sui-types/src/gas_model/tables.rs +++ b/crates/sui-types/src/gas_model/tables.rs @@ -43,7 +43,7 @@ pub static INITIAL_COST_SCHEDULE: Lazy = Lazy::new(initial_cost_sched /// /// Every client must use an instance of this type to interact with the Move VM. #[allow(dead_code)] -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct GasStatus { pub gas_model_version: u64, cost_table: CostTable, @@ -70,6 +70,15 @@ pub struct GasStatus { instructions_current_tier_mult: u64, pub num_native_calls: u64, + pub transl_gas: u64, +} + +#[derive(Debug)] +pub struct GasDetails { + pub instructions_executed: u64, + pub stack_height: u64, + pub memory_allocated: u64, + pub transl_gas_used: u64, } impl GasStatus { @@ -106,6 +115,7 @@ impl GasStatus { stack_size_next_tier_start, instructions_next_tier_start, num_native_calls: 0, + transl_gas: 0, } } @@ -133,6 +143,20 @@ impl GasStatus { stack_size_next_tier_start: None, instructions_next_tier_start: None, num_native_calls: 0, + transl_gas: 0, + } + } + + pub fn set_transl_gas(&mut self, transl_gas: u64) { + self.transl_gas = transl_gas + } + + pub fn gas_details(&self) -> GasDetails { + GasDetails { + instructions_executed: self.instructions_executed, + stack_height: self.stack_height_high_water_mark, + memory_allocated: self.stack_size_high_water_mark, + transl_gas_used: self.transl_gas, } } diff --git a/sui-execution/latest/sui-adapter/Cargo.toml b/sui-execution/latest/sui-adapter/Cargo.toml index df9241c8fd2ea..5f02ebbdb8649 100644 --- a/sui-execution/latest/sui-adapter/Cargo.toml +++ b/sui-execution/latest/sui-adapter/Cargo.toml @@ -18,6 +18,9 @@ tracing.workspace = true serde.workspace = true indexmap.workspace = true serde_json.workspace = true +once_cell.workspace = true +csv.workspace = true +lz4_flex = "0.10.0" move-binary-format.workspace = true move-bytecode-utils.workspace = true @@ -37,6 +40,8 @@ sui-verifier = { path = "../sui-verifier", package = "sui-verifier-latest" } move-vm-types = { path = "../../../external-crates/move/crates/move-vm-types" } move-regex-borrow-graph = { path = "../../../external-crates/move/crates/move-regex-borrow-graph" } +similar.workspace = true + sui-macros.workspace = true sui-protocol-config.workspace = true sui-types.workspace = true diff --git a/sui-execution/latest/sui-adapter/src/execution_engine.rs b/sui-execution/latest/sui-adapter/src/execution_engine.rs index 910f7583ab5b7..cdd3f20706764 100644 --- a/sui-execution/latest/sui-adapter/src/execution_engine.rs +++ b/sui-execution/latest/sui-adapter/src/execution_engine.rs @@ -8,10 +8,16 @@ mod checked { use crate::execution_mode::{self, ExecutionMode}; use crate::execution_value::SuiResolver; + use csv::Writer; use move_binary_format::CompiledModule; use move_trace_format::format::MoveTraceBuilder; use move_vm_runtime::move_vm::MoveVM; use mysten_common::debug_fatal; + use once_cell::sync::Lazy; + use similar::TextDiff; + use std::fs::File; + use std::io::BufWriter; + use std::sync::Mutex; use std::{cell::RefCell, collections::HashSet, rc::Rc, sync::Arc}; use sui_types::accumulator_root::{ACCUMULATOR_ROOT_CREATE_FUNC, ACCUMULATOR_ROOT_MODULE}; use sui_types::balance::{ @@ -20,6 +26,7 @@ mod checked { }; use sui_types::execution_params::ExecutionOrEarlyError; use sui_types::gas_coin::GAS; + use sui_types::gas_model::tables::GasDetails; use sui_types::messages_checkpoint::CheckpointTimestamp; use sui_types::metrics::LimitsMetrics; use sui_types::object::OBJECT_START_VERSION; @@ -57,12 +64,12 @@ mod checked { use sui_types::digests::{ ChainIdentifier, get_mainnet_chain_identifier, get_testnet_chain_identifier, }; - use sui_types::effects::TransactionEffects; + use sui_types::effects::{TransactionEffects, TransactionEffectsAPI}; use sui_types::error::{ExecutionError, ExecutionErrorKind}; use sui_types::execution::{ExecutionTiming, ResultWithTimings}; - use sui_types::execution_status::ExecutionStatus; - use sui_types::gas::GasCostSummary; + use sui_types::execution_status::{ExecutionFailureStatus, ExecutionStatus}; use sui_types::gas::SuiGasStatus; + use sui_types::gas::{GasCostSummary, GasUsageReport}; use sui_types::id::UID; use sui_types::inner_temporary_store::InnerTemporaryStore; use sui_types::storage::BackingStore; @@ -71,7 +78,7 @@ mod checked { use sui_types::sui_system_state::{ADVANCE_EPOCH_SAFE_MODE_FUNCTION_NAME, AdvanceEpochParams}; use sui_types::transaction::{ Argument, AuthenticatorStateExpire, AuthenticatorStateUpdate, CallArg, ChangeEpoch, - Command, EndOfEpochTransactionKind, GasData, GenesisTransaction, ObjectArg, + Command, EndOfEpochTransactionKind, GasData, GenesisTransaction, InputObjects, ObjectArg, ProgrammableTransaction, StoredExecutionTimeObservations, TransactionKind, is_gas_paid_from_address_balance, }; @@ -84,6 +91,70 @@ mod checked { sui_system_state::{ADVANCE_EPOCH_FUNCTION_NAME, SUI_SYSTEM_MODULE_NAME}, }; + static CSV_WRITER: Lazy>>> = + once_cell::sync::Lazy::new(|| { + let file = File::create("/opt/sui/gas.csv").expect("failed to create file"); + let enc = BufWriter::new(file); + let mut writer = Writer::from_writer(enc); + writer + .write_record(&[ + "tx_digest", + "gas_budget", + "new_computation_cost", + "old_computation_cost", + "new_storage_cost", + "old_storage_cost", + "new_storage_rebate", + "old_storage_rebate", + "new_gas_used", + "old_gas_used", + "new_instructions", + "old_instructions", + "new_stack_height", + "old_stack_height", + "new_memory_allocated", + "old_memory_allocated", + "translation", + ]) + .expect("failed to write gas header"); + Mutex::new(writer) + }); + + fn write_gas_row( + transaction_digest: String, + new_gas: &GasUsageReport, + new_gas_details: &GasDetails, + old_gas: &GasUsageReport, + old_gas_details: &GasDetails, + ) { + if new_gas.cost_summary == old_gas.cost_summary { + return; + } + let mut writer = CSV_WRITER.lock().unwrap(); + writer + .write_record(&[ + &transaction_digest, + &new_gas.gas_budget.to_string(), + &new_gas.cost_summary.computation_cost.to_string(), + &old_gas.cost_summary.computation_cost.to_string(), + &new_gas.cost_summary.storage_cost.to_string(), + &old_gas.cost_summary.storage_cost.to_string(), + &new_gas.cost_summary.storage_rebate.to_string(), + &old_gas.cost_summary.storage_rebate.to_string(), + &new_gas.gas_used.to_string(), + &old_gas.gas_used.to_string(), + &new_gas_details.instructions_executed.to_string(), + &old_gas_details.instructions_executed.to_string(), + &new_gas_details.stack_height.to_string(), + &old_gas_details.stack_height.to_string(), + &new_gas_details.memory_allocated.to_string(), + &old_gas_details.memory_allocated.to_string(), + &new_gas_details.transl_gas_used.to_string(), + ]) + .expect("failed to write gas row"); + writer.flush().expect("failed to flush gas writer"); + } + #[instrument(name = "tx_execute_to_effects", level = "debug", skip_all)] pub fn execute_transaction_to_effects( store: &dyn BackingStore, @@ -108,7 +179,80 @@ mod checked { Vec, Result, ) { + let mut new_gas_status = gas_status.clone(); + let input_objects = input_objects.into_inner(); + let normal_effects = execute_transaction_to_effects_::( + store, + input_objects.clone(), + gas_data.clone(), + gas_status, + transaction_kind.clone(), + transaction_signer, + transaction_digest, + move_vm, + epoch_id, + epoch_timestamp_ms, + protocol_config, + metrics.clone(), + enable_expensive_checks, + execution_params.clone(), + &mut None, + ); + + let mut new_protocol_config = protocol_config.clone(); + new_protocol_config.set_enable_ptb_execution_v2_for_testing(true); + new_gas_status.set_inner_gas_model_version(new_protocol_config.gas_model_version()); + + let new_effects = execute_transaction_to_effects_::( + store, + input_objects, + gas_data, + new_gas_status, + transaction_kind.clone(), + transaction_signer, + transaction_digest, + move_vm, + epoch_id, + epoch_timestamp_ms, + &new_protocol_config, + metrics, + enable_expensive_checks, + execution_params.clone(), + trace_builder_opt, + ); + + compare_effects::(&normal_effects, &new_effects); + + let (tmp_store, gas_status, effects, timing, result, _gas_details) = normal_effects; + (tmp_store, gas_status, effects, timing, result) + } + + #[instrument(name = "new_tx_execute_to_effects", level = "debug", skip_all)] + fn execute_transaction_to_effects_( + store: &dyn BackingStore, + input_objects: InputObjects, + gas_data: GasData, + gas_status: SuiGasStatus, + transaction_kind: TransactionKind, + transaction_signer: SuiAddress, + transaction_digest: TransactionDigest, + move_vm: &Arc, + epoch_id: &EpochId, + epoch_timestamp_ms: u64, + protocol_config: &ProtocolConfig, + metrics: Arc, + enable_expensive_checks: bool, + execution_params: ExecutionOrEarlyError, + trace_builder_opt: &mut Option, + ) -> ( + InnerTemporaryStore, + SuiGasStatus, + TransactionEffects, + Vec, + Result, + GasDetails, + ) { let mutable_inputs = if enable_expensive_checks { input_objects.all_mutable_inputs().keys().copied().collect() } else { @@ -180,6 +324,8 @@ mod checked { trace_builder_opt, ); + let gas_details = gas_charger.move_gas_status().gas_details(); + let status = if let Err(error) = &execution_result { // Elaborate errors in logs if they are unexpected or their status is terse. use ExecutionErrorKind as K; @@ -274,9 +420,90 @@ mod checked { effects, timings, execution_result, + gas_details, ) } + fn compare_effects( + normal_effects: &( + InnerTemporaryStore, + SuiGasStatus, + TransactionEffects, + Vec, + Result, + GasDetails, + ), + new_effects: &( + InnerTemporaryStore, + SuiGasStatus, + TransactionEffects, + Vec, + Result, + GasDetails, + ), + ) { + let ok = match (normal_effects.2.status(), new_effects.2.status()) { + // success => success + (ExecutionStatus::Success, ExecutionStatus::Success) => true, + // Invariant violation in new + ( + _, + ExecutionStatus::Failure { + error: ExecutionFailureStatus::InvariantViolation, + .. + }, + ) => false, + // failure => failure + ( + ExecutionStatus::Failure { error: _, .. }, + ExecutionStatus::Failure { + error: _other_error, + .. + }, + ) => true, + // Ran out of gas in the new one + ( + _, + ExecutionStatus::Failure { + error: ExecutionFailureStatus::InsufficientGas, + .. + }, + ) => true, + _ => false, + }; + + write_gas_row( + normal_effects.2.transaction_digest().to_string(), + &new_effects.1.gas_usage_report(), + &new_effects.5, + &normal_effects.1.gas_usage_report(), + &normal_effects.5, + ); + + if !ok { + tracing::warn!( + "{} TransactionEffects differ", + normal_effects.2.transaction_digest() + ); + let t1 = format!("{:#?}", normal_effects.2); + let t2 = format!("{:#?}", new_effects.2); + let s = TextDiff::from_lines(&t1, &t2).unified_diff().to_string(); + let data = format!( + "---\nDIGEST: {}\n>>\n{}\n<<<", + normal_effects.2.transaction_digest(), + s, + ); + let output_file = format!("/opt/sui/outputs/{}", normal_effects.2.transaction_digest()); + + std::fs::write(&output_file, &data).expect("Failed to write output file"); + } else { + tracing::info!( + "{} TransactionEffects are the same for both executions", + normal_effects.2.transaction_digest() + ); + } + } + pub fn execute_genesis_state_update( store: &dyn BackingStore, protocol_config: &ProtocolConfig, diff --git a/sui-execution/latest/sui-adapter/src/static_programmable_transactions/metering/translation_meter.rs b/sui-execution/latest/sui-adapter/src/static_programmable_transactions/metering/translation_meter.rs index 910b71eca8040..8b15f9b9ad9c1 100644 --- a/sui-execution/latest/sui-adapter/src/static_programmable_transactions/metering/translation_meter.rs +++ b/sui-execution/latest/sui-adapter/src/static_programmable_transactions/metering/translation_meter.rs @@ -14,6 +14,20 @@ use sui_types::error::{ExecutionError, ExecutionErrorKind}; pub struct TranslationMeter<'pc, 'gas> { protocol_config: &'pc ProtocolConfig, charger: &'gas mut GasCharger, + translation_gas: u64, +} + +impl<'pc, 'gas> Drop for TranslationMeter<'pc, 'gas> { + fn drop(&mut self) { + let final_gas = self.charger.move_gas_status().gas_used_pre_gas_price(); + if final_gas > self.translation_gas { + self.charger.move_gas_status_mut().set_transl_gas( + final_gas + .checked_sub(self.translation_gas) + .expect("translation gas should not fail"), + ); + } + } } impl<'pc, 'gas> TranslationMeter<'pc, 'gas> { @@ -21,9 +35,11 @@ impl<'pc, 'gas> TranslationMeter<'pc, 'gas> { protocol_config: &'pc ProtocolConfig, gas_charger: &'gas mut GasCharger, ) -> TranslationMeter<'pc, 'gas> { + let translation_gas = gas_charger.move_gas_status().gas_used_pre_gas_price(); TranslationMeter { protocol_config, charger: gas_charger, + translation_gas, } } diff --git a/sui-execution/latest/sui-adapter/src/static_programmable_transactions/mod.rs b/sui-execution/latest/sui-adapter/src/static_programmable_transactions/mod.rs index d373206c67032..60fe0f9831b47 100644 --- a/sui-execution/latest/sui-adapter/src/static_programmable_transactions/mod.rs +++ b/sui-execution/latest/sui-adapter/src/static_programmable_transactions/mod.rs @@ -64,6 +64,7 @@ pub fn execute( }; let txn = typing::translate_and_verify::(&mut translation_meter, &env, txn) .map_err(|e| (e, vec![]))?; + drop(translation_meter); execution::interpreter::execute::( &mut env, metrics,