diff --git a/Cargo.lock b/Cargo.lock index 5a2c0ffdc36c1..607ec1412b705 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7869,6 +7869,13 @@ checksum = "1fafa6961cabd9c63bcd77a45d7e3b7f3b552b70417831fb0f56db717e72407e" name = "move-abstract-interpreter" version = "0.1.0" +[[package]] +name = "move-abstract-interpreter-replay_cut" +version = "0.1.0" +dependencies = [ + "itertools 0.13.0", +] + [[package]] name = "move-abstract-interpreter-v2" version = "0.1.0" @@ -7982,6 +7989,21 @@ dependencies = [ "move-vm-config", ] +[[package]] +name = "move-bytecode-verifier-replay_cut" +version = "0.1.0" +dependencies = [ + "move-abstract-interpreter", + "move-abstract-stack", + "move-binary-format", + "move-borrow-graph", + "move-bytecode-verifier-meter", + "move-core-types", + "move-regex-borrow-graph", + "move-vm-config", + "petgraph 0.8.1", +] + [[package]] name = "move-bytecode-verifier-v0" version = "0.1.0" @@ -8507,6 +8529,20 @@ dependencies = [ "walkdir", ] +[[package]] +name = "move-stdlib-natives-replay_cut" +version = "0.1.1" +dependencies = [ + "hex", + "move-binary-format", + "move-core-types", + "move-vm-runtime-replay_cut", + "move-vm-types-replay_cut", + "sha2 0.9.9", + "sha3 0.9.1", + "smallvec", +] + [[package]] name = "move-stdlib-natives-v0" version = "0.1.1" @@ -8676,6 +8712,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "move-vm-runtime-replay_cut" +version = "0.1.0" +dependencies = [ + "better_any", + "fail", + "move-binary-format", + "move-bytecode-verifier-replay_cut", + "move-core-types", + "move-trace-format", + "move-vm-config", + "move-vm-types-replay_cut", + "once_cell", + "parking_lot 0.11.2", + "smallvec", + "tracing", +] + [[package]] name = "move-vm-runtime-v0" version = "0.1.0" @@ -8727,6 +8781,18 @@ dependencies = [ "tracing", ] +[[package]] +name = "move-vm-types-replay_cut" +version = "0.1.0" +dependencies = [ + "bcs", + "move-binary-format", + "move-core-types", + "move-vm-profiler", + "serde", + "smallvec", +] + [[package]] name = "move-vm-types-v0" version = "0.1.0" @@ -13533,6 +13599,39 @@ dependencies = [ "tracing", ] +[[package]] +name = "sui-adapter-replay_cut" +version = "0.1.0" +dependencies = [ + "anyhow", + "bcs", + "indexmap 2.8.0", + "leb128", + "move-binary-format", + "move-bytecode-utils", + "move-bytecode-verifier-meter", + "move-bytecode-verifier-replay_cut", + "move-core-types", + "move-regex-borrow-graph", + "move-trace-format", + "move-vm-config", + "move-vm-profiler", + "move-vm-runtime-replay_cut", + "move-vm-types-replay_cut", + "mysten-common", + "mysten-metrics", + "nonempty", + "parking_lot 0.12.3", + "serde", + "serde_json", + "sui-macros", + "sui-move-natives-replay_cut", + "sui-protocol-config", + "sui-types", + "sui-verifier-replay_cut", + "tracing", +] + [[package]] name = "sui-adapter-transactional-tests" version = "0.1.0" @@ -14396,37 +14495,46 @@ version = "0.1.0" dependencies = [ "cargo_metadata 0.15.4", "move-abstract-interpreter", + "move-abstract-interpreter-replay_cut", "move-abstract-interpreter-v2", "move-binary-format", "move-bytecode-verifier", "move-bytecode-verifier-meter", + "move-bytecode-verifier-replay_cut", "move-bytecode-verifier-v0", "move-bytecode-verifier-v1", "move-bytecode-verifier-v2", "move-trace-format", "move-vm-config", "move-vm-runtime", + "move-vm-runtime-replay_cut", "move-vm-runtime-v0", "move-vm-runtime-v1", "move-vm-runtime-v2", + "move-vm-types-replay_cut", "move-vm-types-v0", "move-vm-types-v1", "move-vm-types-v2", "petgraph 0.5.1", + "similar", "sui-adapter-latest", + "sui-adapter-replay_cut", "sui-adapter-v0", "sui-adapter-v1", "sui-adapter-v2", "sui-move-natives-latest", + "sui-move-natives-replay_cut", "sui-move-natives-v0", "sui-move-natives-v1", "sui-move-natives-v2", "sui-protocol-config", "sui-types", "sui-verifier-latest", + "sui-verifier-replay_cut", "sui-verifier-v0", "sui-verifier-v1", "sui-verifier-v2", + "tracing", ] [[package]] @@ -15670,6 +15778,28 @@ dependencies = [ "tracing", ] +[[package]] +name = "sui-move-natives-replay_cut" +version = "0.1.0" +dependencies = [ + "bcs", + "better_any", + "fastcrypto", + "fastcrypto-vdf", + "fastcrypto-zkp", + "indexmap 2.8.0", + "move-binary-format", + "move-core-types", + "move-stdlib-natives-replay_cut", + "move-vm-runtime-replay_cut", + "move-vm-types-replay_cut", + "rand 0.8.5", + "smallvec", + "sui-protocol-config", + "sui-types", + "tracing", +] + [[package]] name = "sui-move-natives-v0" version = "0.1.0" @@ -16178,6 +16308,7 @@ dependencies = [ "sui-types", "tabled", "telemetry-subscribers", + "tempfile", "thiserror 1.0.69", "tokio", "toml 0.7.4", @@ -17039,6 +17170,21 @@ dependencies = [ "sui-types", ] +[[package]] +name = "sui-verifier-replay_cut" +version = "0.1.0" +dependencies = [ + "move-abstract-stack", + "move-binary-format", + "move-bytecode-utils", + "move-bytecode-verifier-meter", + "move-bytecode-verifier-replay_cut", + "move-core-types", + "move-vm-config", + "sui-protocol-config", + "sui-types", +] + [[package]] name = "sui-verifier-transactional-tests" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 9ef6acf6a85d9..69b8b590b06f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,9 +40,9 @@ exclude = [ "external-crates/move/crates/move-ir-to-bytecode-syntax", "external-crates/move/crates/move-ir-types", "external-crates/move/crates/move-model", - "external-crates/move/crates/move-model-2", + "external-crates/move/crates/move-model-2", "external-crates/move/crates/move-package", - "external-crates/move/crates/move-package-alt", + "external-crates/move/crates/move-package-alt", "external-crates/move/crates/move-proc-macros", "external-crates/move/crates/move-regex-borrow-graph", "external-crates/move/crates/move-stackless-bytecode", @@ -58,6 +58,10 @@ exclude = [ "external-crates/move/crates/move-vm-transactional-tests", "external-crates/move/crates/serializer-tests", "external-crates/move/crates/test-generation", + "external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier", + "external-crates/move/move-execution/replay_cut/crates/move-stdlib-natives", + "external-crates/move/move-execution/replay_cut/crates/move-vm-runtime", + "external-crates/move/move-execution/replay_cut/crates/move-vm-types", "external-crates/move/move-execution/v0/crates/move-bytecode-verifier", "external-crates/move/move-execution/v0/crates/move-stdlib-natives", "external-crates/move/move-execution/v0/crates/move-vm-runtime", @@ -212,6 +216,9 @@ members = [ "sui-execution/latest/sui-adapter", "sui-execution/latest/sui-move-natives", "sui-execution/latest/sui-verifier", + "sui-execution/replay_cut/sui-adapter", + "sui-execution/replay_cut/sui-move-natives", + "sui-execution/replay_cut/sui-verifier", "sui-execution/v0/sui-adapter", "sui-execution/v0/sui-move-natives", "sui-execution/v0/sui-verifier", diff --git a/crates/sui-protocol-config/src/lib.rs b/crates/sui-protocol-config/src/lib.rs index 8ccaba364b6fb..a0d36476a46b2 100644 --- a/crates/sui-protocol-config/src/lib.rs +++ b/crates/sui-protocol-config/src/lib.rs @@ -778,6 +778,14 @@ struct FeatureFlags { #[serde(skip_serializing_if = "is_false")] enable_ptb_execution_v2: bool, + // Enable the new VM + #[serde(skip_serializing_if = "is_false")] + enable_vm_v2: bool, + + // Enable the new adapter + #[serde(skip_serializing_if = "is_false")] + enable_adapter_v2: bool, + // Provide better type resolution errors in the adapter. #[serde(skip_serializing_if = "is_false")] better_adapter_type_resolution_errors: bool, @@ -2525,6 +2533,13 @@ impl ProtocolConfig { ProtocolConfig::get_for_version(ProtocolVersion::MAX, Chain::Unknown) } + pub fn get_use_vm_v2(&self) -> bool { + self.feature_flags.enable_vm_v2 + } + pub fn get_use_adapter_v2(&self) -> bool { + self.feature_flags.enable_adapter_v2 + } + fn get_for_version_impl(version: ProtocolVersion, chain: Chain) -> Self { #[cfg(msim)] { @@ -4533,6 +4548,14 @@ impl ProtocolConfig { self.feature_flags.correct_gas_payment_limit_check = val; } + pub fn set_enable_vm_v2_for_testing(&mut self, val: bool) { + self.feature_flags.enable_vm_v2 = val; + } + + pub fn set_enable_adapter_v2_for_testing(&mut self, val: bool) { + self.feature_flags.enable_adapter_v2 = val; + } + pub fn set_consensus_round_prober_probe_accepted_rounds(&mut self, val: bool) { self.feature_flags .consensus_round_prober_probe_accepted_rounds = val; diff --git a/crates/sui-replay-2/Cargo.toml b/crates/sui-replay-2/Cargo.toml index bf953c054f864..9e79e918b7d62 100644 --- a/crates/sui-replay-2/Cargo.toml +++ b/crates/sui-replay-2/Cargo.toml @@ -52,6 +52,9 @@ tabled.workspace = true [build-dependencies] cynic-codegen.workspace = true +[dev-dependencies] +tempfile.workspace = true + [features] tracing = [ "sui-types/tracing", diff --git a/crates/sui-replay-2/src/execution.rs b/crates/sui-replay-2/src/execution.rs index ad043bc07587c..d17612464b613 100644 --- a/crates/sui-replay-2/src/execution.rs +++ b/crates/sui-replay-2/src/execution.rs @@ -307,32 +307,30 @@ impl sui_types::storage::ObjectStore for ReplayStore<'_> { // at the checkpoint (mimic latest runtime behavior) fn get_object(&self, object_id: &ObjectID) -> Option { trace!("get_object({})", object_id); - - match self.object_cache.borrow().get(object_id) { - Some(versions) => versions.last_key_value().map(|(_version, obj)| obj.clone()), - None => { - let fetched_object = self - .store - .get_objects(&[ObjectKey { - object_id: *object_id, - version_query: VersionQuery::AtCheckpoint(self.checkpoint), - }]) - .map_err(|e| SuiErrorKind::Storage(e.to_string())) - .ok()? - .into_iter() - .next()? - .map(|(obj, _version)| obj)?; - - // Add the fetched object to the cache - let mut cache = self.object_cache.borrow_mut(); - cache - .entry(*object_id) - .or_default() - .insert(fetched_object.version().value(), fetched_object.clone()); - - Some(fetched_object) - } + if let Some(cache) = self.object_cache.borrow().get(object_id) { + return cache.last_key_value().map(|(_version, obj)| obj.clone()); } + + let fetched_object = self + .store + .get_objects(&[ObjectKey { + object_id: *object_id, + version_query: VersionQuery::AtCheckpoint(self.checkpoint), + }]) + .map_err(|e| SuiErrorKind::Storage(e.to_string())) + .ok()? + .into_iter() + .next()? + .map(|(obj, _version)| obj)?; + + // Add the fetched object to the cache + let mut cache = self.object_cache.borrow_mut(); + cache + .entry(*object_id) + .or_default() + .insert(fetched_object.version().value(), fetched_object.clone()); + + Some(fetched_object) } // Get an object by its ID and version diff --git a/crates/sui-replay-2/src/main.rs b/crates/sui-replay-2/src/main.rs index ed84139731948..e49b47717d24f 100644 --- a/crates/sui-replay-2/src/main.rs +++ b/crates/sui-replay-2/src/main.rs @@ -15,6 +15,54 @@ use sui_types::base_types::ObjectID; // Define the `GIT_REVISION` and `VERSION` consts bin_version::bin_version!(); +/// Process package-related commands (rebuild, extract, overwrite) +async fn process_package_command(command: &Command) -> Result<()> { + match command { + Command::RebuildPackage { + package_id, + package_source, + output_path, + node, + } => { + let object_id = + ObjectID::from_str(package_id).map_err(|e| anyhow!("Invalid package ID: {}", e))?; + + rebuild_package( + node.clone(), + object_id, + package_source.clone(), + output_path.clone(), + )?; + + Ok(()) + } + Command::ExtractPackage { + package_id, + output_path, + node, + } => { + let object_id = + ObjectID::from_str(package_id).map_err(|e| anyhow!("Invalid package ID: {}", e))?; + + extract_package(node.clone(), object_id, output_path.clone())?; + + Ok(()) + } + Command::OverwritePackage { + package_id, + package_path, + node, + } => { + let object_id = + ObjectID::from_str(package_id).map_err(|e| anyhow!("Invalid package ID: {}", e))?; + + overwrite_package(node.clone(), object_id, package_path.clone())?; + + Ok(()) + } + } +} + #[tokio::main] async fn main() -> Result<()> { let _guard = telemetry_subscribers::TelemetryConfig::new() @@ -25,50 +73,7 @@ async fn main() -> Result<()> { // Handle subcommands first if let Some(command) = &config.command { - match command { - Command::RebuildPackage { - package_id, - package_source, - output_path, - node, - } => { - let object_id = ObjectID::from_str(package_id) - .map_err(|e| anyhow!("Invalid package ID: {}", e))?; - - rebuild_package( - node.clone(), - object_id, - package_source.clone(), - output_path.clone(), - )?; - - return Ok(()); - } - Command::ExtractPackage { - package_id, - output_path, - node, - } => { - let object_id = ObjectID::from_str(package_id) - .map_err(|e| anyhow!("Invalid package ID: {}", e))?; - - extract_package(node.clone(), object_id, output_path.clone())?; - - return Ok(()); - } - Command::OverwritePackage { - package_id, - package_path, - node, - } => { - let object_id = ObjectID::from_str(package_id) - .map_err(|e| anyhow!("Invalid package ID: {}", e))?; - - overwrite_package(node.clone(), object_id, package_path.clone())?; - - return Ok(()); - } - } + return process_package_command(command).await; } // Handle regular replay mode @@ -88,3 +93,249 @@ async fn main() -> Result<()> { } Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_object_id_parsing_valid() { + // Test that valid object IDs can be parsed + let valid_id = "0x0000000000000000000000000000000000000000000000000000000000000002"; + let result = ObjectID::from_str(valid_id); + assert!(result.is_ok(), "Valid object ID should parse successfully"); + } + + #[test] + fn test_object_id_parsing_invalid() { + // Test that invalid object IDs fail to parse + let invalid_id = "not_a_valid_object_id"; + let result = ObjectID::from_str(invalid_id); + assert!(result.is_err(), "Invalid object ID should fail to parse"); + } + + #[tokio::test] + async fn test_process_package_command_invalid_package_id() { + use std::path::PathBuf; + use sui_replay_2::Node; + + // Test rebuild command with invalid package ID + let command = Command::RebuildPackage { + package_id: "invalid_id".to_string(), + package_source: PathBuf::from("/tmp/source"), + output_path: None, + node: Node::Mainnet, + }; + + let result = process_package_command(&command).await; + assert!( + result.is_err(), + "Processing command with invalid package ID should fail" + ); + assert!( + result + .unwrap_err() + .to_string() + .contains("Invalid package ID"), + "Error message should mention invalid package ID" + ); + } + + #[tokio::test] + async fn test_process_package_command_extract_invalid_id() { + use std::path::PathBuf; + use sui_replay_2::Node; + + // Test extract command with invalid package ID + let command = Command::ExtractPackage { + package_id: "bad_id".to_string(), + output_path: PathBuf::from("/tmp/output"), + node: Node::Testnet, + }; + + let result = process_package_command(&command).await; + assert!( + result.is_err(), + "Extract command with invalid package ID should fail" + ); + } + + #[tokio::test] + async fn test_process_package_command_overwrite_invalid_id() { + use std::path::PathBuf; + use sui_replay_2::Node; + + // Test overwrite command with invalid package ID + let command = Command::OverwritePackage { + package_id: "xyz".to_string(), + package_path: PathBuf::from("/tmp/package"), + node: Node::Mainnet, + }; + + let result = process_package_command(&command).await; + assert!( + result.is_err(), + "Overwrite command with invalid package ID should fail" + ); + } + + #[tokio::test] + async fn test_handle_replay_config_missing_digest_and_path() { + use sui_replay_2::{ + Node, ReplayConfigExperimental, ReplayConfigStableInternal, StoreMode, + handle_replay_config, + }; + + // Create config with neither digest nor digests_path + let stable_config = ReplayConfigStableInternal { + digest: None, + digests_path: None, + terminate_early: false, + trace: false, + output_dir: None, + show_effects: false, + overwrite: false, + }; + + let experimental_config = ReplayConfigExperimental { + node: Node::Mainnet, + verbose: false, + store_mode: StoreMode::GqlOnly, + track_time: false, + cache_executor: false, + }; + + let result = + handle_replay_config(&stable_config, &experimental_config, "test_version").await; + + assert!( + result.is_err(), + "Config without digest or digests_path should fail" + ); + assert!( + result + .unwrap_err() + .to_string() + .contains("either --digest or --digests-path must be provided"), + "Error should mention missing digest or digests_path" + ); + } + + #[tokio::test] + async fn test_handle_replay_config_with_digest() { + use sui_replay_2::{ + Node, ReplayConfigExperimental, ReplayConfigStableInternal, StoreMode, + handle_replay_config, + }; + use tempfile::TempDir; + + // Create a temporary output directory + let temp_dir = TempDir::new().unwrap(); + + // Create config with a digest + let stable_config = ReplayConfigStableInternal { + digest: Some("825koQKtB5ULrX6egKTZm525ermqsNsoJBAxjh45EsE8".to_string()), + digests_path: None, + terminate_early: false, + trace: false, + output_dir: Some(temp_dir.path().to_path_buf()), + show_effects: false, + overwrite: false, + }; + + let experimental_config = ReplayConfigExperimental { + node: Node::Mainnet, + verbose: false, + store_mode: StoreMode::FsOnly, + track_time: false, + cache_executor: false, + }; + + // This will fail because there's no actual transaction to replay, + // but it should get past the validation stage + let result = + handle_replay_config(&stable_config, &experimental_config, "test_version").await; + + // The function should fail during replay, not during config validation + // If it fails with "must be provided" it means validation failed + assert!(result.is_ok(), "Replay failed."); + } + + #[tokio::test] + async fn test_handle_replay_config_with_digests_path() { + use std::io::Write; + use sui_replay_2::{ + Node, ReplayConfigExperimental, ReplayConfigStableInternal, StoreMode, + handle_replay_config, + }; + use tempfile::NamedTempFile; + + let mut digests_file = NamedTempFile::new().unwrap(); + writeln!(digests_file, "825koQKtB5ULrX6egKTZm525ermqsNsoJBAxjh45EsE8").unwrap(); + writeln!( + digests_file, + "DqqRuchzvgXpVdPwi5wvmUR1oxXiEYc74w1sLipvS48n" + ) + .unwrap(); + digests_file.flush().unwrap(); + + let stable_config = ReplayConfigStableInternal { + digest: None, + digests_path: Some(digests_file.path().to_path_buf()), + terminate_early: false, + trace: false, + output_dir: None, + show_effects: false, + overwrite: false, + }; + + let experimental_config = ReplayConfigExperimental { + node: Node::Mainnet, + verbose: false, + store_mode: StoreMode::FsOnly, + track_time: false, + cache_executor: false, + }; + + let result = + handle_replay_config(&stable_config, &experimental_config, "test_version").await; + assert!(result.is_ok(), "Replay failed."); + } + + #[cfg(feature = "tracing")] + #[tokio::test] + async fn test_handle_replay_config_tracing_enabled() { + use sui_replay_2::{ + Node, ReplayConfigExperimental, ReplayConfigStableInternal, StoreMode, + handle_replay_config, + }; + use tempfile::TempDir; + + let temp_dir = TempDir::new().unwrap(); + + // Create config with tracing enabled + let stable_config = ReplayConfigStableInternal { + digest: Some("825koQKtB5ULrX6egKTZm525ermqsNsoJBAxjh45EsE8".to_string()), + digests_path: None, + terminate_early: false, + trace: true, + output_dir: Some(temp_dir.path().to_path_buf()), + show_effects: false, + overwrite: false, + }; + + let experimental_config = ReplayConfigExperimental { + node: Node::Mainnet, + verbose: false, + store_mode: StoreMode::FsOnly, + track_time: false, + cache_executor: false, + }; + + // With tracing feature enabled, this should not fail due to tracing + let result = + handle_replay_config(&stable_config, &experimental_config, "test_version").await; + + assert!(result.is_ok(), "Replay failed."); + } +} diff --git a/crates/sui-types/src/gas.rs b/crates/sui-types/src/gas.rs index f57e391b92255..a06b7e430bc42 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 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 edaef262c4434..61c6201d79721 100644 --- a/crates/sui-types/src/gas_model/tables.rs +++ b/crates/sui-types/src/gas_model/tables.rs @@ -38,7 +38,7 @@ pub static ZERO_COST_SCHEDULE: Lazy = Lazy::new(zero_cost_schedule); /// /// 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, diff --git a/crates/sui-types/src/transaction.rs b/crates/sui-types/src/transaction.rs index 1477d0dadb335..ec8c98e87acb7 100644 --- a/crates/sui-types/src/transaction.rs +++ b/crates/sui-types/src/transaction.rs @@ -3861,6 +3861,7 @@ impl std::fmt::Debug for InputObjects { // An InputObjects new-type that has been verified by sui-transaction-checks, and can be // safely passed to execution. +#[derive(Clone)] pub struct CheckedInputObjects(InputObjects); // DO NOT CALL outside of sui-transaction-checks, genesis, or replay. diff --git a/external-crates/move/Cargo.lock b/external-crates/move/Cargo.lock index 710d359c334eb..a94a93f0fa8c3 100644 --- a/external-crates/move/Cargo.lock +++ b/external-crates/move/Cargo.lock @@ -388,6 +388,21 @@ dependencies = [ "petgraph", ] +[[package]] +name = "bytecode-verifier-tests-replay_cut" +version = "0.1.0" +dependencies = [ + "fail", + "hex", + "move-abstract-interpreter", + "move-binary-format", + "move-bytecode-verifier-meter", + "move-bytecode-verifier-replay_cut", + "move-core-types", + "move-vm-config", + "petgraph", +] + [[package]] name = "bytecode-verifier-tests-v0" version = "0.1.0" @@ -2052,6 +2067,13 @@ dependencies = [ "itertools 0.10.5", ] +[[package]] +name = "move-abstract-interpreter-replay_cut" +version = "0.1.0" +dependencies = [ + "itertools 0.10.5", +] + [[package]] name = "move-abstract-interpreter-v2" version = "0.1.0" @@ -2172,6 +2194,22 @@ dependencies = [ "move-vm-config", ] +[[package]] +name = "move-bytecode-verifier-replay_cut" +version = "0.1.0" +dependencies = [ + "hex-literal", + "move-abstract-interpreter", + "move-abstract-stack", + "move-binary-format", + "move-borrow-graph", + "move-bytecode-verifier-meter", + "move-core-types", + "move-regex-borrow-graph", + "move-vm-config", + "petgraph", +] + [[package]] name = "move-bytecode-verifier-v0" version = "0.1.0" @@ -2801,6 +2839,20 @@ dependencies = [ "walkdir", ] +[[package]] +name = "move-stdlib-natives-replay_cut" +version = "0.1.1" +dependencies = [ + "hex", + "move-binary-format", + "move-core-types", + "move-vm-runtime-replay_cut", + "move-vm-types-replay_cut", + "sha2", + "sha3", + "smallvec", +] + [[package]] name = "move-stdlib-natives-v0" version = "0.1.1" @@ -2984,6 +3036,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "move-vm-runtime-replay_cut" +version = "0.1.0" +dependencies = [ + "anyhow", + "better_any", + "fail", + "hex", + "move-binary-format", + "move-bytecode-verifier-replay_cut", + "move-compiler", + "move-core-types", + "move-ir-compiler", + "move-trace-format", + "move-vm-config", + "move-vm-types-replay_cut", + "once_cell", + "parking_lot 0.11.2", + "proptest", + "smallvec", + "tracing", +] + [[package]] name = "move-vm-runtime-v0" version = "0.1.0" @@ -3059,6 +3134,19 @@ dependencies = [ "move-transactional-test-runner", ] +[[package]] +name = "move-vm-types-replay_cut" +version = "0.1.0" +dependencies = [ + "bcs", + "move-binary-format", + "move-core-types", + "move-vm-profiler", + "proptest", + "serde", + "smallvec", +] + [[package]] name = "move-vm-types-v0" version = "0.1.0" diff --git a/external-crates/move/Cargo.toml b/external-crates/move/Cargo.toml index 714ae725d7391..1025f46595aba 100644 --- a/external-crates/move/Cargo.toml +++ b/external-crates/move/Cargo.toml @@ -6,24 +6,30 @@ members = [ "move-execution/v0/crates/bytecode-verifier-tests", "move-execution/v1/crates/bytecode-verifier-tests", "move-execution/v2/crates/bytecode-verifier-tests", + "move-execution/replay_cut/crates/bytecode-verifier-tests", # "move-execution/$CUT/crates/bytecode-verifier-tests", "move-execution/v0/crates/move-bytecode-verifier", "move-execution/v1/crates/move-bytecode-verifier", "move-execution/v2/crates/move-bytecode-verifier", + "move-execution/replay_cut/crates/move-bytecode-verifier", # "move-execution/$CUT/crates/move-bytecode-verifier", "move-execution/v0/crates/move-vm-runtime", "move-execution/v1/crates/move-vm-runtime", "move-execution/v2/crates/move-vm-runtime", + "move-execution/replay_cut/crates/move-vm-runtime", # "move-execution/$CUT/crates/move-vm-runtime", "move-execution/v0/crates/move-stdlib-natives", "move-execution/v1/crates/move-stdlib-natives", "move-execution/v2/crates/move-stdlib-natives", + "move-execution/replay_cut/crates/move-stdlib-natives", # "move-execution/$CUT/crates/move-stdlib-natives", "move-execution/v2/crates/move-abstract-interpreter", + "move-execution/replay_cut/crates/move-abstract-interpreter", # "move-execution/$CUT/crates/move-abstract-interpreter", "move-execution/v0/crates/move-vm-types", "move-execution/v1/crates/move-vm-types", "move-execution/v2/crates/move-vm-types", + "move-execution/replay_cut/crates/move-vm-types", # "move-execution/$CUT/crates/move-vm-types", ] diff --git a/external-crates/move/crates/move-binary-format/src/file_format_common.rs b/external-crates/move/crates/move-binary-format/src/file_format_common.rs index 8cc696bceee97..064bfe9197873 100644 --- a/external-crates/move/crates/move-binary-format/src/file_format_common.rs +++ b/external-crates/move/crates/move-binary-format/src/file_format_common.rs @@ -657,6 +657,12 @@ pub fn instruction_opcode(instruction: &Bytecode) -> Opcodes { } } +impl Into for &Bytecode { + fn into(self) -> Opcodes { + instruction_opcode(self) + } +} + /// The encoding of the instruction is the serialized form of it, but disregarding the /// serialization of the instruction's argument(s). pub fn instruction_key(instruction: &Bytecode) -> u8 { diff --git a/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/Cargo.toml b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/Cargo.toml new file mode 100644 index 0000000000000..8ec0f1defa66f --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "bytecode-verifier-tests-replay_cut" +version = "0.1.0" +authors = ["Diem Association "] +description = "Diem bytecode verifier tests" +repository = "https://github.com/diem/diem" +homepage = "https://diem.com" +license = "Apache-2.0" +publish = false +edition = "2024" + +[dev-dependencies] +petgraph.workspace = true +fail = { workspace = true, features = ["failpoints"] } +hex.workspace = true + +move-binary-format = { workspace = true, features = ["fuzzing"] } +# referred to via path for execution versioning +move-bytecode-verifier = { path = "../move-bytecode-verifier", package = "move-bytecode-verifier-replay_cut" } +move-bytecode-verifier-meter.workspace = true +move-core-types.workspace = true +move-vm-config.workspace = true +move-abstract-interpreter.workspace = true + +[features] +fuzzing = ["move-binary-format/fuzzing"] diff --git a/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/METER_TESTING.md b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/METER_TESTING.md new file mode 100644 index 0000000000000..2f1865f6ed685 --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/METER_TESTING.md @@ -0,0 +1,5 @@ +This testsuite can be run in a specific way to print the time until a 'complex' program is detected or accepted. Call as in: + +``` +cargo test --release -- --nocapture 1>/dev/null +``` diff --git a/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/lib.rs b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/lib.rs new file mode 100644 index 0000000000000..211d2cd3a769b --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/lib.rs @@ -0,0 +1,11 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +#![forbid(unsafe_code)] + +#[cfg(test)] +pub mod support; + +#[cfg(test)] +pub mod unit_tests; diff --git a/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/support/mod.rs b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/support/mod.rs new file mode 100644 index 0000000000000..9bc165217a056 --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/support/mod.rs @@ -0,0 +1,36 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +use move_binary_format::{ + CompiledModule, + file_format::{ + Bytecode, CodeUnit, FunctionDefinition, FunctionHandle, IdentifierIndex, ModuleHandleIndex, + SignatureIndex, empty_module, + }, +}; + +/// Create a dummy module to wrap the bytecode program in local@code +pub fn dummy_procedure_module(code: Vec) -> CompiledModule { + let mut module = empty_module(); + let code_unit = CodeUnit { + code, + ..Default::default() + }; + let fun_def = FunctionDefinition { + code: Some(code_unit), + ..Default::default() + }; + + let fun_handle = FunctionHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(0), + parameters: SignatureIndex(0), + return_: SignatureIndex(0), + type_parameters: vec![], + }; + + module.function_handles.push(fun_handle); + module.function_defs.push(fun_def); + module +} diff --git a/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/binary_samples.rs b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/binary_samples.rs new file mode 100644 index 0000000000000..1ae42dacbb25e --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/binary_samples.rs @@ -0,0 +1,38 @@ +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +//! Tests in here are based on binary representation of modules taken from production. Those tests +//! may fail over time if the representation becomes out of date, then they can be removed. +//! Right now the serve to calibrate the metering working as expected. Those tests represent +//! cases which we want to continue to succeed. + +use crate::unit_tests::production_config; +use move_binary_format::{CompiledModule, binary_config::BinaryConfig, errors::VMResult}; +use move_bytecode_verifier::verifier; +use move_bytecode_verifier_meter::bound::BoundMeter; + +#[allow(unused)] +fn run_binary_test(name: &str, bytes: &str) -> VMResult<()> { + let bytes = hex::decode(bytes).expect("invalid hex string"); + let config = BinaryConfig::legacy_with_flags( + /* check_no_extraneous_bytes */ true, /* deprecate_global_storage_ops */ false, + ); + let m = CompiledModule::deserialize_with_config(&bytes, &config).expect("invalid module"); + let (verifier_config, meter_config) = production_config(); + let mut meter = BoundMeter::new(meter_config); + verifier::verify_module_with_config_for_test(name, &verifier_config, &m, &mut meter) +} + +macro_rules! do_test { + ($name:expr) => {{ + let name = $name; + let code = std::fs::read_to_string(format!("tests/binaries/{name}.bytes")).unwrap(); + let res = run_binary_test(name, &code); + assert!(res.is_ok(), "{:?}", res) + }}; +} + +#[test] +fn router() { + do_test!("router"); +} diff --git a/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/bounds_tests.proptest-regressions b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/bounds_tests.proptest-regressions new file mode 100644 index 0000000000000..3945bd370c9d0 --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/bounds_tests.proptest-regressions @@ -0,0 +1,10 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 2beb0a0e65962432af560e626fa109d269b07db8807968413425f0bb14bb3667 # shrinks to module = CompiledModule: { datatype_handles: [ DatatypeHandle { module: ModuleHandleIndex(0), name: IdentifierIndex(0), is_resource: false }, DatatypeHandle { module: ModuleHandleIndex(0), name: IdentifierIndex(0), is_resource: false },] function_handles: [ FunctionHandle { module: ModuleHandleIndex(0), name: IdentifierIndex(0), signature: FunctionSignatureIndex(0) }, FunctionHandle { module: ModuleHandleIndex(0), name: IdentifierIndex(0), signature: FunctionSignatureIndex(1) },] struct_defs: [ StructDefinition { struct_handle: 1, access: 0x4, field_count: 0, fields: 0 },] field_defs: [] function_defs: [ FunctionDefinition { function: 1, access: 0x2, code: CodeUnit { max_stack_size: 0, locals: 0 code: [] } },] type_signatures: [ TypeSignature(Unit),] function_signatures: [ FunctionSignature { return_type: Unit, arg_types: [] }, FunctionSignature { return_type: Unit, arg_types: [] },] locals_signatures: [ LocalsSignature([]),] string_pool: [ "",] address_pool: [ Address([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),] } +cc c14ae393a6eefae82c0f4ede2acaa0aa0e993c1bba3fe3e5958e6e31cb5d2957 # shrinks to module = CompiledModule: { module_handles: [ ModuleHandle { address: AddressPoolIndex(0), name: IdentifierIndex(0) },] datatype_handles: [ DatatypeHandle { module: ModuleHandleIndex(0), name: IdentifierIndex(0), is_resource: false }, DatatypeHandle { module: ModuleHandleIndex(0), name: IdentifierIndex(0), is_resource: false },] function_handles: [ FunctionHandle { module: ModuleHandleIndex(0), name: IdentifierIndex(0), signature: FunctionSignatureIndex(0) }, FunctionHandle { module: ModuleHandleIndex(0), name: IdentifierIndex(0), signature: FunctionSignatureIndex(1) },] struct_defs: [ StructDefinition { struct_handle: 1, access: 0x4, field_count: 0, fields: 0 },] field_defs: [] function_defs: [ FunctionDefinition { function: 1, access: 0x2, code: CodeUnit { max_stack_size: 0, locals: 0 code: [] } },] type_signatures: [ TypeSignature(Unit),] function_signatures: [ FunctionSignature { return_type: Unit, arg_types: [] }, FunctionSignature { return_type: Unit, arg_types: [] },] locals_signatures: [ LocalsSignature([]),] string_pool: [ "",] address_pool: [ Address([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),] } , oob_mutations = [] +cc 88615e15ef42d29405cd91d6d0a573ccbeb833d0c7471f718ee794bc5ba399ca # shrinks to module = CompiledModule: { module_handles: [ ModuleHandle { address: AddressPoolIndex(0), name: IdentifierIndex(0) },] datatype_handles: [ DatatypeHandle { module: ModuleHandleIndex(0), name: IdentifierIndex(0), is_resource: false }, DatatypeHandle { module: ModuleHandleIndex(0), name: IdentifierIndex(0), is_resource: false }, DatatypeHandle { module: ModuleHandleIndex(0), name: IdentifierIndex(0), is_resource: false },] function_handles: [ FunctionHandle { module: ModuleHandleIndex(0), name: IdentifierIndex(0), signature: FunctionSignatureIndex(0) }, FunctionHandle { module: ModuleHandleIndex(0), name: IdentifierIndex(0), signature: FunctionSignatureIndex(1) },] struct_defs: [ StructDefinition { struct_handle: 1, access: 0x4, field_count: 0, fields: 0 }, StructDefinition { struct_handle: 2, access: 0x4, field_count: 0, fields: 0 },] field_defs: [] function_defs: [ FunctionDefinition { function: 1, access: 0x2, code: CodeUnit { max_stack_size: 0, locals: 0 code: [] } },] type_signatures: [ TypeSignature(Unit),] function_signatures: [ FunctionSignature { return_type: Unit, arg_types: [] }, FunctionSignature { return_type: Unit, arg_types: [] },] locals_signatures: [ LocalsSignature([]),] string_pool: [ "",] address_pool: [ Address([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),] } , oob_mutations = [OutOfBoundsMutation { src_kind: StructDefinition, src_idx: Index(0), dst_kind: FieldDefinition, offset: 0 }] +cc a34039f5d57751762a6eacf3ca3a2857781fb0bd0af0b7a06a9427f896f29aa9 # shrinks to module = CompiledModule: { module_handles: [ ModuleHandle { address: AddressPoolIndex(0), name: IdentifierIndex(0) },] datatype_handles: [ DatatypeHandle { module: ModuleHandleIndex(0), name: IdentifierIndex(0), is_resource: false }, DatatypeHandle { module: ModuleHandleIndex(0), name: IdentifierIndex(0), is_resource: false },] function_handles: [ FunctionHandle { module: ModuleHandleIndex(0), name: IdentifierIndex(0), signature: FunctionSignatureIndex(0) }, FunctionHandle { module: ModuleHandleIndex(0), name: IdentifierIndex(0), signature: FunctionSignatureIndex(1) },] struct_defs: [ StructDefinition { struct_handle: 1, access: 0x2, field_count: 0, fields: 0 },] field_defs: [] function_defs: [ FunctionDefinition { function: 1, access: 0x0, code: CodeUnit { max_stack_size: 0, locals: 0 code: [ BrTrue(1),] } },] type_signatures: [ TypeSignature(Unit), TypeSignature(Unit),] function_signatures: [ FunctionSignature { return_type: Unit, arg_types: [] }, FunctionSignature { return_type: Unit, arg_types: [] },] locals_signatures: [ LocalsSignature([]),] string_pool: [ "",] address_pool: [ Address([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),] } , oob_mutations = [] diff --git a/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/bounds_tests.rs b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/bounds_tests.rs new file mode 100644 index 0000000000000..34fe0a08cce52 --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/bounds_tests.rs @@ -0,0 +1,406 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +use move_binary_format::{check_bounds::BoundsChecker, file_format::*, file_format_common}; +use move_core_types::vm_status::StatusCode; + +#[test] +fn empty_module_no_errors() { + BoundsChecker::verify_module( + &basic_test_module(), + /* deprecate_global_storage_ops */ true, + ) + .unwrap(); +} + +#[test] +fn invalid_default_module() { + BoundsChecker::verify_module( + &CompiledModule { + version: file_format_common::VERSION_MAX, + publishable: true, + ..Default::default() + }, + /* deprecate_global_storage_ops */ true, + ) + .unwrap_err(); +} + +#[test] +fn invalid_self_module_handle_index() { + let mut m = basic_test_module(); + m.self_module_handle_idx = ModuleHandleIndex(12); + assert_eq!( + BoundsChecker::verify_module(&m, /* deprecate_global_storage_ops */ true) + .unwrap_err() + .major_status(), + StatusCode::INDEX_OUT_OF_BOUNDS + ); +} + +#[test] +fn invalid_type_param_in_fn_return_() { + use SignatureToken::*; + + let mut m = basic_test_module(); + m.function_handles[0].return_ = SignatureIndex(1); + m.signatures.push(Signature(vec![TypeParameter(0)])); + assert_eq!(m.signatures.len(), 2); + assert_eq!( + BoundsChecker::verify_module(&m, /* deprecate_global_storage_ops */ true) + .unwrap_err() + .major_status(), + StatusCode::INDEX_OUT_OF_BOUNDS + ); +} + +#[test] +fn invalid_type_param_in_fn_parameters() { + use SignatureToken::*; + + let mut m = basic_test_module(); + m.function_handles[0].parameters = SignatureIndex(1); + m.signatures.push(Signature(vec![TypeParameter(0)])); + assert_eq!( + BoundsChecker::verify_module(&m, /* deprecate_global_storage_ops */ true) + .unwrap_err() + .major_status(), + StatusCode::INDEX_OUT_OF_BOUNDS + ); +} + +#[test] +fn invalid_struct_in_fn_return_() { + use SignatureToken::*; + + let mut m = basic_test_module(); + m.function_handles[0].return_ = SignatureIndex(1); + m.signatures + .push(Signature(vec![Datatype(DatatypeHandleIndex::new(1))])); + assert_eq!( + BoundsChecker::verify_module(&m, /* deprecate_global_storage_ops */ true) + .unwrap_err() + .major_status(), + StatusCode::INDEX_OUT_OF_BOUNDS + ); +} + +#[test] +fn invalid_type_param_in_field() { + use SignatureToken::*; + + let mut m = basic_test_module(); + match &mut m.struct_defs[0].field_information { + StructFieldInformation::Declared(fields) => { + fields[0].signature.0 = TypeParameter(0); + assert_eq!( + BoundsChecker::verify_module(&m, /* deprecate_global_storage_ops */ true) + .unwrap_err() + .major_status(), + StatusCode::INDEX_OUT_OF_BOUNDS + ); + } + _ => panic!("attempt to change a field that does not exist"), + } +} + +#[test] +fn invalid_struct_in_field() { + use SignatureToken::*; + + let mut m = basic_test_module(); + match &mut m.struct_defs[0].field_information { + StructFieldInformation::Declared(fields) => { + fields[0].signature.0 = Datatype(DatatypeHandleIndex::new(3)); + assert_eq!( + BoundsChecker::verify_module(&m, /* deprecate_global_storage_ops */ true) + .unwrap_err() + .major_status(), + StatusCode::INDEX_OUT_OF_BOUNDS + ); + } + _ => panic!("attempt to change a field that does not exist"), + } +} + +#[test] +fn invalid_struct_with_actuals_in_field() { + use SignatureToken::*; + + let mut m = basic_test_module(); + match &mut m.struct_defs[0].field_information { + StructFieldInformation::Declared(fields) => { + fields[0].signature.0 = DatatypeInstantiation(Box::new(( + DatatypeHandleIndex::new(0), + vec![TypeParameter(0)], + ))); + assert_eq!( + BoundsChecker::verify_module(&m, /* deprecate_global_storage_ops */ true) + .unwrap_err() + .major_status(), + StatusCode::NUMBER_OF_TYPE_ARGUMENTS_MISMATCH + ); + } + _ => panic!("attempt to change a field that does not exist"), + } +} + +#[test] +fn invalid_locals_id_in_call() { + use Bytecode::*; + + let mut m = basic_test_module(); + m.function_instantiations.push(FunctionInstantiation { + handle: FunctionHandleIndex::new(0), + type_parameters: SignatureIndex::new(1), + }); + let func_inst_idx = FunctionInstantiationIndex(m.function_instantiations.len() as u16 - 1); + m.function_defs[0].code.as_mut().unwrap().code = vec![CallGeneric(func_inst_idx)]; + assert_eq!( + BoundsChecker::verify_module(&m, /* deprecate_global_storage_ops */ true) + .unwrap_err() + .major_status(), + StatusCode::INDEX_OUT_OF_BOUNDS + ); +} + +#[test] +fn invalid_type_param_in_call() { + use Bytecode::*; + use SignatureToken::*; + + let mut m = basic_test_module(); + m.signatures.push(Signature(vec![TypeParameter(0)])); + m.function_instantiations.push(FunctionInstantiation { + handle: FunctionHandleIndex::new(0), + type_parameters: SignatureIndex::new(1), + }); + let func_inst_idx = FunctionInstantiationIndex(m.function_instantiations.len() as u16 - 1); + m.function_defs[0].code.as_mut().unwrap().code = vec![CallGeneric(func_inst_idx)]; + assert_eq!( + BoundsChecker::verify_module(&m, /* deprecate_global_storage_ops */ true) + .unwrap_err() + .major_status(), + StatusCode::INDEX_OUT_OF_BOUNDS + ); +} + +#[test] +fn invalid_struct_as_type_actual_in_exists() { + use Bytecode::*; + use SignatureToken::*; + + let mut m = basic_test_module(); + m.signatures + .push(Signature(vec![Datatype(DatatypeHandleIndex::new(3))])); + m.function_instantiations.push(FunctionInstantiation { + handle: FunctionHandleIndex::new(0), + type_parameters: SignatureIndex::new(1), + }); + let func_inst_idx = FunctionInstantiationIndex(m.function_instantiations.len() as u16 - 1); + m.function_defs[0].code.as_mut().unwrap().code = vec![CallGeneric(func_inst_idx)]; + assert_eq!( + BoundsChecker::verify_module(&m, /* deprecate_global_storage_ops */ true) + .unwrap_err() + .major_status(), + StatusCode::INDEX_OUT_OF_BOUNDS + ); +} + +#[test] +fn invalid_friend_module_address() { + let mut m = basic_test_module(); + m.friend_decls.push(ModuleHandle { + address: AddressIdentifierIndex::new(m.address_identifiers.len() as TableIndex), + name: IdentifierIndex::new(0), + }); + assert_eq!( + BoundsChecker::verify_module(&m, /* deprecate_global_storage_ops */ true) + .unwrap_err() + .major_status(), + StatusCode::INDEX_OUT_OF_BOUNDS + ); +} + +#[test] +fn invalid_friend_module_name() { + let mut m = basic_test_module(); + m.friend_decls.push(ModuleHandle { + address: AddressIdentifierIndex::new(0), + name: IdentifierIndex::new(m.identifiers.len() as TableIndex), + }); + assert_eq!( + BoundsChecker::verify_module(&m, /* deprecate_global_storage_ops */ true) + .unwrap_err() + .major_status(), + StatusCode::INDEX_OUT_OF_BOUNDS + ); +} + +#[test] +fn invalid_signature_for_vector_operation() { + use Bytecode::*; + + let skeleton = basic_test_module(); + let sig_index = SignatureIndex(skeleton.signatures.len() as u16); + for bytecode in [ + VecPack(sig_index, 0), + VecLen(sig_index), + VecImmBorrow(sig_index), + VecMutBorrow(sig_index), + VecPushBack(sig_index), + VecPopBack(sig_index), + VecUnpack(sig_index, 0), + VecSwap(sig_index), + ] { + let mut m = skeleton.clone(); + m.function_defs[0].code.as_mut().unwrap().code = vec![bytecode]; + assert_eq!( + BoundsChecker::verify_module(&m, /* deprecate_global_storage_ops */ true) + .unwrap_err() + .major_status(), + StatusCode::INDEX_OUT_OF_BOUNDS + ); + } +} + +#[test] +fn invalid_struct_for_vector_operation() { + use Bytecode::*; + use SignatureToken::*; + + let mut skeleton = basic_test_module(); + skeleton + .signatures + .push(Signature(vec![Datatype(DatatypeHandleIndex::new(3))])); + let sig_index = SignatureIndex((skeleton.signatures.len() - 1) as u16); + for bytecode in [ + VecPack(sig_index, 0), + VecLen(sig_index), + VecImmBorrow(sig_index), + VecMutBorrow(sig_index), + VecPushBack(sig_index), + VecPopBack(sig_index), + VecUnpack(sig_index, 0), + VecSwap(sig_index), + ] { + let mut m = skeleton.clone(); + m.function_defs[0].code.as_mut().unwrap().code = vec![bytecode]; + assert_eq!( + BoundsChecker::verify_module(&m, /* deprecate_global_storage_ops */ true) + .unwrap_err() + .major_status(), + StatusCode::INDEX_OUT_OF_BOUNDS + ); + } +} + +#[test] +fn invalid_type_param_for_vector_operation() { + use Bytecode::*; + use SignatureToken::*; + + let mut skeleton = basic_test_module(); + skeleton.signatures.push(Signature(vec![TypeParameter(0)])); + let sig_index = SignatureIndex((skeleton.signatures.len() - 1) as u16); + for bytecode in [ + VecPack(sig_index, 0), + VecLen(sig_index), + VecImmBorrow(sig_index), + VecMutBorrow(sig_index), + VecPushBack(sig_index), + VecPopBack(sig_index), + VecUnpack(sig_index, 0), + VecSwap(sig_index), + ] { + let mut m = skeleton.clone(); + m.function_defs[0].code.as_mut().unwrap().code = vec![bytecode]; + assert_eq!( + BoundsChecker::verify_module(&m, /* deprecate_global_storage_ops */ true) + .unwrap_err() + .major_status(), + StatusCode::INDEX_OUT_OF_BOUNDS + ); + } +} + +#[test] +fn invalid_variant_handle_index_for_enum_operation() { + use Bytecode::*; + + let skeleton = basic_test_module(); + let variant_handle_index = VariantHandleIndex(skeleton.variant_handles.len() as u16); + let variant_handle_inst_index = + VariantInstantiationHandleIndex(skeleton.variant_instantiation_handles.len() as u16); + for bytecode in [ + PackVariant(variant_handle_index), + UnpackVariant(variant_handle_index), + UnpackVariantImmRef(variant_handle_index), + UnpackVariantMutRef(variant_handle_index), + PackVariantGeneric(variant_handle_inst_index), + UnpackVariantGeneric(variant_handle_inst_index), + UnpackVariantGenericImmRef(variant_handle_inst_index), + UnpackVariantGenericMutRef(variant_handle_inst_index), + ] { + let mut m = skeleton.clone(); + m.function_defs[0].code.as_mut().unwrap().code = vec![bytecode]; + assert_eq!( + BoundsChecker::verify_module(&m, /* deprecate_global_storage_ops */ true) + .unwrap_err() + .major_status(), + StatusCode::INDEX_OUT_OF_BOUNDS + ); + } +} + +#[test] +fn invalid_variant_jump_table_index() { + use Bytecode::*; + + let skeleton = basic_test_module(); + let jt_index = VariantJumpTableIndex( + skeleton.function_defs[0] + .code + .as_ref() + .map(|c| c.jump_tables.len() as u16) + .unwrap_or(0u16), + ); + let mut m = skeleton.clone(); + m.function_defs[0].code.as_mut().unwrap().code = vec![VariantSwitch(jt_index)]; + assert_eq!( + BoundsChecker::verify_module(&m, /* deprecate_global_storage_ops */ true) + .unwrap_err() + .major_status(), + StatusCode::INDEX_OUT_OF_BOUNDS + ); +} + +#[test] +fn invalid_variant_jump_table_code_offset() { + use Bytecode::*; + + let mut skeleton = basic_test_module_with_enum(); + let enum_index = EnumDefinitionIndex(0); + skeleton.function_defs[0].code.as_mut().unwrap().code = vec![LdU64(0), Pop, Ret]; + skeleton.function_defs[0].code.as_mut().unwrap().jump_tables = vec![VariantJumpTable { + head_enum: enum_index, + jump_table: JumpTableInner::Full(vec![100]), + }]; + + let jt_index = VariantJumpTableIndex( + skeleton.function_defs[0] + .code + .as_ref() + .map(|c| c.jump_tables.len() as u16) + .unwrap_or(0u16), + ); + let mut m = skeleton.clone(); + m.function_defs[0].code.as_mut().unwrap().code = vec![VariantSwitch(jt_index)]; + assert_eq!( + BoundsChecker::verify_module(&m, /* deprecate_global_storage_ops */ true) + .unwrap_err() + .major_status(), + StatusCode::INDEX_OUT_OF_BOUNDS + ); +} diff --git a/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/catch_unwind.rs b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/catch_unwind.rs new file mode 100644 index 0000000000000..c43d625800e0a --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/catch_unwind.rs @@ -0,0 +1,27 @@ +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 +use fail::FailScenario; +use move_binary_format::file_format::empty_module; +use move_core_types::{ + state::{self, VMState}, + vm_status::StatusCode, +}; +use move_vm_config::verifier::VerifierConfig; +use std::panic::{self, PanicInfo}; + +#[ignore] +#[test] +fn test_unwind() { + let scenario = FailScenario::setup(); + fail::cfg("verifier-failpoint-panic", "panic").unwrap(); + + panic::set_hook(Box::new(move |_: &PanicInfo<'_>| { + assert_eq!(state::get_state(), VMState::VERIFIER); + })); + + let m = empty_module(); + let res = move_bytecode_verifier::verify_module_with_config(&VerifierConfig::unbounded(), &m) + .unwrap_err(); + assert_eq!(res.major_status(), StatusCode::VERIFIER_INVARIANT_VIOLATION); + scenario.teardown(); +} diff --git a/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/code_unit_tests.rs b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/code_unit_tests.rs new file mode 100644 index 0000000000000..9993b6d0c4027 --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/code_unit_tests.rs @@ -0,0 +1,142 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +use crate::support::dummy_procedure_module; +use move_binary_format::file_format::Bytecode; +use move_bytecode_verifier::ability_cache::AbilityCache; +use move_bytecode_verifier::code_unit_verifier; +use move_bytecode_verifier_meter::dummy::DummyMeter; +use move_core_types::vm_status::StatusCode; +use move_vm_config::verifier::VerifierConfig; + +#[test] +fn invalid_fallthrough_br_true() { + let module = &dummy_procedure_module(vec![Bytecode::LdFalse, Bytecode::BrTrue(1)]); + let ability_cache = &mut AbilityCache::new(module); + let result = code_unit_verifier::verify_module( + &Default::default(), + module, + ability_cache, + &mut DummyMeter, + ); + assert_eq!( + result.unwrap_err().major_status(), + StatusCode::INVALID_FALL_THROUGH + ); +} + +#[test] +fn invalid_fallthrough_br_false() { + let module = &dummy_procedure_module(vec![Bytecode::LdTrue, Bytecode::BrFalse(1)]); + let ability_cache = &mut AbilityCache::new(module); + let result = code_unit_verifier::verify_module( + &Default::default(), + module, + ability_cache, + &mut DummyMeter, + ); + assert_eq!( + result.unwrap_err().major_status(), + StatusCode::INVALID_FALL_THROUGH + ); +} + +// all non-branch instructions should trigger invalid fallthrough; just check one of them +#[test] +fn invalid_fallthrough_non_branch() { + let module = &dummy_procedure_module(vec![Bytecode::LdTrue, Bytecode::Pop]); + let ability_cache = &mut AbilityCache::new(module); + let result = code_unit_verifier::verify_module( + &Default::default(), + module, + ability_cache, + &mut DummyMeter, + ); + assert_eq!( + result.unwrap_err().major_status(), + StatusCode::INVALID_FALL_THROUGH + ); +} + +#[test] +fn valid_fallthrough_branch() { + let module = &dummy_procedure_module(vec![Bytecode::Branch(0)]); + let ability_cache = &mut AbilityCache::new(module); + let result = code_unit_verifier::verify_module( + &Default::default(), + module, + ability_cache, + &mut DummyMeter, + ); + assert!(result.is_ok()); +} + +#[test] +fn valid_fallthrough_ret() { + let module = &dummy_procedure_module(vec![Bytecode::Ret]); + let ability_cache = &mut AbilityCache::new(module); + let result = code_unit_verifier::verify_module( + &Default::default(), + module, + ability_cache, + &mut DummyMeter, + ); + assert!(result.is_ok()); +} + +#[test] +fn valid_fallthrough_abort() { + let module = &dummy_procedure_module(vec![Bytecode::LdU64(7), Bytecode::Abort]); + let ability_cache = &mut AbilityCache::new(module); + let result = code_unit_verifier::verify_module( + &Default::default(), + module, + ability_cache, + &mut DummyMeter, + ); + assert!(result.is_ok()); +} + +#[test] +fn test_max_number_of_bytecode() { + let mut nops = vec![]; + for _ in 0..u16::MAX - 1 { + nops.push(Bytecode::Nop); + } + nops.push(Bytecode::Ret); + let module = &dummy_procedure_module(nops); + let ability_cache = &mut AbilityCache::new(module); + + let result = code_unit_verifier::verify_module( + &VerifierConfig::default(), + module, + ability_cache, + &mut DummyMeter, + ); + assert!(result.is_ok()); +} + +#[test] +fn test_max_basic_blocks() { + let mut code = (0..17) + .map(|idx| Bytecode::Branch(idx + 1)) + .collect::>(); + code.push(Bytecode::Ret); + let module = &dummy_procedure_module(code); + let ability_cache = &mut AbilityCache::new(module); + + let result = code_unit_verifier::verify_module( + &VerifierConfig { + max_basic_blocks: Some(16), + ..Default::default() + }, + module, + ability_cache, + &mut DummyMeter, + ); + assert_eq!( + result.unwrap_err().major_status(), + StatusCode::TOO_MANY_BASIC_BLOCKS + ); +} diff --git a/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/constants_tests.rs b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/constants_tests.rs new file mode 100644 index 0000000000000..d521f36ba7388 --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/constants_tests.rs @@ -0,0 +1,265 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 +use move_binary_format::file_format::{Constant, SignatureToken, empty_module}; +use move_bytecode_verifier::constants; +use move_core_types::vm_status::StatusCode; + +#[test] +fn valid_primitives() { + let mut module = empty_module(); + module.constant_pool = vec![ + Constant { + type_: SignatureToken::Bool, + data: vec![0], + }, + Constant { + type_: SignatureToken::U8, + data: vec![0], + }, + Constant { + type_: SignatureToken::U16, + data: vec![0, 0], + }, + Constant { + type_: SignatureToken::U32, + data: vec![0, 0, 0, 0], + }, + Constant { + type_: SignatureToken::U64, + data: vec![0, 0, 0, 0, 0, 0, 0, 0], + }, + Constant { + type_: SignatureToken::U128, + data: vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + }, + Constant { + type_: SignatureToken::U256, + data: vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, + ], + }, + Constant { + type_: SignatureToken::Address, + data: vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, + ], + }, + ]; + assert!(constants::verify_module(&module).is_ok()); +} + +#[test] +fn invalid_primitives() { + malformed(SignatureToken::U8, vec![0, 0]); + malformed(SignatureToken::U16, vec![0, 0, 0, 0]); + malformed(SignatureToken::U32, vec![0, 0, 0]); + malformed(SignatureToken::U64, vec![0]); + malformed(SignatureToken::U128, vec![0]); + malformed(SignatureToken::U256, vec![0, 0]); + let data = vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, + ]; + malformed(SignatureToken::Address, data); +} + +#[test] +fn valid_vectors() { + let double_vec = |item: Vec| -> Vec { + let mut items = vec![2]; + items.extend(item.clone()); + items.extend(item); + items + }; + let large_vec = |item: Vec| -> Vec { + let mut items = vec![0xFF, 0xFF, 3]; + (0..0xFFFF).for_each(|_| items.extend(item.clone())); + items + }; + let mut module = empty_module(); + module.constant_pool = vec![ + // empty + Constant { + type_: tvec(SignatureToken::Bool), + data: vec![0], + }, + Constant { + type_: tvec(tvec(SignatureToken::Bool)), + data: vec![0], + }, + Constant { + type_: tvec(tvec(tvec(tvec(SignatureToken::Bool)))), + data: vec![0], + }, + Constant { + type_: tvec(tvec(tvec(tvec(SignatureToken::Bool)))), + data: double_vec(vec![0]), + }, + // small + Constant { + type_: tvec(SignatureToken::Bool), + data: vec![9, 1, 1, 1, 1, 1, 1, 1, 1, 1], + }, + Constant { + type_: tvec(SignatureToken::U8), + data: vec![9, 1, 1, 1, 1, 1, 1, 1, 1, 1], + }, + // large + Constant { + type_: tvec(SignatureToken::Bool), + data: large_vec(vec![0]), + }, + Constant { + type_: tvec(SignatureToken::U8), + data: large_vec(vec![0]), + }, + Constant { + type_: tvec(SignatureToken::U16), + data: large_vec(vec![0, 0]), + }, + Constant { + type_: tvec(SignatureToken::U32), + data: large_vec(vec![0, 0, 0, 0]), + }, + Constant { + type_: tvec(SignatureToken::U64), + data: large_vec(vec![0, 0, 0, 0, 0, 0, 0, 0]), + }, + Constant { + type_: tvec(SignatureToken::U128), + data: large_vec(vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + }, + Constant { + type_: tvec(SignatureToken::U256), + data: large_vec(vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, + ]), + }, + Constant { + type_: tvec(SignatureToken::Address), + data: large_vec(vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, + ]), + }, + // double large + Constant { + type_: tvec(tvec(SignatureToken::Bool)), + data: double_vec(large_vec(vec![0])), + }, + Constant { + type_: tvec(tvec(SignatureToken::U8)), + data: double_vec(large_vec(vec![0])), + }, + Constant { + type_: tvec(tvec(SignatureToken::U16)), + data: double_vec(large_vec(vec![0, 0])), + }, + Constant { + type_: tvec(tvec(SignatureToken::U32)), + data: double_vec(large_vec(vec![0, 0, 0, 0])), + }, + Constant { + type_: tvec(tvec(SignatureToken::U64)), + data: double_vec(large_vec(vec![0, 0, 0, 0, 0, 0, 0, 0])), + }, + Constant { + type_: tvec(tvec(SignatureToken::U128)), + data: double_vec(large_vec(vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ])), + }, + Constant { + type_: tvec(tvec(SignatureToken::U256)), + data: double_vec(large_vec(vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, + ])), + }, + Constant { + type_: tvec(tvec(SignatureToken::Address)), + data: double_vec(large_vec(vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, + ])), + }, + ]; + assert!(constants::verify_module(&module).is_ok()); +} + +#[test] +fn invalid_vectors() { + let double_vec = |item: Vec| -> Vec { + let mut items = vec![2]; + items.extend(item.clone()); + items.extend(item); + items + }; + let too_large_vec = |item: Vec| -> Vec { + let mut items = vec![0xFF, 0xFF, 3]; + (0..(0xFFFF + 1)).for_each(|_| items.extend(item.clone())); + items + }; + // wrong inner + malformed(tvec(SignatureToken::U16), vec![1, 0]); + malformed(tvec(SignatureToken::U32), vec![1, 0]); + malformed(tvec(SignatureToken::U64), vec![1, 0]); + malformed( + tvec(SignatureToken::Address), + vec![ + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ], + ); + // wrong lens + malformed(tvec(SignatureToken::U8), vec![0, 0]); + malformed(tvec(SignatureToken::U8), vec![0, 1]); + malformed(tvec(SignatureToken::U8), vec![2, 1, 1, 1]); + malformed(tvec(tvec(SignatureToken::U8)), double_vec(vec![0, 0])); + // too large + malformed(tvec(SignatureToken::U8), too_large_vec(vec![0])); +} + +#[test] +fn invalid_types() { + invalid_type(SignatureToken::TypeParameter(0), vec![0]); + invalid_type(SignatureToken::TypeParameter(0xFA), vec![0]); + invalid_type(tvec(SignatureToken::TypeParameter(0)), vec![0]); + invalid_type(tvec(SignatureToken::TypeParameter(0xAF)), vec![0]); + + invalid_type(SignatureToken::Signer, vec![0]); + invalid_type(tvec(SignatureToken::Signer), vec![0]); + + // TODO cannot check structs are banned currently. This can be handled by IR and source lang + // tests + // invalid_type(SignatureToken::Datatype(DatatypeHandleIndex(0)), vec![0]); +} + +fn tvec(s: SignatureToken) -> SignatureToken { + SignatureToken::Vector(Box::new(s)) +} + +fn malformed(type_: SignatureToken, data: Vec) { + error(type_, data, StatusCode::MALFORMED_CONSTANT_DATA) +} + +fn invalid_type(type_: SignatureToken, data: Vec) { + error(type_, data, StatusCode::INVALID_CONSTANT_TYPE) +} + +fn error(type_: SignatureToken, data: Vec, code: StatusCode) { + let mut module = empty_module(); + module.constant_pool = vec![Constant { type_, data }]; + assert!( + constants::verify_module(&module) + .unwrap_err() + .major_status() + == code + ) +} diff --git a/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/control_flow_graph_tests.rs b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/control_flow_graph_tests.rs new file mode 100644 index 0000000000000..0933d781c2df8 --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/control_flow_graph_tests.rs @@ -0,0 +1,242 @@ +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +use itertools::Itertools; + +use move_bytecode_verifier_meter::absint::ControlFlowGraph; + +use move_binary_format::file_format::{ + Bytecode, EnumDefinitionIndex, JumpTableInner, VariantJumpTable, VariantJumpTableIndex, +}; + +#[test] +fn traversal_no_loops() { + let cfg = { + use Bytecode::*; + ControlFlowGraph::new( + &[ + /* L0 */ LdTrue, + /* */ BrTrue(3), + /* L2 */ Branch(3), + /* L3 */ Ret, + ], + &[], + ) + }; + + cfg.display(); + assert_eq!(cfg.num_blocks(), 3); + assert_eq!(traversal(&cfg), vec![0, 2, 3]); +} + +#[test] +fn traversal_no_loops_with_switch() { + let cfg = { + use Bytecode::*; + ControlFlowGraph::new( + &[ + /* L0 */ VariantSwitch(VariantJumpTableIndex::new(0)), + /* */ Nop, + /* */ Nop, + /* */ Nop, + /* */ Nop, + /* */ Nop, + /* */ BrTrue(8), + /* L2 */ Branch(8), + /* L3 */ Ret, + ], + &[VariantJumpTable { + // Doesn't matter + head_enum: EnumDefinitionIndex::new(0), + jump_table: JumpTableInner::Full(vec![1, 8, 2, 4]), + }], + ) + }; + + cfg.display(); + assert_eq!(cfg.num_blocks(), 6); + assert_eq!(dbg!(traversal(&cfg)), vec![0, 1, 2, 4, 7, 8]); +} + +#[test] +fn traversal_loops() { + let cfg = { + use Bytecode::*; + ControlFlowGraph::new( + &[ + /* L0: Outer head */ LdTrue, + /* Outer break */ BrTrue(6), + /* L2: Inner head */ LdTrue, + /* Inner break */ BrTrue(5), + /* L4: Inner continue */ Branch(2), + /* Outer continue */ Branch(0), + /* L6: */ Ret, + ], + &[], + ) + }; + + cfg.display(); + assert_eq!(cfg.num_blocks(), 5); + assert_eq!(traversal(&cfg), vec![0, 2, 4, 5, 6]); +} + +#[test] +fn traversal_loops_with_switch() { + let cfg = { + use Bytecode::*; + ControlFlowGraph::new( + &[ + /* L0: Outer head */ LdTrue, + /* Outer break */ BrTrue(4), + /* L2: Inner head */ VariantSwitch(VariantJumpTableIndex::new(0)), + /* Outer continue */ Branch(0), + /* L6: */ Ret, + ], + &[VariantJumpTable { + // Doesn't matter + head_enum: EnumDefinitionIndex::new(0), + jump_table: JumpTableInner::Full(vec![ + /* Inner break */ 3, /* Inner continue */ 2, + ]), + }], + ) + }; + + cfg.display(); + assert_eq!(cfg.num_blocks(), 4); + assert_eq!(traversal(&cfg), vec![0, 2, 3, 4]); +} + +#[test] +fn traversal_non_loop_back_branch() { + let cfg = { + use Bytecode::*; + ControlFlowGraph::new( + &[ + /* L0 */ Branch(2), + /* L1 */ Ret, + /* L2 */ Branch(1), + ], + &[], + ) + }; + + cfg.display(); + assert_eq!(cfg.num_blocks(), 3); + assert_eq!(traversal(&cfg), vec![0, 2, 1]); +} + +#[test] +fn traversal_non_loop_back_branch_variant_switch() { + let cfg = { + use Bytecode::*; + ControlFlowGraph::new( + &[ + /* L0 */ VariantSwitch(VariantJumpTableIndex::new(0)), + /* L1 */ Ret, + /* L2 */ Branch(1), + ], + &[VariantJumpTable { + // Doesn't matter + head_enum: EnumDefinitionIndex::new(0), + jump_table: JumpTableInner::Full(vec![2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]), + }], + ) + }; + + cfg.display(); + assert_eq!(cfg.num_blocks(), 3); + assert_eq!(traversal(&cfg), vec![0, 2, 1]); +} + +#[test] +fn out_of_order_blocks_variant_switch() { + const PERMUTATION_BOUND: usize = 2000; + + let blocks = (0..=127) + .map(|i| { + ( + i, + vec![ + Bytecode::Pop, // Pop the value from the variant switch + Bytecode::LdU16(i), // Ld the number so we can track what block this is canonically + Bytecode::Pop, // Then pop it + Bytecode::Ret, // Then ret + ], + ) + }) + .collect::>(); + + let block_len = blocks.last().unwrap().1.len() as u16; + + let (canonical_blocks, canonical_traversal) = { + let jump_table = + JumpTableInner::Full(blocks.iter().map(|(i, _)| 1 + *i * block_len).collect()); + let mut start_block = vec![Bytecode::VariantSwitch(VariantJumpTableIndex::new(0))]; + start_block.extend(blocks.clone().into_iter().flat_map(|(_, block)| block)); + + let cfg = ControlFlowGraph::new( + &start_block, + &[VariantJumpTable { + // Doesn't matter + head_enum: EnumDefinitionIndex::new(0), + jump_table, + }], + ); + + cfg.display(); + (cfg.num_blocks(), traversal(&cfg)) + }; + + assert_eq!(canonical_blocks, 129); + assert_eq!(canonical_traversal.len(), 129); + + for permutation in blocks.into_iter().permutations(128).take(PERMUTATION_BOUND) { + // orig index => new_index + // identity permutation == perm[i] == i; + let mut perm = vec![]; + let mut blocks = vec![Bytecode::VariantSwitch(VariantJumpTableIndex::new(0))]; + for (index, mut block) in permutation.into_iter() { + perm.push(index); + blocks.append(&mut block); + } + + let jump_table = JumpTableInner::Full(perm.iter().map(|i| 1 + *i * block_len).collect()); + + let cfg = ControlFlowGraph::new( + &blocks, + &[VariantJumpTable { + // Doesn't matter + head_enum: EnumDefinitionIndex::new(0), + jump_table, + }], + ); + assert_eq!( + cfg.num_blocks(), + canonical_blocks, + "num blocks differ: Permutation: {:?}", + perm + ); + assert_eq!( + traversal(&cfg), + canonical_traversal, + "traversal differs: Permutation: {:?}", + perm + ); + } +} + +/// Return a vector containing the `BlockId`s from `cfg` in the order suggested by successively +/// calling `ControlFlowGraph::next_block` starting from the entry block. +fn traversal(cfg: &dyn ControlFlowGraph) -> Vec { + let mut order = Vec::with_capacity(cfg.num_blocks() as usize); + let mut next = Some(cfg.entry_block_id()); + + while let Some(block) = next { + order.push(block); + next = cfg.next_block(block); + } + + order +} diff --git a/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/control_flow_tests.rs b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/control_flow_tests.rs new file mode 100644 index 0000000000000..97bc06161e545 --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/control_flow_tests.rs @@ -0,0 +1,240 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +use crate::support::dummy_procedure_module; +use move_binary_format::{ + errors::PartialVMResult, + file_format::{Bytecode, CompiledModule, FunctionDefinitionIndex, TableIndex}, +}; +use move_bytecode_verifier::control_flow; +use move_bytecode_verifier_meter::dummy::DummyMeter; +use move_core_types::vm_status::StatusCode; +use move_vm_config::verifier::VerifierConfig; + +fn verify_module(verifier_config: &VerifierConfig, module: &CompiledModule) -> PartialVMResult<()> { + for (idx, function_definition) in module + .function_defs() + .iter() + .enumerate() + .filter(|(_, def)| !def.is_native()) + { + let current_function = FunctionDefinitionIndex(idx as TableIndex); + let code = function_definition + .code + .as_ref() + .expect("unexpected native function"); + + control_flow::verify_function( + verifier_config, + module, + current_function, + function_definition, + code, + &mut DummyMeter, + )?; + } + Ok(()) +} + +//************************************************************************************************** +// Simple cases - Copied from code unit verifier +//************************************************************************************************** + +#[test] +fn empty_bytecode() { + let module = dummy_procedure_module(vec![]); + let result = verify_module(&Default::default(), &module); + assert_eq!( + result.unwrap_err().major_status(), + StatusCode::EMPTY_CODE_UNIT, + ); +} + +#[test] +fn empty_bytecode_v5() { + let mut module = dummy_procedure_module(vec![]); + module.version = 5; + + let result = verify_module(&Default::default(), &module); + assert_eq!( + result.unwrap_err().major_status(), + StatusCode::EMPTY_CODE_UNIT, + ); +} + +#[test] +fn invalid_fallthrough_br_true() { + let module = dummy_procedure_module(vec![Bytecode::LdFalse, Bytecode::BrTrue(1)]); + let result = verify_module(&Default::default(), &module); + assert_eq!( + result.unwrap_err().major_status(), + StatusCode::INVALID_FALL_THROUGH + ); +} + +#[test] +fn invalid_fallthrough_br_false() { + let module = dummy_procedure_module(vec![Bytecode::LdTrue, Bytecode::BrFalse(1)]); + let result = verify_module(&Default::default(), &module); + assert_eq!( + result.unwrap_err().major_status(), + StatusCode::INVALID_FALL_THROUGH + ); +} + +// all non-branch instructions should trigger invalid fallthrough; just check one of them +#[test] +fn invalid_fallthrough_non_branch() { + let module = dummy_procedure_module(vec![Bytecode::LdTrue, Bytecode::Pop]); + let result = verify_module(&Default::default(), &module); + assert_eq!( + result.unwrap_err().major_status(), + StatusCode::INVALID_FALL_THROUGH + ); +} + +#[test] +fn valid_fallthrough_branch() { + let module = dummy_procedure_module(vec![Bytecode::Branch(0)]); + let result = verify_module(&Default::default(), &module); + assert!(result.is_ok()); +} + +#[test] +fn valid_fallthrough_ret() { + let module = dummy_procedure_module(vec![Bytecode::Ret]); + let result = verify_module(&Default::default(), &module); + assert!(result.is_ok()); +} + +#[test] +fn valid_fallthrough_abort() { + let module = dummy_procedure_module(vec![Bytecode::LdU64(7), Bytecode::Abort]); + let result = verify_module(&Default::default(), &module); + assert!(result.is_ok()); +} + +#[test] +fn nested_loops_max_depth() { + let module = dummy_procedure_module(vec![ + Bytecode::LdFalse, + Bytecode::LdFalse, + Bytecode::BrFalse(1), + Bytecode::BrFalse(0), + Bytecode::Ret, + ]); + let result = verify_module( + &VerifierConfig { + max_loop_depth: Some(2), + ..VerifierConfig::default() + }, + &module, + ); + assert!(result.is_ok()); +} + +#[test] +fn nested_loops_exceed_max_depth() { + let module = dummy_procedure_module(vec![ + Bytecode::LdFalse, + Bytecode::LdFalse, + Bytecode::LdFalse, + Bytecode::BrFalse(2), + Bytecode::BrFalse(1), + Bytecode::BrFalse(0), + Bytecode::Ret, + ]); + let result = verify_module( + &VerifierConfig { + max_loop_depth: Some(2), + ..VerifierConfig::default() + }, + &module, + ); + assert_eq!( + result.unwrap_err().major_status(), + StatusCode::LOOP_MAX_DEPTH_REACHED + ); +} + +#[test] +fn non_loop_backward_jump() { + let module = dummy_procedure_module(vec![ + Bytecode::Branch(2), + Bytecode::Ret, + Bytecode::Branch(1), + ]); + let result = verify_module(&Default::default(), &module); + assert!(result.is_ok()); +} + +#[test] +fn non_loop_backward_jump_v5() { + let mut module = dummy_procedure_module(vec![ + Bytecode::Branch(2), + Bytecode::Ret, + Bytecode::Branch(1), + ]); + + module.version = 5; + let result = verify_module(&Default::default(), &module); + assert_eq!( + result.unwrap_err().major_status(), + StatusCode::INVALID_LOOP_SPLIT, + ); +} + +#[test] +fn irreducible_control_flow_graph() { + let module = dummy_procedure_module(vec![ + Bytecode::LdTrue, + Bytecode::BrTrue(3), + Bytecode::Nop, + Bytecode::LdFalse, + Bytecode::BrFalse(2), + Bytecode::Ret, + ]); + let result = verify_module(&Default::default(), &module); + assert_eq!( + result.unwrap_err().major_status(), + StatusCode::INVALID_LOOP_SPLIT, + ); +} + +#[test] +fn nested_loop_break() { + let module = dummy_procedure_module(vec![ + Bytecode::LdFalse, + Bytecode::LdFalse, + Bytecode::LdFalse, + Bytecode::Branch(7), + Bytecode::BrFalse(2), + Bytecode::BrFalse(1), + Bytecode::BrFalse(0), + Bytecode::Ret, + ]); + let result = verify_module(&Default::default(), &module); + assert!(result.is_ok()); +} + +#[test] +fn nested_loop_break_v5() { + let mut module = dummy_procedure_module(vec![ + Bytecode::LdFalse, + Bytecode::LdFalse, + Bytecode::LdFalse, + Bytecode::Branch(7), + Bytecode::BrFalse(2), + Bytecode::BrFalse(1), + Bytecode::BrFalse(0), + Bytecode::Ret, + ]); + + module.version = 5; + let result = verify_module(&Default::default(), &module); + assert_eq!( + result.unwrap_err().major_status(), + StatusCode::INVALID_LOOP_BREAK, + ); +} diff --git a/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/duplication_tests.rs b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/duplication_tests.rs new file mode 100644 index 0000000000000..72a93d053717c --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/duplication_tests.rs @@ -0,0 +1,25 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +use move_binary_format::file_format::*; +use move_bytecode_verifier::DuplicationChecker; + +#[test] +fn duplicated_friend_decls() { + let mut m = basic_test_module(); + let handle = ModuleHandle { + address: AddressIdentifierIndex::new(0), + name: IdentifierIndex::new(0), + }; + m.friend_decls.push(handle.clone()); + m.friend_decls.push(handle); + DuplicationChecker::verify_module(&m).unwrap_err(); +} + +#[test] +fn duplicated_variant_handles() { + let mut m = basic_test_module_with_enum(); + m.variant_handles.push(m.variant_handles[0].clone()); + DuplicationChecker::verify_module(&m).unwrap_err(); +} diff --git a/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/generic_ops_tests.rs b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/generic_ops_tests.rs new file mode 100644 index 0000000000000..1a8ba64ed8924 --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/generic_ops_tests.rs @@ -0,0 +1,478 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +use move_binary_format::file_format::*; +use move_bytecode_verifier::InstructionConsistency; +use move_core_types::{ + account_address::AccountAddress, identifier::Identifier, vm_status::StatusCode, +}; +use move_vm_config::verifier::VerifierConfig; + +// Make a Module with 2 structs and 2 resources with one field each, and 2 functions. +// One of the struct/resource and one of the function is generic, the other "normal". +// Also make a test function whose body will be filled by given test cases. +fn make_module() -> CompiledModule { + CompiledModule { + version: move_binary_format::file_format_common::VERSION_MAX, + publishable: true, + module_handles: vec![ + // only self module + ModuleHandle { + address: AddressIdentifierIndex(0), + name: IdentifierIndex(0), + }, + ], + self_module_handle_idx: ModuleHandleIndex(0), + identifiers: vec![ + Identifier::new("M").unwrap(), // Module name + Identifier::new("S").unwrap(), // Struct name + Identifier::new("GS").unwrap(), // Generic struct name + Identifier::new("R").unwrap(), // Resource name + Identifier::new("GR").unwrap(), // Generic resource name + Identifier::new("f").unwrap(), // Field name + Identifier::new("fn").unwrap(), // Function name + Identifier::new("g_fn").unwrap(), // Generic function name + Identifier::new("test_fn").unwrap(), // Test function name + ], + address_identifiers: vec![ + AccountAddress::ZERO, // Module address + ], + datatype_handles: vec![ + DatatypeHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(1), + abilities: AbilitySet::PRIMITIVES, + type_parameters: vec![], + }, + DatatypeHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(2), + abilities: AbilitySet::PRIMITIVES, + type_parameters: vec![DatatypeTyParameter { + constraints: AbilitySet::PRIMITIVES, + is_phantom: false, + }], + }, + DatatypeHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(3), + abilities: AbilitySet::EMPTY | Ability::Key, + type_parameters: vec![], + }, + DatatypeHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(4), + abilities: AbilitySet::EMPTY | Ability::Key, + type_parameters: vec![DatatypeTyParameter { + constraints: AbilitySet::PRIMITIVES, + is_phantom: false, + }], + }, + ], + struct_defs: vec![ + // struct S { f: u64 } + StructDefinition { + struct_handle: DatatypeHandleIndex(0), + field_information: StructFieldInformation::Declared(vec![FieldDefinition { + name: IdentifierIndex(5), + signature: TypeSignature(SignatureToken::U64), + }]), + }, + // struct GS { f: T } + StructDefinition { + struct_handle: DatatypeHandleIndex(1), + field_information: StructFieldInformation::Declared(vec![FieldDefinition { + name: IdentifierIndex(5), + signature: TypeSignature(SignatureToken::TypeParameter(0)), + }]), + }, + // struct R has key { f: u64 } + StructDefinition { + struct_handle: DatatypeHandleIndex(2), + field_information: StructFieldInformation::Declared(vec![FieldDefinition { + name: IdentifierIndex(5), + signature: TypeSignature(SignatureToken::U64), + }]), + }, + // struct GR has key { f: T } + StructDefinition { + struct_handle: DatatypeHandleIndex(3), + field_information: StructFieldInformation::Declared(vec![FieldDefinition { + name: IdentifierIndex(5), + signature: TypeSignature(SignatureToken::TypeParameter(0)), + }]), + }, + ], + function_handles: vec![ + // fun fn() + FunctionHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(6), + parameters: SignatureIndex(0), + return_: SignatureIndex(0), + type_parameters: vec![], + }, + // fun g_fn() + FunctionHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(7), + parameters: SignatureIndex(0), + return_: SignatureIndex(0), + type_parameters: vec![AbilitySet::EMPTY | Ability::Key], + }, + // fun test_fn(Sender) + FunctionHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(8), + parameters: SignatureIndex(1), + return_: SignatureIndex(0), + type_parameters: vec![], + }, + ], + function_defs: vec![ + // public fun fn() { return; } + FunctionDefinition { + function: FunctionHandleIndex(0), + visibility: Visibility::Public, + is_entry: false, + acquires_global_resources: vec![], + code: Some(CodeUnit { + locals: SignatureIndex(0), + code: vec![Bytecode::Ret], + jump_tables: vec![], + }), + }, + // fun g_fn() { return; } + FunctionDefinition { + function: FunctionHandleIndex(1), + visibility: Visibility::Private, + is_entry: false, + acquires_global_resources: vec![], + code: Some(CodeUnit { + locals: SignatureIndex(0), + code: vec![Bytecode::Ret], + jump_tables: vec![], + }), + }, + // fun test_fn() { ... } - tests will fill up the code + FunctionDefinition { + function: FunctionHandleIndex(2), + visibility: Visibility::Private, + is_entry: false, + acquires_global_resources: vec![], + code: Some(CodeUnit { + locals: SignatureIndex(0), + code: vec![], + jump_tables: vec![], + }), + }, + ], + signatures: vec![ + Signature(vec![]), // void + Signature(vec![SignatureToken::Signer]), // Signer + ], + constant_pool: vec![ + // an address + Constant { + type_: SignatureToken::Address, + data: AccountAddress::random().to_vec(), + }, + ], + metadata: vec![], + field_handles: vec![], + friend_decls: vec![], + struct_def_instantiations: vec![], + function_instantiations: vec![], + field_instantiations: vec![], + enum_defs: vec![], + enum_def_instantiations: vec![], + variant_handles: vec![], + variant_instantiation_handles: vec![], + } +} + +#[test] +fn generic_call_to_non_generic_func() { + let mut module = make_module(); + // bogus `CallGeneric fn()` + module.function_defs[2].code = Some(CodeUnit { + locals: SignatureIndex(0), + code: vec![ + Bytecode::CallGeneric(FunctionInstantiationIndex(0)), + Bytecode::Ret, + ], + jump_tables: vec![], + }); + module.function_instantiations.push(FunctionInstantiation { + handle: FunctionHandleIndex(0), + type_parameters: SignatureIndex(2), + }); + module.signatures.push(Signature(vec![SignatureToken::U64])); + let err = InstructionConsistency::verify_module(&VerifierConfig::default(), &module) + .expect_err("CallGeneric to non generic function must fail"); + assert_eq!( + err.major_status(), + StatusCode::GENERIC_MEMBER_OPCODE_MISMATCH + ); +} + +#[test] +fn non_generic_call_to_generic_func() { + let mut module = make_module(); + // bogus `Call g_fn()` + module.function_defs[2].code = Some(CodeUnit { + locals: SignatureIndex(0), + code: vec![Bytecode::Call(FunctionHandleIndex(1)), Bytecode::Ret], + jump_tables: vec![], + }); + let err = InstructionConsistency::verify_module(&VerifierConfig::default(), &module) + .expect_err("Call to generic function must fail"); + assert_eq!( + err.major_status(), + StatusCode::GENERIC_MEMBER_OPCODE_MISMATCH + ); +} + +#[test] +fn generic_pack_on_non_generic_struct() { + let mut module = make_module(); + // bogus `PackGeneric S` + module.function_defs[2].code = Some(CodeUnit { + locals: SignatureIndex(0), + code: vec![ + Bytecode::LdU64(10), + Bytecode::PackGeneric(StructDefInstantiationIndex(0)), + Bytecode::Pop, + Bytecode::Ret, + ], + jump_tables: vec![], + }); + module + .struct_def_instantiations + .push(StructDefInstantiation { + def: StructDefinitionIndex(0), + type_parameters: SignatureIndex(2), + }); + module.signatures.push(Signature(vec![SignatureToken::U64])); + let err = InstructionConsistency::verify_module(&VerifierConfig::default(), &module) + .expect_err("PackGeneric to non generic struct must fail"); + assert_eq!( + err.major_status(), + StatusCode::GENERIC_MEMBER_OPCODE_MISMATCH + ); +} + +#[test] +fn non_generic_pack_on_generic_struct() { + let mut module = make_module(); + // bogus `Pack GS` + module.function_defs[2].code = Some(CodeUnit { + locals: SignatureIndex(0), + code: vec![ + Bytecode::LdU64(10), + Bytecode::Pack(StructDefinitionIndex(1)), + Bytecode::Pop, + Bytecode::Ret, + ], + jump_tables: vec![], + }); + let err = InstructionConsistency::verify_module(&VerifierConfig::default(), &module) + .expect_err("Pack to generic struct must fail"); + assert_eq!( + err.major_status(), + StatusCode::GENERIC_MEMBER_OPCODE_MISMATCH + ); +} + +#[test] +fn generic_unpack_on_non_generic_struct() { + let mut module = make_module(); + // bogus `UnpackGeneric S` + module.function_defs[2].code = Some(CodeUnit { + locals: SignatureIndex(0), + code: vec![ + Bytecode::LdU64(10), + Bytecode::Pack(StructDefinitionIndex(0)), + Bytecode::UnpackGeneric(StructDefInstantiationIndex(0)), + Bytecode::Pop, + Bytecode::Ret, + ], + jump_tables: vec![], + }); + module + .struct_def_instantiations + .push(StructDefInstantiation { + def: StructDefinitionIndex(0), + type_parameters: SignatureIndex(2), + }); + module.signatures.push(Signature(vec![SignatureToken::U64])); + let err = InstructionConsistency::verify_module(&VerifierConfig::default(), &module) + .expect_err("UnpackGeneric to non generic struct must fail"); + assert_eq!( + err.major_status(), + StatusCode::GENERIC_MEMBER_OPCODE_MISMATCH + ); +} + +#[test] +fn non_generic_unpack_on_generic_struct() { + let mut module = make_module(); + // bogus `Unpack GS` + module.function_defs[2].code = Some(CodeUnit { + locals: SignatureIndex(0), + code: vec![ + Bytecode::LdU64(10), + Bytecode::PackGeneric(StructDefInstantiationIndex(0)), + Bytecode::Unpack(StructDefinitionIndex(1)), + Bytecode::Pop, + Bytecode::Ret, + ], + jump_tables: vec![], + }); + module + .struct_def_instantiations + .push(StructDefInstantiation { + def: StructDefinitionIndex(1), + type_parameters: SignatureIndex(2), + }); + module.signatures.push(Signature(vec![SignatureToken::U64])); + let err = InstructionConsistency::verify_module(&VerifierConfig::default(), &module) + .expect_err("Unpack to generic struct must fail"); + assert_eq!( + err.major_status(), + StatusCode::GENERIC_MEMBER_OPCODE_MISMATCH + ); +} + +#[test] +fn generic_mut_borrow_field_on_non_generic_struct() { + let mut module = make_module(); + // bogus `MutBorrowFieldGeneric S.t` + module.function_defs[2].code = Some(CodeUnit { + locals: SignatureIndex(0), + code: vec![ + Bytecode::LdU64(10), + Bytecode::Pack(StructDefinitionIndex(0)), + Bytecode::MutBorrowFieldGeneric(FieldInstantiationIndex(0)), + Bytecode::Pop, + Bytecode::Ret, + ], + jump_tables: vec![], + }); + module.field_instantiations.push(FieldInstantiation { + handle: FieldHandleIndex(0), + type_parameters: SignatureIndex(2), + }); + module.field_handles.push(FieldHandle { + owner: StructDefinitionIndex(0), + field: 0, + }); + module.signatures.push(Signature(vec![SignatureToken::U64])); + let err = InstructionConsistency::verify_module(&VerifierConfig::default(), &module) + .expect_err("MutBorrowFieldGeneric to non generic struct must fail"); + assert_eq!( + err.major_status(), + StatusCode::GENERIC_MEMBER_OPCODE_MISMATCH + ); +} + +#[test] +fn non_generic_mut_borrow_field_on_generic_struct() { + let mut module = make_module(); + // bogus `MutBorrowField GS.f` + module.function_defs[2].code = Some(CodeUnit { + locals: SignatureIndex(0), + code: vec![ + Bytecode::LdU64(10), + Bytecode::PackGeneric(StructDefInstantiationIndex(0)), + Bytecode::MutBorrowField(FieldHandleIndex(0)), + Bytecode::Pop, + Bytecode::Ret, + ], + jump_tables: vec![], + }); + module + .struct_def_instantiations + .push(StructDefInstantiation { + def: StructDefinitionIndex(1), + type_parameters: SignatureIndex(2), + }); + module.field_handles.push(FieldHandle { + owner: StructDefinitionIndex(1), + field: 0, + }); + module.signatures.push(Signature(vec![SignatureToken::U64])); + let err = InstructionConsistency::verify_module(&VerifierConfig::default(), &module) + .expect_err("MutBorrowField to generic struct must fail"); + assert_eq!( + err.major_status(), + StatusCode::GENERIC_MEMBER_OPCODE_MISMATCH + ); +} + +#[test] +fn generic_borrow_field_on_non_generic_struct() { + let mut module = make_module(); + // bogus `ImmBorrowFieldGeneric S.f` + module.function_defs[2].code = Some(CodeUnit { + locals: SignatureIndex(0), + code: vec![ + Bytecode::LdU64(10), + Bytecode::Pack(StructDefinitionIndex(0)), + Bytecode::ImmBorrowFieldGeneric(FieldInstantiationIndex(0)), + Bytecode::Pop, + Bytecode::Ret, + ], + jump_tables: vec![], + }); + module.field_instantiations.push(FieldInstantiation { + handle: FieldHandleIndex(0), + type_parameters: SignatureIndex(2), + }); + module.field_handles.push(FieldHandle { + owner: StructDefinitionIndex(0), + field: 0, + }); + module.signatures.push(Signature(vec![SignatureToken::U64])); + let err = InstructionConsistency::verify_module(&VerifierConfig::default(), &module) + .expect_err("ImmBorrowFieldGeneric to non generic struct must fail"); + assert_eq!( + err.major_status(), + StatusCode::GENERIC_MEMBER_OPCODE_MISMATCH + ); +} + +#[test] +fn non_generic_borrow_field_on_generic_struct() { + let mut module = make_module(); + // bogus `ImmBorrowField GS.f` + module.function_defs[2].code = Some(CodeUnit { + locals: SignatureIndex(0), + code: vec![ + Bytecode::LdU64(10), + Bytecode::PackGeneric(StructDefInstantiationIndex(0)), + Bytecode::ImmBorrowField(FieldHandleIndex(0)), + Bytecode::Pop, + Bytecode::Ret, + ], + jump_tables: vec![], + }); + module + .struct_def_instantiations + .push(StructDefInstantiation { + def: StructDefinitionIndex(1), + type_parameters: SignatureIndex(2), + }); + module.field_handles.push(FieldHandle { + owner: StructDefinitionIndex(1), + field: 0, + }); + module.signatures.push(Signature(vec![SignatureToken::U64])); + let err = InstructionConsistency::verify_module(&VerifierConfig::default(), &module) + .expect_err("ImmBorrowField to generic struct must fail"); + assert_eq!( + err.major_status(), + StatusCode::GENERIC_MEMBER_OPCODE_MISMATCH + ); +} diff --git a/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/large_type_test.rs b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/large_type_test.rs new file mode 100644 index 0000000000000..56206543f31fd --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/large_type_test.rs @@ -0,0 +1,165 @@ +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +use crate::unit_tests::production_config; +use move_binary_format::file_format::{ + Bytecode, CodeUnit, FunctionDefinition, FunctionHandle, FunctionHandleIndex, IdentifierIndex, + ModuleHandleIndex, Signature, SignatureIndex, SignatureToken, Visibility::Public, empty_module, +}; +use move_bytecode_verifier_meter::bound::BoundMeter; +use move_core_types::{identifier::Identifier, vm_status::StatusCode}; + +const NUM_LOCALS: u8 = 64; +const NUM_CALLS: u16 = 77; +const NUM_FUNCTIONS: u16 = 177; + +fn get_nested_vec_type(len: usize) -> SignatureToken { + let mut ret = SignatureToken::Bool; + for _ in 0..len { + ret = SignatureToken::Vector(Box::new(ret)); + } + ret +} + +#[test] +fn test_large_types() { + // See also: github.com/aptos-labs/aptos-core/security/advisories/GHSA-37qw-jfpw-8899 + let mut m = empty_module(); + + m.signatures.push(Signature( + std::iter::repeat_n( + SignatureToken::Reference(Box::new(get_nested_vec_type(64))), + NUM_LOCALS as usize, + ) + .collect(), + )); + + m.function_handles.push(FunctionHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(0), + parameters: SignatureIndex(0), + return_: SignatureIndex(0), + type_parameters: vec![], + }); + m.function_defs.push(FunctionDefinition { + function: FunctionHandleIndex(0), + visibility: Public, + is_entry: false, + acquires_global_resources: vec![], + code: Some(CodeUnit { + locals: SignatureIndex(0), + code: vec![Bytecode::Call(FunctionHandleIndex(0)), Bytecode::Ret], + jump_tables: vec![], + }), + }); + + // returns_vecs + m.identifiers.push(Identifier::new("returns_vecs").unwrap()); + m.function_handles.push(FunctionHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(1), + parameters: SignatureIndex(0), + return_: SignatureIndex(1), + type_parameters: vec![], + }); + m.function_defs.push(FunctionDefinition { + function: FunctionHandleIndex(1), + visibility: Public, + is_entry: false, + acquires_global_resources: vec![], + code: Some(CodeUnit { + locals: SignatureIndex(0), + code: vec![Bytecode::Call(FunctionHandleIndex(1)), Bytecode::Ret], + jump_tables: vec![], + }), + }); + + // takes_and_returns_vecs + m.identifiers + .push(Identifier::new("takes_and_returns_vecs").unwrap()); + m.function_handles.push(FunctionHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(2), + parameters: SignatureIndex(1), + return_: SignatureIndex(1), + type_parameters: vec![], + }); + m.function_defs.push(FunctionDefinition { + function: FunctionHandleIndex(2), + visibility: Public, + is_entry: false, + acquires_global_resources: vec![], + code: Some(CodeUnit { + locals: SignatureIndex(0), + code: vec![Bytecode::Call(FunctionHandleIndex(1)), Bytecode::Ret], + jump_tables: vec![], + }), + }); + + // takes_vecs + m.identifiers.push(Identifier::new("takes_vecs").unwrap()); + m.function_handles.push(FunctionHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(3), + parameters: SignatureIndex(1), + return_: SignatureIndex(0), + type_parameters: vec![], + }); + m.function_defs.push(FunctionDefinition { + function: FunctionHandleIndex(3), + visibility: Public, + is_entry: false, + acquires_global_resources: vec![], + code: Some(CodeUnit { + locals: SignatureIndex(0), + code: vec![Bytecode::Ret], + jump_tables: vec![], + }), + }); + + // other fcts + for i in 0..NUM_FUNCTIONS { + m.identifiers + .push(Identifier::new(format!("f{}", i)).unwrap()); + m.function_handles.push(FunctionHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(i + 4), + parameters: SignatureIndex(0), + return_: SignatureIndex(0), + type_parameters: vec![], + }); + m.function_defs.push(FunctionDefinition { + function: FunctionHandleIndex(i + 4), + visibility: Public, + is_entry: false, + acquires_global_resources: vec![], + code: Some(CodeUnit { + locals: SignatureIndex(0), + code: vec![], + jump_tables: vec![], + }), + }); + + let code = &mut m.function_defs[i as usize + 4].code.as_mut().unwrap().code; + code.clear(); + code.push(Bytecode::Call(FunctionHandleIndex(1))); + for _ in 0..NUM_CALLS { + code.push(Bytecode::Call(FunctionHandleIndex(2))); + } + code.push(Bytecode::Call(FunctionHandleIndex(3))); + code.push(Bytecode::Ret); + } + + let (verifier_config, meter_config) = production_config(); + let mut meter = BoundMeter::new(meter_config); + let result = move_bytecode_verifier::verify_module_with_config_for_test( + "test_large_types", + &verifier_config, + &m, + &mut meter, + ); + assert_eq!( + result.unwrap_err().major_status(), + StatusCode::CONSTRAINT_NOT_SATISFIED, + ); +} diff --git a/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/limit_tests.rs b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/limit_tests.rs new file mode 100644 index 0000000000000..3b1cb2d5fdece --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/limit_tests.rs @@ -0,0 +1,828 @@ +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +use crate::unit_tests::production_config; +use move_binary_format::file_format::*; +use move_bytecode_verifier::{limits::LimitsVerifier, verify_module_with_config_for_test}; +use move_bytecode_verifier_meter::dummy::DummyMeter; +use move_core_types::{ + account_address::AccountAddress, identifier::Identifier, vm_status::StatusCode, +}; +use move_vm_config::verifier::{DEFAULT_MAX_IDENTIFIER_LENGTH, VerifierConfig}; + +#[test] +fn test_function_handle_type_instantiation() { + let mut m = basic_test_module(); + m.function_handles.push(FunctionHandle { + module: ModuleHandleIndex::new(0), + name: IdentifierIndex::new(0), + parameters: SignatureIndex(0), + return_: SignatureIndex(0), + type_parameters: std::iter::repeat_n(AbilitySet::ALL, 10).collect(), + }); + + assert_eq!( + LimitsVerifier::verify_module( + &VerifierConfig { + max_generic_instantiation_length: Some(9), + ..Default::default() + }, + &m + ) + .unwrap_err() + .major_status(), + StatusCode::TOO_MANY_TYPE_PARAMETERS + ); +} + +#[test] +fn test_struct_handle_type_instantiation() { + let mut m = basic_test_module(); + m.datatype_handles.push(DatatypeHandle { + module: ModuleHandleIndex::new(0), + name: IdentifierIndex::new(0), + abilities: AbilitySet::ALL, + type_parameters: std::iter::repeat_n( + DatatypeTyParameter { + constraints: AbilitySet::ALL, + is_phantom: false, + }, + 10, + ) + .collect(), + }); + + assert_eq!( + LimitsVerifier::verify_module( + &VerifierConfig { + max_generic_instantiation_length: Some(9), + ..Default::default() + }, + &m + ) + .unwrap_err() + .major_status(), + StatusCode::TOO_MANY_TYPE_PARAMETERS + ); +} + +#[test] +fn test_function_handle_parameters() { + let mut m = basic_test_module(); + m.signatures.push(Signature( + std::iter::repeat_n(SignatureToken::Bool, 10).collect(), + )); + m.function_handles.push(FunctionHandle { + module: ModuleHandleIndex::new(0), + name: IdentifierIndex::new(0), + parameters: SignatureIndex(1), + return_: SignatureIndex(0), + type_parameters: vec![], + }); + + assert_eq!( + LimitsVerifier::verify_module( + &VerifierConfig { + max_function_parameters: Some(9), + ..Default::default() + }, + &m + ) + .unwrap_err() + .major_status(), + StatusCode::TOO_MANY_PARAMETERS + ); +} + +#[test] +fn big_vec_unpacks() { + const N_TYPE_PARAMS: usize = 16; + let mut st = SignatureToken::Vector(Box::new(SignatureToken::U8)); + let type_params = vec![st; N_TYPE_PARAMS]; + st = SignatureToken::DatatypeInstantiation(Box::new((DatatypeHandleIndex(0), type_params))); + const N_VEC_PUSH: u16 = 1000; + let mut code = vec![]; + // 1. CopyLoc: ... -> ... st + // 2. VecPack: ... st -> ... Vec + // 3. VecUnpack: ... Vec -> ... st, st, st, ... st + for _ in 0..N_VEC_PUSH { + code.push(Bytecode::CopyLoc(0)); + code.push(Bytecode::VecPack(SignatureIndex(1), 1)); + code.push(Bytecode::VecUnpack(SignatureIndex(1), 1 << 15)); + } + // 1. VecPack: ... st, st, st, ... st -> ... Vec + // 2. Pop: ... Vec -> ... + for _ in 0..N_VEC_PUSH { + code.push(Bytecode::VecPack(SignatureIndex(1), 1 << 15)); + code.push(Bytecode::Pop); + } + code.push(Bytecode::Ret); + let type_param_constraints = DatatypeTyParameter { + constraints: AbilitySet::EMPTY, + is_phantom: false, + }; + let module = CompiledModule { + version: 5, + publishable: true, + self_module_handle_idx: ModuleHandleIndex(0), + module_handles: vec![ModuleHandle { + address: AddressIdentifierIndex(0), + name: IdentifierIndex(0), + }], + datatype_handles: vec![DatatypeHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(1), + abilities: AbilitySet::ALL, + type_parameters: vec![type_param_constraints; N_TYPE_PARAMS], + }], + function_handles: vec![FunctionHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(0), + parameters: SignatureIndex(1), + return_: SignatureIndex(0), + type_parameters: vec![], + }], + field_handles: vec![], + friend_decls: vec![], + struct_def_instantiations: vec![], + function_instantiations: vec![], + field_instantiations: vec![], + signatures: vec![Signature(vec![]), Signature(vec![st])], + identifiers: vec![ + Identifier::new("f").unwrap(), + Identifier::new("generic_struct").unwrap(), + ], + address_identifiers: vec![AccountAddress::ONE], + constant_pool: vec![], + metadata: vec![], + struct_defs: vec![StructDefinition { + struct_handle: DatatypeHandleIndex(0), + field_information: StructFieldInformation::Native, + }], + function_defs: vec![FunctionDefinition { + function: FunctionHandleIndex(0), + visibility: Visibility::Public, + is_entry: true, + acquires_global_resources: vec![], + code: Some(CodeUnit { + locals: SignatureIndex(0), + code, + jump_tables: vec![], + }), + }], + enum_defs: vec![], + enum_def_instantiations: vec![], + variant_handles: vec![], + variant_instantiation_handles: vec![], + }; + + // save module and verify that it can ser/de + let mut mvbytes = vec![]; + module.serialize(&mut mvbytes).unwrap(); + let module = CompiledModule::deserialize_with_defaults(&mvbytes).unwrap(); + + let res = verify_module_with_config_for_test( + "big_vec_unpacks", + &VerifierConfig { + max_loop_depth: Some(5), + max_generic_instantiation_length: Some(32), + max_function_parameters: Some(128), + max_basic_blocks: Some(1024), + max_push_size: Some(10000), + ..Default::default() + }, + &module, + &mut DummyMeter, + ); + assert_eq!( + res.unwrap_err().major_status(), + StatusCode::VALUE_STACK_PUSH_OVERFLOW + ); +} + +const MAX_STRUCTS: usize = 200; +const MAX_FIELDS: usize = 30; +const MAX_FUNCTIONS: usize = 1000; + +#[test] +fn max_struct_test() { + let config = VerifierConfig { + max_data_definitions: Some(MAX_STRUCTS), + max_fields_in_struct: Some(MAX_FIELDS), + max_function_definitions: Some(MAX_FUNCTIONS), + ..Default::default() + }; + let mut module = leaf_module("M"); + multi_struct(&mut module, 0); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!(res, Ok(())); + multi_struct(&mut module, 1); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!(res, Ok(())); + let mut module = leaf_module("M"); + multi_struct(&mut module, MAX_STRUCTS / 2); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!(res, Ok(())); + let mut module = leaf_module("M"); + multi_struct(&mut module, MAX_STRUCTS); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!(res, Ok(())); + let mut module = leaf_module("M"); + multi_struct(&mut module, MAX_STRUCTS * 2); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!( + res.unwrap_err().major_status(), + StatusCode::MAX_STRUCT_DEFINITIONS_REACHED, + ); + let mut module = leaf_module("M"); + multi_struct(&mut module, MAX_STRUCTS + 1); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!( + res.unwrap_err().major_status(), + StatusCode::MAX_STRUCT_DEFINITIONS_REACHED, + ); +} + +#[test] +fn max_fields_test() { + let config = VerifierConfig { + max_data_definitions: Some(MAX_STRUCTS), + max_fields_in_struct: Some(MAX_FIELDS), + max_function_definitions: Some(MAX_FUNCTIONS), + ..Default::default() + }; + let mut module = leaf_module("M"); + multi_struct(&mut module, 1); + multi_fields(&mut module, MAX_FIELDS / 2); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!(res, Ok(())); + let mut module = leaf_module("M"); + multi_struct(&mut module, 10); + multi_fields(&mut module, MAX_FIELDS - 1); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!(res, Ok(())); + let mut module = leaf_module("M"); + multi_struct(&mut module, 50); + multi_fields(&mut module, MAX_FIELDS); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!(res, Ok(())); + let mut module = leaf_module("M"); + multi_struct(&mut module, 100); + multi_fields(&mut module, MAX_FIELDS + 1); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!( + res.unwrap_err().major_status(), + StatusCode::MAX_FIELD_DEFINITIONS_REACHED, + ); + let mut module = leaf_module("M"); + multi_struct(&mut module, 2); + multi_fields(&mut module, MAX_FIELDS * 2); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!( + res.unwrap_err().major_status(), + StatusCode::MAX_FIELD_DEFINITIONS_REACHED, + ); + let mut module = leaf_module("M"); + multi_struct(&mut module, 50); + multi_fields_except_one(&mut module, 0, 2, MAX_FIELDS + 1); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!( + res.unwrap_err().major_status(), + StatusCode::MAX_FIELD_DEFINITIONS_REACHED, + ); + let mut module = leaf_module("M"); + multi_struct(&mut module, 20); + multi_fields_except_one(&mut module, 19, MAX_FIELDS, MAX_FIELDS + 1); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!( + res.unwrap_err().major_status(), + StatusCode::MAX_FIELD_DEFINITIONS_REACHED, + ); + let mut module = leaf_module("M"); + multi_struct(&mut module, 100); + multi_fields_except_one(&mut module, 50, 1, MAX_FIELDS * 2); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!( + res.unwrap_err().major_status(), + StatusCode::MAX_FIELD_DEFINITIONS_REACHED, + ); +} + +#[test] +fn max_functions_test() { + let config = VerifierConfig { + max_data_definitions: Some(MAX_STRUCTS), + max_fields_in_struct: Some(MAX_FIELDS), + max_function_definitions: Some(MAX_FUNCTIONS), + ..Default::default() + }; + let mut module = leaf_module("M"); + multi_struct(&mut module, 1); + multi_functions(&mut module, 1); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!(res, Ok(())); + let mut module = leaf_module("M"); + multi_struct(&mut module, 10); + multi_functions(&mut module, MAX_FUNCTIONS / 2); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!(res, Ok(())); + let mut module = leaf_module("M"); + multi_struct(&mut module, 5); + multi_functions(&mut module, MAX_FUNCTIONS); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!(res, Ok(())); + let mut module = leaf_module("M"); + multi_struct(&mut module, 5); + multi_functions(&mut module, MAX_FUNCTIONS - 1); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!(res, Ok(())); + let mut module = leaf_module("M"); + multi_struct(&mut module, 5); + multi_functions(&mut module, MAX_FUNCTIONS + 1); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!( + res.unwrap_err().major_status(), + StatusCode::MAX_FUNCTION_DEFINITIONS_REACHED, + ); + let mut module = leaf_module("M"); + multi_functions(&mut module, MAX_FUNCTIONS * 2); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!( + res.unwrap_err().major_status(), + StatusCode::MAX_FUNCTION_DEFINITIONS_REACHED, + ); +} + +#[test] +fn max_mixed_config_test() { + let config = VerifierConfig { + max_data_definitions: Some(MAX_STRUCTS), + max_fields_in_struct: Some(MAX_FIELDS), + max_function_definitions: Some(MAX_FUNCTIONS), + ..Default::default() + }; + let mut module = leaf_module("M"); + multi_struct(&mut module, MAX_STRUCTS); + multi_fields(&mut module, MAX_FIELDS); + multi_functions(&mut module, MAX_FUNCTIONS); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!(res, Ok(())); + + let config = VerifierConfig { + max_function_definitions: None, + max_data_definitions: None, + max_fields_in_struct: None, + ..Default::default() + }; + let mut module = leaf_module("M"); + multi_struct(&mut module, 1); + multi_fields(&mut module, 1); + multi_functions(&mut module, 1); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!(res, Ok(())); + let mut module = leaf_module("M"); + multi_struct(&mut module, MAX_STRUCTS); + multi_fields(&mut module, MAX_FIELDS); + multi_functions(&mut module, MAX_FUNCTIONS); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!(res, Ok(())); + let mut module = leaf_module("M"); + multi_struct(&mut module, MAX_STRUCTS * 2); + multi_fields(&mut module, MAX_FIELDS * 2); + multi_functions(&mut module, MAX_FUNCTIONS * 2); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!(res, Ok(())); + let mut module = leaf_module("M"); + multi_struct(&mut module, MAX_STRUCTS + 1); + multi_fields(&mut module, MAX_FIELDS + 1); + multi_functions(&mut module, MAX_FUNCTIONS + 1); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!(res, Ok(())); + + let config = VerifierConfig { + max_data_definitions: Some(MAX_STRUCTS), + max_fields_in_struct: Some(MAX_FIELDS), + ..Default::default() + }; + let mut module = leaf_module("M"); + multi_struct(&mut module, MAX_STRUCTS); + multi_fields(&mut module, MAX_FIELDS); + multi_functions(&mut module, MAX_FUNCTIONS); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!(res, Ok(())); + let mut module = leaf_module("M"); + multi_struct(&mut module, MAX_STRUCTS); + multi_fields(&mut module, MAX_FIELDS); + multi_functions(&mut module, MAX_FUNCTIONS + 10); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!(res, Ok(())); + let mut module = leaf_module("M"); + multi_struct(&mut module, MAX_STRUCTS); + multi_fields(&mut module, MAX_FIELDS); + multi_functions(&mut module, MAX_FUNCTIONS * 3); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!(res, Ok(())); + let mut module = leaf_module("M"); + multi_struct(&mut module, MAX_STRUCTS * 2); + multi_fields(&mut module, MAX_FIELDS); + multi_functions(&mut module, MAX_FUNCTIONS + 1); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!( + res.unwrap_err().major_status(), + StatusCode::MAX_STRUCT_DEFINITIONS_REACHED, + ); + let mut module = leaf_module("M"); + multi_struct(&mut module, MAX_STRUCTS); + multi_fields(&mut module, MAX_FIELDS * 2); + multi_functions(&mut module, MAX_FUNCTIONS * 3); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!( + res.unwrap_err().major_status(), + StatusCode::MAX_FIELD_DEFINITIONS_REACHED, + ); + + let config = VerifierConfig { + max_data_definitions: Some(MAX_STRUCTS), + max_function_definitions: Some(MAX_FUNCTIONS), + ..Default::default() + }; + let mut module = leaf_module("M"); + multi_struct(&mut module, MAX_STRUCTS); + multi_fields(&mut module, MAX_FIELDS); + multi_functions(&mut module, MAX_FUNCTIONS); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!(res, Ok(())); + let mut module = leaf_module("M"); + multi_struct(&mut module, MAX_STRUCTS); + multi_fields(&mut module, MAX_FIELDS + 1); + multi_functions(&mut module, MAX_FUNCTIONS); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!(res, Ok(())); + let mut module = leaf_module("M"); + multi_struct(&mut module, MAX_STRUCTS); + multi_fields(&mut module, MAX_FIELDS * 3); + multi_functions(&mut module, MAX_FUNCTIONS); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!(res, Ok(())); + let mut module = leaf_module("M"); + multi_struct(&mut module, MAX_STRUCTS * 2); + multi_fields(&mut module, MAX_FIELDS * 3); + multi_functions(&mut module, MAX_FUNCTIONS); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!( + res.unwrap_err().major_status(), + StatusCode::MAX_STRUCT_DEFINITIONS_REACHED, + ); + let mut module = leaf_module("M"); + multi_struct(&mut module, MAX_STRUCTS); + multi_fields(&mut module, MAX_FIELDS * 2); + multi_functions(&mut module, MAX_FUNCTIONS * 2); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!( + res.unwrap_err().major_status(), + StatusCode::MAX_FUNCTION_DEFINITIONS_REACHED, + ); + + let config = VerifierConfig { + max_fields_in_struct: Some(MAX_FIELDS), + max_function_definitions: Some(MAX_FUNCTIONS), + ..Default::default() + }; + let mut module = leaf_module("M"); + multi_struct(&mut module, MAX_STRUCTS); + multi_fields(&mut module, MAX_FIELDS); + multi_functions(&mut module, MAX_FUNCTIONS); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!(res, Ok(())); + let mut module = leaf_module("M"); + multi_struct(&mut module, MAX_STRUCTS * 3); + multi_fields(&mut module, MAX_FIELDS); + multi_functions(&mut module, MAX_FUNCTIONS); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!(res, Ok(())); + let mut module = leaf_module("M"); + multi_struct(&mut module, MAX_STRUCTS + 1); + multi_fields(&mut module, MAX_FIELDS); + multi_functions(&mut module, MAX_FUNCTIONS); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!(res, Ok(())); + let mut module = leaf_module("M"); + multi_struct(&mut module, MAX_STRUCTS + 1); + multi_fields(&mut module, MAX_FIELDS * 3); + multi_functions(&mut module, MAX_FUNCTIONS); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!( + res.unwrap_err().major_status(), + StatusCode::MAX_FIELD_DEFINITIONS_REACHED, + ); + let mut module = leaf_module("M"); + multi_struct(&mut module, MAX_STRUCTS * 2); + multi_fields(&mut module, MAX_FIELDS); + multi_functions(&mut module, MAX_FUNCTIONS * 2); + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!( + res.unwrap_err().major_status(), + StatusCode::MAX_FUNCTION_DEFINITIONS_REACHED, + ); +} + +#[test] +fn max_identifier_len() { + let (config, _) = production_config(); + let max_ident = "z".repeat( + config + .max_identifier_len + .unwrap_or(DEFAULT_MAX_IDENTIFIER_LENGTH) as usize, + ); + let good_module = leaf_module(&max_ident); + + let res = LimitsVerifier::verify_module(&config, &good_module); + assert!(res.is_ok()); + + let max_ident = "z".repeat( + (config + .max_identifier_len + .unwrap_or(DEFAULT_MAX_IDENTIFIER_LENGTH) as usize) + / 2, + ); + let good_module = leaf_module(&max_ident); + + let res = LimitsVerifier::verify_module(&config, &good_module); + assert!(res.is_ok()); + + let over_max_ident = "z".repeat( + 1 + config + .max_identifier_len + .unwrap_or(DEFAULT_MAX_IDENTIFIER_LENGTH) as usize, + ); + let bad_module = leaf_module(&over_max_ident); + let res = LimitsVerifier::verify_module(&config, &bad_module); + + assert_eq!( + res.unwrap_err().major_status(), + StatusCode::IDENTIFIER_TOO_LONG, + ); + + let over_max_ident = "zx".repeat( + 1 + config + .max_identifier_len + .unwrap_or(DEFAULT_MAX_IDENTIFIER_LENGTH) as usize, + ); + let bad_module = leaf_module(&over_max_ident); + let res = LimitsVerifier::verify_module(&config, &bad_module); + + assert_eq!( + res.unwrap_err().major_status(), + StatusCode::IDENTIFIER_TOO_LONG, + ); +} + +#[test] +fn max_vec_len() { + let config = VerifierConfig { + max_constant_vector_len: Some(0xFFFF - 1), + ..Default::default() + }; + let double_vec = |item: Vec| -> Vec { + let mut items = vec![2]; + items.extend(item.clone()); + items.extend(item); + items + }; + let large_vec = |item: Vec| -> Vec { + let mut items = vec![0xFF, 0xFF, 3]; + (0..0xFFFF).for_each(|_| items.extend(item.clone())); + items + }; + fn tvec(s: SignatureToken) -> SignatureToken { + SignatureToken::Vector(Box::new(s)) + } + + let mut module = empty_module(); + module.constant_pool = vec![Constant { + type_: tvec(SignatureToken::Bool), + data: large_vec(vec![0]), + }]; + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!( + res.unwrap_err().major_status(), + StatusCode::TOO_MANY_VECTOR_ELEMENTS, + ); + + let mut module = empty_module(); + module.constant_pool = vec![Constant { + type_: tvec(SignatureToken::U256), + data: large_vec(vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]), + }]; + let res = LimitsVerifier::verify_module(&config, &module); + assert_eq!( + res.unwrap_err().major_status(), + StatusCode::TOO_MANY_VECTOR_ELEMENTS, + ); + + let config = VerifierConfig { + max_constant_vector_len: Some(0xFFFF), + ..Default::default() + }; + + let mut module = empty_module(); + module.constant_pool = vec![ + // empty + Constant { + type_: tvec(SignatureToken::Bool), + data: vec![0], + }, + Constant { + type_: tvec(tvec(SignatureToken::Bool)), + data: vec![0], + }, + Constant { + type_: tvec(tvec(tvec(tvec(SignatureToken::Bool)))), + data: vec![0], + }, + Constant { + type_: tvec(tvec(tvec(tvec(SignatureToken::Bool)))), + data: double_vec(vec![0]), + }, + // small + Constant { + type_: tvec(SignatureToken::Bool), + data: vec![9, 1, 1, 1, 1, 1, 1, 1, 1, 1], + }, + Constant { + type_: tvec(SignatureToken::U8), + data: vec![9, 1, 1, 1, 1, 1, 1, 1, 1, 1], + }, + // large + Constant { + type_: tvec(SignatureToken::Bool), + data: large_vec(vec![0]), + }, + Constant { + type_: tvec(SignatureToken::U8), + data: large_vec(vec![0]), + }, + Constant { + type_: tvec(SignatureToken::U16), + data: large_vec(vec![0, 0]), + }, + Constant { + type_: tvec(SignatureToken::U32), + data: large_vec(vec![0, 0, 0, 0]), + }, + Constant { + type_: tvec(SignatureToken::U64), + data: large_vec(vec![0, 0, 0, 0, 0, 0, 0, 0]), + }, + Constant { + type_: tvec(SignatureToken::U128), + data: large_vec(vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + }, + Constant { + type_: tvec(SignatureToken::U256), + data: large_vec(vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, + ]), + }, + Constant { + type_: tvec(SignatureToken::Address), + data: large_vec(vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, + ]), + }, + // double large + Constant { + type_: tvec(tvec(SignatureToken::Bool)), + data: double_vec(large_vec(vec![0])), + }, + Constant { + type_: tvec(tvec(SignatureToken::U8)), + data: double_vec(large_vec(vec![0])), + }, + Constant { + type_: tvec(tvec(SignatureToken::U16)), + data: double_vec(large_vec(vec![0, 0])), + }, + Constant { + type_: tvec(tvec(SignatureToken::U32)), + data: double_vec(large_vec(vec![0, 0, 0, 0])), + }, + Constant { + type_: tvec(tvec(SignatureToken::U64)), + data: double_vec(large_vec(vec![0, 0, 0, 0, 0, 0, 0, 0])), + }, + Constant { + type_: tvec(tvec(SignatureToken::U128)), + data: double_vec(large_vec(vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ])), + }, + Constant { + type_: tvec(tvec(SignatureToken::U256)), + data: double_vec(large_vec(vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, + ])), + }, + Constant { + type_: tvec(tvec(SignatureToken::Address)), + data: double_vec(large_vec(vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, + ])), + }, + ]; + let res = LimitsVerifier::verify_module(&config, &module); + + assert!(res.is_ok()); +} + +fn multi_struct(module: &mut CompiledModule, count: usize) { + for i in 0..count { + module + .identifiers + .push(Identifier::new(format!("A_{}", i)).unwrap()); + module.datatype_handles.push(DatatypeHandle { + module: module.self_module_handle_idx, + name: IdentifierIndex((module.identifiers.len() - 1) as u16), + abilities: AbilitySet::EMPTY, + type_parameters: vec![], + }); + module.struct_defs.push(StructDefinition { + struct_handle: DatatypeHandleIndex((module.datatype_handles.len() - 1) as u16), + field_information: StructFieldInformation::Declared(vec![]), + }); + } +} + +fn multi_fields(module: &mut CompiledModule, count: usize) { + for def in &mut module.struct_defs { + let mut fields = vec![]; + for i in 0..count { + module + .identifiers + .push(Identifier::new(format!("f_{}", i)).unwrap()); + fields.push(FieldDefinition { + name: Default::default(), + signature: TypeSignature(SignatureToken::U8), + }); + } + def.field_information = StructFieldInformation::Declared(fields); + } +} + +fn multi_fields_except_one(module: &mut CompiledModule, idx: usize, count: usize, one: usize) { + for (struct_idx, def) in module.struct_defs.iter_mut().enumerate() { + let mut fields = vec![]; + let count = if struct_idx == idx { one } else { count }; + for i in 0..count { + module + .identifiers + .push(Identifier::new(format!("f_{}", i)).unwrap()); + fields.push(FieldDefinition { + name: Default::default(), + signature: TypeSignature(SignatureToken::U8), + }); + } + def.field_information = StructFieldInformation::Declared(fields); + } +} + +fn multi_functions(module: &mut CompiledModule, count: usize) { + module.signatures.push(Signature(vec![])); + for i in 0..count { + module + .identifiers + .push(Identifier::new(format!("func_{}", i)).unwrap()); + module.function_handles.push(FunctionHandle { + module: module.self_module_handle_idx, + name: IdentifierIndex((module.identifiers.len() - 1) as u16), + parameters: SignatureIndex((module.signatures.len() - 1) as u16), + return_: SignatureIndex((module.signatures.len() - 1) as u16), + type_parameters: vec![], + }); + module.function_defs.push(FunctionDefinition { + function: FunctionHandleIndex((module.function_handles.len() - 1) as u16), + visibility: Visibility::Public, + is_entry: false, + acquires_global_resources: vec![], + code: Some(CodeUnit { + locals: SignatureIndex((module.signatures.len() - 1) as u16), + code: vec![Bytecode::Ret], + jump_tables: vec![], + }), + }); + } +} + +fn leaf_module(name: &str) -> CompiledModule { + let mut module = empty_module(); + module.identifiers[0] = Identifier::new(name).unwrap(); + module.address_identifiers[0] = AccountAddress::ONE; + module +} diff --git a/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/locals.rs b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/locals.rs new file mode 100644 index 0000000000000..cc43c7f1c543f --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/locals.rs @@ -0,0 +1,124 @@ +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +use crate::unit_tests::production_config; +use move_binary_format::file_format::{ + Bytecode, CodeUnit, FunctionDefinition, FunctionHandle, FunctionHandleIndex, IdentifierIndex, + ModuleHandleIndex, Signature, SignatureIndex, SignatureToken, Visibility::Public, empty_module, +}; +use move_bytecode_verifier_meter::bound::BoundMeter; +use move_core_types::{identifier::Identifier, vm_status::StatusCode}; + +#[test] +fn test_locals() { + // See also: github.com/aptos-labs/aptos-core/security/advisories/GHSA-jjqw-f9pc-525j + let mut m = empty_module(); + + const MAX_BASIC_BLOCKS: u16 = 1024; + const MAX_LOCALS: u8 = 255; + const NUM_FUNCTIONS: u16 = 16; + + m.function_handles.push(FunctionHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(0), + parameters: SignatureIndex(0), + return_: SignatureIndex(0), + type_parameters: vec![], + }); + + m.function_defs.push(FunctionDefinition { + function: FunctionHandleIndex(0), + visibility: Public, + is_entry: true, + acquires_global_resources: vec![], + code: Some(CodeUnit { + locals: SignatureIndex(0), + code: vec![Bytecode::Ret], + jump_tables: vec![], + }), + }); + + // signature of locals in f1..f + m.signatures.push(Signature( + std::iter::repeat_n(SignatureToken::U8, MAX_LOCALS as usize).collect(), + )); + + m.identifiers.push(Identifier::new("pwn").unwrap()); + + // create returns_bool_and_u64 + m.signatures + .push(Signature(vec![SignatureToken::Bool, SignatureToken::U8])); + m.identifiers + .push(Identifier::new("returns_bool_and_u64").unwrap()); + m.function_handles.push(FunctionHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(1), + parameters: SignatureIndex(0), + return_: SignatureIndex(2), + type_parameters: vec![], + }); + m.function_defs.push(FunctionDefinition { + function: FunctionHandleIndex(1), + visibility: Public, + is_entry: false, + acquires_global_resources: vec![], + code: Some(CodeUnit { + locals: SignatureIndex(0), + code: vec![Bytecode::LdTrue, Bytecode::LdU8(0), Bytecode::Ret], + jump_tables: vec![], + }), + }); + + // create other functions + for i in 1..(NUM_FUNCTIONS + 1) { + m.identifiers + .push(Identifier::new(format!("f{}", i)).unwrap()); + m.function_handles.push(FunctionHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(i + 1), // the +1 accounts for returns_bool_and_u64 + parameters: SignatureIndex(0), + return_: SignatureIndex(0), + type_parameters: vec![], + }); + m.function_defs.push(FunctionDefinition { + function: FunctionHandleIndex(i + 1), + visibility: Public, + is_entry: false, + acquires_global_resources: vec![], + code: Some(CodeUnit { + locals: SignatureIndex(1), + code: vec![], + jump_tables: vec![], + }), + }); + + let code = &mut m.function_defs[i as usize + 1].code.as_mut().unwrap().code; + + for _ in 0..(MAX_BASIC_BLOCKS / 2 - MAX_LOCALS as u16 - 3) { + code.push(Bytecode::LdTrue); + code.push(Bytecode::BrTrue((code.len() + 2) as u16)); + code.push(Bytecode::Ret); + code.push(Bytecode::LdTrue); + code.push(Bytecode::BrTrue(0)); + } + for i in 0..MAX_LOCALS { + code.push(Bytecode::Call(FunctionHandleIndex(1))); // calls returns_bool_and_u64 + code.push(Bytecode::StLoc(i)); // i'th local is now available for the first time + code.push(Bytecode::BrTrue(0)); + } + code.push(Bytecode::Ret); + } + + let (verifier_config, meter_config) = production_config(); + let mut meter = BoundMeter::new(meter_config); + let result = move_bytecode_verifier::verify_module_with_config_for_test( + "test_locals", + &verifier_config, + &m, + &mut meter, + ); + assert_eq!( + result.unwrap_err().major_status(), + StatusCode::CONSTRAINT_NOT_SATISFIED + ); +} diff --git a/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/loop_summary_tests.rs b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/loop_summary_tests.rs new file mode 100644 index 0000000000000..0bfba81b9f36f --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/loop_summary_tests.rs @@ -0,0 +1,416 @@ +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +use move_abstract_interpreter::control_flow_graph::VMControlFlowGraph; +use move_binary_format::file_format::Bytecode; +use move_bytecode_verifier::loop_summary::{LoopPartition, LoopSummary}; + +macro_rules! assert_node { + ( $summary:ident, $node:expr ; $block:expr, $preds:expr, $descs:expr, $backs:expr ) => { + let (s, n) = (&$summary, $node); + assert_eq!(s.block(n), $block, "Block"); + + let descs = $descs; + for d in descs { + assert!(s.is_descendant(n, *d), "{:?} -> {:?}", n, d) + } + + assert_eq!(s.pred_edges(n), $preds, "Predecessor Edges"); + assert_eq!(s.back_edges(n), $backs, "Back Edges"); + }; +} + +#[test] +fn linear_summary() { + let summary = { + use Bytecode::*; + LoopSummary::new(&VMControlFlowGraph::new( + &[ + /* B0, L0 */ Nop, + /* */ Branch(2), + /* B2, L1 */ Nop, + /* */ Branch(4), + /* B4, L2 */ Ret, + ], + &[], + )) + }; + + let n: Vec<_> = summary.preorder().collect(); + + assert_eq!(n.len(), 3); + + assert_node!( + summary, n[0]; + /* block */ 0, + /* preds */ &[], + /* descs */ &[n[1], n[2]], + /* backs */ &[] + ); + + assert_node!( + summary, n[1]; + /* block */ 2, + /* preds */ &[n[0]], + /* descs */ &[n[2]], + /* backs */ &[] + ); + + assert_node!( + summary, n[2]; + /* block */ 4, + /* preds */ &[n[1]], + /* descs */ &[], + /* backs */ &[] + ); +} + +#[test] +fn non_loop_back_branch_summary() { + let summary = { + use Bytecode::*; + LoopSummary::new(&VMControlFlowGraph::new( + &[ + /* B0, L0 */ Nop, + /* */ Branch(3), + /* B2, L2 */ Ret, + /* B3, L1 */ Branch(2), + ], + &[], + )) + }; + + let n: Vec<_> = summary.preorder().collect(); + + assert_eq!(n.len(), 3); + + assert_node!( + summary, n[0]; + /* block */ 0, + /* preds */ &[], + /* descs */ &[n[1], n[2]], + /* backs */ &[] + ); + + assert_node!( + summary, n[1]; + /* block */ 3, + /* preds */ &[n[0]], + /* descs */ &[n[2]], + /* backs */ &[] + ); + + assert_node!( + summary, n[2]; + /* block */ 2, + /* preds */ &[n[1]], + /* descs */ &[], + /* backs */ &[] + ); +} + +#[test] +fn branching_summary() { + let summary = { + use Bytecode::*; + LoopSummary::new(&VMControlFlowGraph::new( + &[ + /* B0, L0 */ LdTrue, + /* */ BrTrue(3), + /* B2, L2 */ Nop, + /* B3, L1 */ Ret, + ], + &[], + )) + }; + + let n: Vec<_> = summary.preorder().collect(); + + assert_eq!(n.len(), 3); + + assert_node!( + summary, n[0]; + /* block */ 0, + /* preds */ &[], + /* descs */ &[n[1], n[2]], + /* backs */ &[] + ); + + assert_node!( + summary, n[1]; + /* block */ 3, + /* preds */ &[n[0], n[2]], + /* descs */ &[], + /* backs */ &[] + ); + + assert_node!( + summary, n[2]; + /* block */ 2, + /* preds */ &[n[0]], + /* descs */ &[], + /* backs */ &[] + ); + + // Although L2 -> L1 is an edge in the CFG, it's not an edge in the DFST, so L2 is said to have + // no descendants. + assert!(!summary.is_descendant(n[2], n[1])); +} + +#[test] +fn looping_summary() { + let summary = { + use Bytecode::*; + LoopSummary::new(&VMControlFlowGraph::new( + &[ + /* B0, L0 */ LdTrue, + /* */ BrTrue(4), + /* B2, L2 */ Nop, + /* */ Branch(0), + /* B4, L1 */ Ret, + ], + &[], + )) + }; + + let n: Vec<_> = summary.preorder().collect(); + + assert_eq!(n.len(), 3); + + assert_node!( + summary, n[0]; + /* block */ 0, + /* preds */ &[], + /* descs */ &[n[1], n[2]], + /* backs */ &[n[2]] + ); + + assert_node!( + summary, n[1]; + /* block */ 4, + /* preds */ &[n[0]], + /* descs */ &[], + /* backs */ &[] + ); + + assert_node!( + summary, n[2]; + /* block */ 2, + /* preds */ &[n[0]], + /* descs */ &[], + /* backs */ &[] + ); +} + +#[test] +fn branches_in_loops_summary() { + let summary = { + use Bytecode::*; + LoopSummary::new(&VMControlFlowGraph::new( + &[ + /* B0, L0 */ LdTrue, + /* */ BrTrue(3), + /* B2, L3 */ Nop, + /* B3, L1 */ LdFalse, + /* */ BrFalse(0), + /* B5, L2 */ Ret, + ], + &[], + )) + }; + + let n: Vec<_> = summary.preorder().collect(); + + assert_eq!(n.len(), 4); + + assert_node!( + summary, n[0]; + /* block */ 0, + /* preds */ &[], + /* descs */ &[n[1], n[2], n[3]], + /* backs */ &[n[1]] + ); + + assert_node!( + summary, n[1]; + /* block */ 3, + /* preds */ &[n[0], n[3]], + /* descs */ &[n[2]], + /* backs */ &[] + ); + + assert_node!( + summary, n[2]; + /* block */ 5, + /* preds */ &[n[1]], + /* descs */ &[], + /* backs */ &[] + ); + + assert_node!( + summary, n[3]; + /* block */ 2, + /* preds */ &[n[0]], + /* descs */ &[], + /* backs */ &[] + ); +} + +#[test] +fn loops_in_branches_summary() { + let summary = { + use Bytecode::*; + LoopSummary::new(&VMControlFlowGraph::new( + &[ + /* B0, L0 */ LdTrue, + /* */ BrTrue(8), + /* B2, L5 */ Nop, + /* B3, L6 */ LdFalse, + /* */ BrFalse(3), + /* B5, L7 */ LdTrue, + /* */ BrTrue(2), + /* B7, L8 */ Branch(13), + /* B8, L1 */ Nop, + /* B9, L2 */ LdTrue, + /* */ BrTrue(8), + /* B11, L3 */ LdFalse, + /* */ BrFalse(9), + /* B13, L4 */ Ret, + ], + &[], + )) + }; + + let n: Vec<_> = summary.preorder().collect(); + + assert_eq!(n.len(), 9); + + assert_node!( + summary, n[0]; + /* block */ 0, + /* preds */ &[], + /* descs */ &[n[1], n[2], n[3], n[4], n[5], n[6], n[7], n[8]], + /* backs */ &[] + ); + + assert_node!( + summary, n[1]; + /* block */ 8, + /* preds */ &[n[0]], + /* descs */ &[n[2], n[3], n[4]], + /* backs */ &[n[2]] + ); + + assert_node!( + summary, n[2]; + /* block */ 9, + /* preds */ &[n[1]], + /* descs */ &[n[3], n[4]], + /* backs */ &[n[3]] + ); + + assert_node!( + summary, n[3]; + /* block */ 11, + /* preds */ &[n[2]], + /* descs */ &[n[4]], + /* backs */ &[] + ); + + assert_node!( + summary, n[4]; + /* block */ 13, + /* preds */ &[n[3], n[8]], + /* descs */ &[], + /* backs */ &[] + ); + + assert_node!( + summary, n[5]; + /* block */ 2, + /* preds */ &[n[0]], + /* descs */ &[n[6], n[7], n[8]], + /* backs */ &[n[7]] + ); + + assert_node!( + summary, n[6]; + /* block */ 3, + /* preds */ &[n[5]], + /* descs */ &[n[7], n[8]], + /* backs */ &[n[6]] + ); + + assert_node!( + summary, n[7]; + /* block */ 5, + /* preds */ &[n[6]], + /* descs */ &[n[8]], + /* backs */ &[] + ); + + assert_node!( + summary, n[8]; + /* block */ 7, + /* preds */ &[n[7]], + /* descs */ &[], + /* backs */ &[] + ); +} + +#[test] +fn loop_collapsing() { + let summary = { + use Bytecode::*; + LoopSummary::new(&VMControlFlowGraph::new( + &[ + /* B0, L0 */ LdTrue, + /* */ BrTrue(4), + /* B2, L2 */ Nop, + /* */ Branch(0), + /* B4, L1 */ Ret, + ], + &[], + )) + }; + + let mut partition = LoopPartition::new(&summary); + let n: Vec<_> = summary.preorder().collect(); + + for id in &n { + assert_eq!(*id, partition.containing_loop(*id), "Self-parent {:?}", id); + } + + assert_eq!(partition.collapse_loop(n[0], &[n[2]].into()), 1); + assert_eq!(partition.containing_loop(n[0]), n[0]); + assert_eq!(partition.containing_loop(n[1]), n[1]); + assert_eq!(partition.containing_loop(n[2]), n[0]); +} + +#[test] +fn nested_loop_collapsing() { + let summary = { + use Bytecode::*; + LoopSummary::new(&VMControlFlowGraph::new( + &[ + /* B0, L0 */ Nop, + /* B1, L1 */ LdTrue, + /* */ BrTrue(1), + /* B3, L2 */ LdFalse, + /* */ BrFalse(0), + /* B5, L3 */ LdTrue, + /* */ BrTrue(0), + /* B7, L4 */ Ret, + ], + &[], + )) + }; + + let mut partition = LoopPartition::new(&summary); + let n: Vec<_> = summary.preorder().collect(); + + // Self-loop is a special case -- its depth should still be bumped. + assert_eq!(partition.collapse_loop(n[1], &[].into()), 1); + assert_eq!(partition.collapse_loop(n[0], &[n[1], n[2]].into()), 2); + assert_eq!(partition.collapse_loop(n[0], &[n[0], n[3]].into()), 3); +} diff --git a/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/many_back_edges.rs b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/many_back_edges.rs new file mode 100644 index 0000000000000..7a2708912a36c --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/many_back_edges.rs @@ -0,0 +1,99 @@ +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +use crate::unit_tests::production_config; +use move_binary_format::file_format::{ + Bytecode, CodeUnit, FunctionDefinition, FunctionHandle, FunctionHandleIndex, IdentifierIndex, + ModuleHandleIndex, Signature, SignatureIndex, SignatureToken, Visibility::Public, empty_module, +}; +use move_bytecode_verifier_meter::bound::BoundMeter; +use move_core_types::{identifier::Identifier, vm_status::StatusCode}; + +const MAX_BASIC_BLOCKS: u16 = 1024; +const MAX_LOCALS: u8 = 255; + +const NUM_FUNCTIONS: u16 = 16; + +#[test] +fn many_backedges() { + let mut m = empty_module(); + + // signature of locals in f1..f + m.signatures.push(Signature( + std::iter::repeat_n(SignatureToken::U8, MAX_LOCALS as usize).collect(), + )); + + // create returns_bool_and_u64 + m.signatures + .push(Signature(vec![SignatureToken::Bool, SignatureToken::U8])); + m.identifiers + .push(Identifier::new("returns_bool_and_u64").unwrap()); + m.function_handles.push(FunctionHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(1), + parameters: SignatureIndex(0), + return_: SignatureIndex(2), + type_parameters: vec![], + }); + m.function_defs.push(FunctionDefinition { + function: FunctionHandleIndex(0), + visibility: Public, + is_entry: false, + acquires_global_resources: vec![], + code: Some(CodeUnit { + locals: SignatureIndex(0), + code: vec![Bytecode::LdTrue, Bytecode::LdU8(0), Bytecode::Ret], + jump_tables: vec![], + }), + }); + + // create other functions + for i in 1..(NUM_FUNCTIONS + 1) { + m.identifiers + .push(Identifier::new(format!("f{}", i)).unwrap()); + m.function_handles.push(FunctionHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(i + 1), // the +1 accounts for returns_bool_and_u64 + parameters: SignatureIndex(0), + return_: SignatureIndex(0), + type_parameters: vec![], + }); + m.function_defs.push(FunctionDefinition { + function: FunctionHandleIndex(i), + visibility: Public, + is_entry: false, + acquires_global_resources: vec![], + code: Some(CodeUnit { + locals: SignatureIndex(1), + code: vec![], + jump_tables: vec![], + }), + }); + + let code = &mut m.function_defs[i as usize].code.as_mut().unwrap().code; + + for _ in 0..(MAX_BASIC_BLOCKS - MAX_LOCALS as u16 - 2) { + code.push(Bytecode::LdTrue); + code.push(Bytecode::BrTrue(0)); + } + for i in 0..MAX_LOCALS { + code.push(Bytecode::Call(FunctionHandleIndex(0))); // calls returns_bool_and_u64 + code.push(Bytecode::StLoc(i)); // i'th local is now available for the first time + code.push(Bytecode::BrTrue(0)); + } + code.push(Bytecode::Ret); + } + + let (verifier_config, meter_config) = production_config(); + let mut meter = BoundMeter::new(meter_config); + let result = move_bytecode_verifier::verify_module_with_config_for_test( + "many_backedges", + &verifier_config, + &m, + &mut meter, + ); + assert_eq!( + result.unwrap_err().major_status(), + StatusCode::CONSTRAINT_NOT_SATISFIED + ); +} diff --git a/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/mod.rs b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/mod.rs new file mode 100644 index 0000000000000..1fd6989229d60 --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/mod.rs @@ -0,0 +1,64 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +use move_binary_format::file_format_common::VERSION_MAX; +use move_vm_config::verifier::{ + DEFAULT_MAX_CONSTANT_VECTOR_LEN, DEFAULT_MAX_IDENTIFIER_LENGTH, DEFAULT_MAX_VARIANTS, + MeterConfig, VerifierConfig, +}; + +pub mod binary_samples; +pub mod bounds_tests; +pub mod code_unit_tests; +pub mod constants_tests; +pub mod control_flow_tests; +pub mod duplication_tests; +pub mod generic_ops_tests; +pub mod large_type_test; +pub mod limit_tests; +pub mod locals; +pub mod loop_summary_tests; +pub mod many_back_edges; +pub mod negative_stack_size_tests; +pub mod reference_safety_tests; +pub mod signature_tests; +pub mod vec_pack_tests; + +/// Configuration used in production. +pub(crate) fn production_config() -> (VerifierConfig, MeterConfig) { + ( + VerifierConfig { + max_loop_depth: Some(5), + max_generic_instantiation_length: Some(32), + max_function_parameters: Some(128), + max_basic_blocks: Some(1024), + max_basic_blocks_in_script: Some(1024), + max_value_stack_size: 1024, + max_type_nodes: Some(256), + max_push_size: Some(10000), + max_dependency_depth: Some(100), + max_data_definitions: Some(200), + max_fields_in_struct: Some(30), + max_function_definitions: Some(1000), + + // Do not use back edge constraints as they are superseded by metering + max_back_edges_per_function: None, + max_back_edges_per_module: None, + + max_constant_vector_len: Some(DEFAULT_MAX_CONSTANT_VECTOR_LEN), + max_identifier_len: Some(DEFAULT_MAX_IDENTIFIER_LENGTH), + disallow_self_identifier: true, + allow_receiving_object_id: true, + reject_mutable_random_on_entry_functions: true, + bytecode_version: VERSION_MAX, + max_variants_in_enum: Some(DEFAULT_MAX_VARIANTS), + additional_borrow_checks: true, + better_loader_errors: true, + private_generics_verifier_v2: false, + sanity_check_with_regex_reference_safety: Some(2_200_000), + deprecate_global_storage_ops: true, + }, + MeterConfig::old_default(), + ) +} diff --git a/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/negative_stack_size_tests.rs b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/negative_stack_size_tests.rs new file mode 100644 index 0000000000000..1b160ce6be167 --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/negative_stack_size_tests.rs @@ -0,0 +1,76 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +use crate::support::dummy_procedure_module; +use move_binary_format::file_format::Bytecode; +use move_bytecode_verifier::ability_cache::AbilityCache; +use move_bytecode_verifier::code_unit_verifier; +use move_bytecode_verifier_meter::dummy::DummyMeter; +use move_core_types::vm_status::StatusCode; + +#[test] +fn one_pop_no_push() { + let module = &dummy_procedure_module(vec![Bytecode::Pop, Bytecode::Ret]); + let ability_cache = &mut AbilityCache::new(module); + let result = code_unit_verifier::verify_module( + &Default::default(), + module, + ability_cache, + &mut DummyMeter, + ); + assert_eq!( + result.unwrap_err().major_status(), + StatusCode::NEGATIVE_STACK_SIZE_WITHIN_BLOCK + ); +} + +#[test] +fn one_pop_one_push() { + // Height: 0 + (-1 + 1) = 0 would have passed original usage verifier + let module = &dummy_procedure_module(vec![Bytecode::ReadRef, Bytecode::Ret]); + let ability_cache = &mut AbilityCache::new(module); + let result = code_unit_verifier::verify_module( + &Default::default(), + module, + ability_cache, + &mut DummyMeter, + ); + assert_eq!( + result.unwrap_err().major_status(), + StatusCode::NEGATIVE_STACK_SIZE_WITHIN_BLOCK + ); +} + +#[test] +fn two_pop_one_push() { + // Height: 0 + 1 + (-2 + 1) = 0 would have passed original usage verifier + let module = &dummy_procedure_module(vec![Bytecode::LdU64(0), Bytecode::Add, Bytecode::Ret]); + let ability_cache = &mut AbilityCache::new(module); + let result = code_unit_verifier::verify_module( + &Default::default(), + module, + ability_cache, + &mut DummyMeter, + ); + assert_eq!( + result.unwrap_err().major_status(), + StatusCode::NEGATIVE_STACK_SIZE_WITHIN_BLOCK + ); +} + +#[test] +fn two_pop_no_push() { + let module = &dummy_procedure_module(vec![Bytecode::WriteRef, Bytecode::Ret]); + let ability_cache = &mut AbilityCache::new(module); + let result = code_unit_verifier::verify_module( + &Default::default(), + module, + ability_cache, + &mut DummyMeter, + ); + assert_eq!( + result.unwrap_err().major_status(), + StatusCode::NEGATIVE_STACK_SIZE_WITHIN_BLOCK + ); +} diff --git a/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/reference_safety_tests.rs b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/reference_safety_tests.rs new file mode 100644 index 0000000000000..0e172390b3a1e --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/reference_safety_tests.rs @@ -0,0 +1,451 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +use crate::unit_tests::production_config; +use move_binary_format::file_format::{ + Bytecode, CodeUnit, FunctionDefinition, FunctionHandle, FunctionHandleIndex, IdentifierIndex, + ModuleHandleIndex, Signature, SignatureIndex, SignatureToken, Visibility::Public, empty_module, +}; +use move_bytecode_verifier_meter::bound::BoundMeter; +use move_core_types::{identifier::Identifier, vm_status::StatusCode}; + +#[test] +fn test_bicliques() { + // See also: github.com/aptos-labs/aptos-core/security/advisories/GHSA-xm6p-ffcq-5p2v + const NUM_LOCALS: u8 = 128; + const NUM_CALLS: u16 = 76; + const NUM_FUNCTIONS: u16 = 1; + + let mut m = empty_module(); + + m.function_handles.push(FunctionHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(0), + parameters: SignatureIndex(0), + return_: SignatureIndex(0), + type_parameters: vec![], + }); + m.function_defs.push(FunctionDefinition { + function: FunctionHandleIndex(0), + visibility: Public, + is_entry: false, + acquires_global_resources: vec![], + code: Some(CodeUnit { + locals: SignatureIndex(0), + code: vec![Bytecode::Call(FunctionHandleIndex(0)), Bytecode::Ret], + jump_tables: vec![], + }), + }); + + // create take_and_return_references + m.signatures.push(Signature( + std::iter::repeat_n( + SignatureToken::Reference(Box::new(SignatureToken::U64)), + NUM_LOCALS as usize, + ) + .collect(), + )); + m.identifiers + .push(Identifier::new("take_and_return_references").unwrap()); + m.function_handles.push(FunctionHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(1), + parameters: SignatureIndex(1), + return_: SignatureIndex(1), + type_parameters: vec![], + }); + m.function_defs.push(FunctionDefinition { + function: FunctionHandleIndex(1), + visibility: Public, + is_entry: false, + acquires_global_resources: vec![], + code: Some(CodeUnit { + locals: SignatureIndex(0), + code: vec![], + jump_tables: vec![], + }), + }); + let code = &mut m.function_defs[1].code.as_mut().unwrap().code; + for i in 0..NUM_LOCALS { + code.push(Bytecode::MoveLoc(i)); + } + code.push(Bytecode::Ret); + + // create swallow_references + m.identifiers + .push(Identifier::new("swallow_references").unwrap()); + m.function_handles.push(FunctionHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(2), + parameters: SignatureIndex(1), + return_: SignatureIndex(0), + type_parameters: vec![], + }); + m.function_defs.push(FunctionDefinition { + function: FunctionHandleIndex(2), + visibility: Public, + is_entry: false, + acquires_global_resources: vec![], + code: Some(CodeUnit { + locals: SignatureIndex(0), + code: vec![Bytecode::Ret], + jump_tables: vec![], + }), + }); + + // create other functions + for i in 1..(NUM_FUNCTIONS + 1) { + m.identifiers + .push(Identifier::new(format!("f{}", i)).unwrap()); + m.function_handles.push(FunctionHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(i + 2), + parameters: SignatureIndex(1), + return_: SignatureIndex(0), + type_parameters: vec![], + }); + m.function_defs.push(FunctionDefinition { + function: FunctionHandleIndex(i + 2), + visibility: Public, + is_entry: false, + acquires_global_resources: vec![], + code: Some(CodeUnit { + locals: SignatureIndex(0), + code: vec![], + jump_tables: vec![], + }), + }); + let code = &mut m.function_defs[i as usize + 2].code.as_mut().unwrap().code; + for j in 0..NUM_LOCALS { + code.push(Bytecode::CopyLoc(j)); + } + for _ in 0..NUM_CALLS { + code.push(Bytecode::Call(FunctionHandleIndex(1))); + } + code.push(Bytecode::Call(FunctionHandleIndex(2))); + code.push(Bytecode::Ret); + } + + let (verifier_config, meter_config) = production_config(); + let mut meter = BoundMeter::new(meter_config); + let result = move_bytecode_verifier::verify_module_with_config_for_test( + "test_bicliques", + &verifier_config, + &m, + &mut meter, + ); + assert_eq!( + result.unwrap_err().major_status(), + StatusCode::CONSTRAINT_NOT_SATISFIED + ); +} + +#[test] +fn test_merge_state_large_graph() { + // See also: github.com/aptos-labs/aptos-core/security/advisories/GHSA-g8v8-fw4c-8h82 + const N: u8 = 127; + const NUM_NOP_BLOCKS: u16 = 950; + const NUM_FUNCTIONS: u16 = 18; + + let mut m = empty_module(); + + m.function_handles.push(FunctionHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(0), + parameters: SignatureIndex(0), + return_: SignatureIndex(0), + type_parameters: vec![], + }); + m.function_defs.push(FunctionDefinition { + function: FunctionHandleIndex(0), + visibility: Public, + is_entry: false, + acquires_global_resources: vec![], + code: Some(CodeUnit { + locals: SignatureIndex(0), + code: vec![Bytecode::Call(FunctionHandleIndex(0)), Bytecode::Ret], + jump_tables: vec![], + }), + }); + + m.signatures.push(Signature( + std::iter::repeat_n( + SignatureToken::Reference(Box::new(SignatureToken::U8)), + N as usize, + ) + .collect(), + )); + + m.identifiers.push(Identifier::new("return_refs").unwrap()); + m.function_handles.push(FunctionHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(1), + parameters: SignatureIndex(0), + return_: SignatureIndex(1), + type_parameters: vec![], + }); + m.function_defs.push(FunctionDefinition { + function: FunctionHandleIndex(1), + visibility: Public, + is_entry: false, + acquires_global_resources: vec![], + code: Some(CodeUnit { + locals: SignatureIndex(0), + code: vec![Bytecode::Call(FunctionHandleIndex(1)), Bytecode::Ret], + jump_tables: vec![], + }), + }); + + m.identifiers + .push(Identifier::new("take_and_return_refs").unwrap()); + m.function_handles.push(FunctionHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(2), + parameters: SignatureIndex(1), + return_: SignatureIndex(1), + type_parameters: vec![], + }); + m.function_defs.push(FunctionDefinition { + function: FunctionHandleIndex(2), + visibility: Public, + is_entry: false, + acquires_global_resources: vec![], + code: Some(CodeUnit { + locals: SignatureIndex(0), + code: vec![Bytecode::Call(FunctionHandleIndex(1)), Bytecode::Ret], + jump_tables: vec![], + }), + }); + + for i in 0..NUM_FUNCTIONS { + m.identifiers + .push(Identifier::new(format!("f{}", i)).unwrap()); + m.function_handles.push(FunctionHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(i + 3), + parameters: SignatureIndex(1), + return_: SignatureIndex(0), + type_parameters: vec![], + }); + m.function_defs.push(FunctionDefinition { + function: FunctionHandleIndex(i + 3), + visibility: Public, + is_entry: false, + acquires_global_resources: vec![], + code: Some(CodeUnit { + locals: SignatureIndex(1), + code: vec![], + jump_tables: vec![], + }), + }); + let code = &mut m.function_defs[i as usize + 3].code.as_mut().unwrap().code; + for j in 0..N { + code.push(Bytecode::CopyLoc(j)); + } + code.push(Bytecode::Call(FunctionHandleIndex(2))); + for j in 0..N { + code.push(Bytecode::StLoc(N + j)); + } + for _ in 0..NUM_NOP_BLOCKS { + code.push(Bytecode::LdTrue); + code.push(Bytecode::BrTrue(0)); + } + + code.push(Bytecode::Ret); + } + + let (verifier_config, meter_config) = production_config(); + let mut meter = BoundMeter::new(meter_config); + let result = move_bytecode_verifier::verify_module_with_config_for_test( + "test_merge_state_large_graph", + &verifier_config, + &m, + &mut meter, + ); + assert_eq!( + result.unwrap_err().major_status(), + StatusCode::CONSTRAINT_NOT_SATISFIED + ); +} + +#[test] +fn test_merge_state() { + // See also: github.com/aptos-labs/aptos-core/security/advisories/GHSA-g8v8-fw4c-8h82 + const NUM_NOP_BLOCKS: u16 = 965; + const NUM_LOCALS: u8 = 32; + const NUM_FUNCTIONS: u16 = 21; + + let mut m = empty_module(); + + m.function_handles.push(FunctionHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(0), + parameters: SignatureIndex(0), + return_: SignatureIndex(0), + type_parameters: vec![], + }); + m.function_defs.push(FunctionDefinition { + function: FunctionHandleIndex(0), + visibility: Public, + is_entry: false, + acquires_global_resources: vec![], + code: Some(CodeUnit { + locals: SignatureIndex(0), + code: vec![Bytecode::Call(FunctionHandleIndex(0)), Bytecode::Ret], + jump_tables: vec![], + }), + }); + + m.signatures + .push(Signature(vec![SignatureToken::Reference(Box::new( + SignatureToken::U8, + ))])); + m.signatures.push(Signature( + std::iter::repeat_n( + SignatureToken::Reference(Box::new(SignatureToken::U8)), + NUM_LOCALS as usize - 1, + ) + .collect(), + )); + + for i in 0..NUM_FUNCTIONS { + m.identifiers + .push(Identifier::new(format!("f{}", i)).unwrap()); + m.function_handles.push(FunctionHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(i + 1), + parameters: SignatureIndex(1), + return_: SignatureIndex(0), + type_parameters: vec![], + }); + m.function_defs.push(FunctionDefinition { + function: FunctionHandleIndex(i + 1), + visibility: Public, + is_entry: false, + acquires_global_resources: vec![], + code: Some(CodeUnit { + locals: SignatureIndex(2), + code: vec![], + jump_tables: vec![], + }), + }); + let code = &mut m.function_defs[i as usize + 1].code.as_mut().unwrap().code; + // create reference id + code.push(Bytecode::CopyLoc(0)); + code.push(Bytecode::StLoc(1)); + // create a path of length NUM_LOCALS - 1 in the borrow graph + for j in 0..(NUM_LOCALS - 2) { + // create Ref(new_id) and factor in empty-path edge id -> new_id + code.push(Bytecode::CopyLoc(1)); + // can't leave those references on stack since basic blocks need to be stack-neutral + code.push(Bytecode::StLoc(j + 2)); + } + for _ in 0..NUM_NOP_BLOCKS { + code.push(Bytecode::LdTrue); + // create back edge to first block + code.push(Bytecode::BrTrue(0)); + } + + code.push(Bytecode::Ret); + } + + let (verifier_config, meter_config) = production_config(); + let mut meter = BoundMeter::new(meter_config); + let result = move_bytecode_verifier::verify_module_with_config_for_test( + "test_merge_state", + &verifier_config, + &m, + &mut meter, + ); + assert_eq!( + result.unwrap_err().major_status(), + StatusCode::PROGRAM_TOO_COMPLEX + ); +} + +#[test] +fn test_copyloc_pop() { + // See also: github.com/aptos-labs/aptos-core/security/advisories/GHSA-2qvr-c9qp-wch7 + const NUM_COPYLOCS: u16 = 1880; + const NUM_CHILDREN: u16 = 1020; + const NUM_FUNCTIONS: u16 = 2; + + let mut m = empty_module(); + + // parameters of f0, f1, ... + m.signatures + .push(Signature(vec![SignatureToken::Reference(Box::new( + SignatureToken::Vector(Box::new(SignatureToken::U8)), + ))])); + // locals of f0, f1, ... + m.signatures.push(Signature(vec![ + SignatureToken::Reference(Box::new(SignatureToken::Vector(Box::new( + SignatureToken::U8, + )))), + SignatureToken::U8, // ignore this, it's just here because I don't want to fix indices and the TypeParameter after removing the collision + ])); + // for VecImmBorrow + m.signatures.push(Signature( + std::iter::repeat_n(SignatureToken::U8, 1).collect(), + )); + m.signatures + .push(Signature(vec![SignatureToken::TypeParameter(0)])); + + for i in 0..NUM_FUNCTIONS { + m.identifiers + .push(Identifier::new(format!("f{}", i)).unwrap()); + m.function_handles.push(FunctionHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(i), + parameters: SignatureIndex(1), + return_: SignatureIndex(0), + type_parameters: vec![], + }); + m.function_defs.push(FunctionDefinition { + function: FunctionHandleIndex(i), + visibility: Public, + is_entry: false, + acquires_global_resources: vec![], + code: Some(CodeUnit { + locals: SignatureIndex(2), + code: vec![], + jump_tables: vec![], + }), + }); + let code = &mut m.function_defs[i as usize].code.as_mut().unwrap().code; + + // create reference id + code.push(Bytecode::CopyLoc(0)); + code.push(Bytecode::StLoc(1)); + // create NUM_CHLIDREN children of id + for _ in 0..NUM_CHILDREN { + code.push(Bytecode::CopyLoc(1)); + code.push(Bytecode::LdU64(0)); + code.push(Bytecode::VecImmBorrow(SignatureIndex(3))); + } + // then do a whole lot of copylocs on that reference + for _ in 0..NUM_COPYLOCS { + code.push(Bytecode::CopyLoc(1)); + code.push(Bytecode::Pop); + } + for _ in 0..NUM_CHILDREN { + code.push(Bytecode::Pop); + } + + code.push(Bytecode::Ret); + } + + let (verifier_config, meter_config) = production_config(); + let mut meter = BoundMeter::new(meter_config); + let result = move_bytecode_verifier::verify_module_with_config_for_test( + "test_copyloc_pop", + &verifier_config, + &m, + &mut meter, + ); + assert_eq!( + result.unwrap_err().major_status(), + StatusCode::CONSTRAINT_NOT_SATISFIED + ); +} diff --git a/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/signature_tests.rs b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/signature_tests.rs new file mode 100644 index 0000000000000..272bccfafda0b --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/signature_tests.rs @@ -0,0 +1,207 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +use crate::unit_tests::production_config; +use move_binary_format::file_format::{ + Bytecode::*, CompiledModule, SignatureToken::*, Visibility::Public, *, +}; +use move_bytecode_verifier::{ + SignatureChecker, ability_cache::AbilityCache, verify_module_unmetered, + verify_module_with_config_for_test, +}; +use move_bytecode_verifier_meter::dummy::DummyMeter; +use move_core_types::{ + account_address::AccountAddress, identifier::Identifier, vm_status::StatusCode, +}; + +#[test] +fn test_reference_of_reference() { + let mut m = basic_test_module(); + m.signatures[0] = Signature(vec![Reference(Box::new(Reference(Box::new( + SignatureToken::Bool, + ))))]); + let ability_cache = &mut AbilityCache::new(&m); + let errors = SignatureChecker::verify_module(&m, ability_cache, &mut DummyMeter); + assert!(errors.is_err()); +} + +#[test] +fn no_verify_locals_good() { + let compiled_module_good = CompiledModule { + version: move_binary_format::file_format_common::VERSION_MAX, + publishable: true, + module_handles: vec![ModuleHandle { + address: AddressIdentifierIndex(0), + name: IdentifierIndex(0), + }], + self_module_handle_idx: ModuleHandleIndex(0), + datatype_handles: vec![], + signatures: vec![ + Signature(vec![Address]), + Signature(vec![U64]), + Signature(vec![]), + ], + function_handles: vec![ + FunctionHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(1), + return_: SignatureIndex(2), + parameters: SignatureIndex(0), + type_parameters: vec![], + }, + FunctionHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(2), + return_: SignatureIndex(2), + parameters: SignatureIndex(1), + type_parameters: vec![], + }, + ], + field_handles: vec![], + friend_decls: vec![], + struct_def_instantiations: vec![], + function_instantiations: vec![], + field_instantiations: vec![], + identifiers: vec![ + Identifier::new("Bad").unwrap(), + Identifier::new("blah").unwrap(), + Identifier::new("foo").unwrap(), + ], + address_identifiers: vec![AccountAddress::new([0; AccountAddress::LENGTH])], + constant_pool: vec![], + metadata: vec![], + struct_defs: vec![], + function_defs: vec![ + FunctionDefinition { + function: FunctionHandleIndex(0), + visibility: Visibility::Public, + is_entry: false, + acquires_global_resources: vec![], + code: Some(CodeUnit { + locals: SignatureIndex(0), + code: vec![Ret], + jump_tables: vec![], + }), + }, + FunctionDefinition { + function: FunctionHandleIndex(1), + visibility: Visibility::Public, + is_entry: false, + acquires_global_resources: vec![], + code: Some(CodeUnit { + locals: SignatureIndex(1), + code: vec![Ret], + jump_tables: vec![], + }), + }, + ], + enum_defs: vec![], + enum_def_instantiations: vec![], + variant_handles: vec![], + variant_instantiation_handles: vec![], + }; + assert!(verify_module_unmetered(&compiled_module_good).is_ok()); +} + +#[test] +fn big_signature_test() { + const N_TYPE_PARAMS: usize = 5; + const INSTANTIATION_DEPTH: usize = 3; + const VECTOR_DEPTH: usize = 250; + let mut st = SignatureToken::U8; + for _ in 0..VECTOR_DEPTH { + st = SignatureToken::Vector(Box::new(st)); + } + for _ in 0..INSTANTIATION_DEPTH { + let type_params = vec![st; N_TYPE_PARAMS]; + st = SignatureToken::DatatypeInstantiation(Box::new((DatatypeHandleIndex(0), type_params))); + } + + const N_READPOP: u16 = 7500; + + let mut code = vec![]; + // 1. ImmBorrowLoc: ... ref + // 2. ReadRef: ... value + // 3. Pop: ... + for _ in 0..N_READPOP { + code.push(Bytecode::ImmBorrowLoc(0)); + code.push(Bytecode::ReadRef); + code.push(Bytecode::Pop); + } + code.push(Bytecode::Ret); + + let type_param_constraints = DatatypeTyParameter { + constraints: AbilitySet::EMPTY, + is_phantom: false, + }; + + let module = CompiledModule { + version: 5, + publishable: true, + self_module_handle_idx: ModuleHandleIndex(0), + module_handles: vec![ModuleHandle { + address: AddressIdentifierIndex(0), + name: IdentifierIndex(0), + }], + datatype_handles: vec![DatatypeHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(1), + abilities: AbilitySet::ALL, + type_parameters: vec![type_param_constraints; N_TYPE_PARAMS], + }], + function_handles: vec![FunctionHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(0), + parameters: SignatureIndex(1), + return_: SignatureIndex(0), + type_parameters: vec![], + }], + field_handles: vec![], + friend_decls: vec![], + struct_def_instantiations: vec![], + function_instantiations: vec![], + field_instantiations: vec![], + signatures: vec![Signature(vec![]), Signature(vec![st])], + identifiers: vec![ + Identifier::new("f").unwrap(), + Identifier::new("generic_struct").unwrap(), + ], + address_identifiers: vec![AccountAddress::ONE], + constant_pool: vec![], + metadata: vec![], + struct_defs: vec![StructDefinition { + struct_handle: DatatypeHandleIndex(0), + field_information: StructFieldInformation::Native, + }], + function_defs: vec![FunctionDefinition { + function: FunctionHandleIndex(0), + visibility: Public, + is_entry: true, + acquires_global_resources: vec![], + code: Some(CodeUnit { + locals: SignatureIndex(0), + code, + jump_tables: vec![], + }), + }], + enum_def_instantiations: vec![], + enum_defs: vec![], + variant_handles: vec![], + variant_instantiation_handles: vec![], + }; + + // save module and verify that it can ser/de + let mut mvbytes = vec![]; + module.serialize(&mut mvbytes).unwrap(); + let module = CompiledModule::deserialize_with_defaults(&mvbytes).unwrap(); + + let res = verify_module_with_config_for_test( + "big_signature_test", + &production_config().0, + &module, + &mut DummyMeter, + ) + .unwrap_err(); + assert_eq!(res.major_status(), StatusCode::TOO_MANY_TYPE_NODES); +} diff --git a/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/vec_pack_tests.rs b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/vec_pack_tests.rs new file mode 100644 index 0000000000000..9fb0fa94555d5 --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/src/unit_tests/vec_pack_tests.rs @@ -0,0 +1,70 @@ +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +use crate::unit_tests::production_config; +use move_binary_format::file_format::{ + Bytecode, CodeUnit, FunctionDefinition, FunctionHandle, FunctionHandleIndex, IdentifierIndex, + ModuleHandleIndex, Signature, SignatureIndex, SignatureToken, Visibility, empty_module, +}; +use move_bytecode_verifier_meter::dummy::DummyMeter; +use move_core_types::{identifier::Identifier, vm_status::StatusCode}; + +fn vec_sig(len: usize) -> SignatureToken { + if len > 0 { + SignatureToken::Vector(Box::new(vec_sig(len - 1))) + } else { + SignatureToken::U8 + } +} + +#[test] +fn test_vec_pack() { + let mut m = empty_module(); + + let sig = SignatureIndex(m.signatures.len() as u16); + m.signatures.push(Signature(vec![vec_sig(255)])); + + m.function_defs.push(FunctionDefinition { + function: FunctionHandleIndex(0), + visibility: Visibility::Private, + is_entry: false, + acquires_global_resources: vec![], + code: Some(CodeUnit { + locals: SignatureIndex(0), + code: vec![], + jump_tables: vec![], + }), + }); + + m.function_handles.push(FunctionHandle { + module: ModuleHandleIndex(0), + name: IdentifierIndex(m.identifiers.len() as u16), + parameters: SignatureIndex(0), + return_: SignatureIndex(0), + type_parameters: vec![], + }); + m.identifiers + .push(Identifier::new("foo".to_string()).unwrap()); + + const COUNT: usize = 3000; + + m.function_defs[0].code.as_mut().unwrap().code = + std::iter::once(&[Bytecode::VecPack(sig, 0)][..]) + .chain(std::iter::repeat_n( + &[Bytecode::VecUnpack(sig, 1024), Bytecode::VecPack(sig, 1024)][..], + COUNT, + )) + .chain(std::iter::once(&[Bytecode::Pop, Bytecode::Ret][..])) + .flatten() + .cloned() + .collect(); + + let res = move_bytecode_verifier::verify_module_with_config_for_test( + "test_vec_pack", + &production_config().0, + &m, + &mut DummyMeter, + ) + .unwrap_err(); + assert_eq!(res.major_status(), StatusCode::VALUE_STACK_PUSH_OVERFLOW); +} diff --git a/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/tests/binaries/router.bytes b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/tests/binaries/router.bytes new file mode 100644 index 0000000000000..95d8a3c103bc3 --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/bytecode-verifier-tests/tests/binaries/router.bytes @@ -0,0 +1 @@ +a11ceb0b050000000901000e020e0603148f0104a3012805cb018f0207da03f90208d30640069307a0010cb308db110000010101020003000400050006010a0401000100070000000008010202000000090102020000020b050600050c060700050d050200030e0207020000040f02070200000410020a0200000111020c01000612000d000113020f01000414020a0200000115100f0100031611120200000117130201000418020d0200000519140d00041a1500020000041b020a020000041c020d020000051d140d00031e111202000006080609070907080808090b0b0e0c080d0b0e080f0b100812080f0e0809090e130914091209160802040403060c030300030404043501010101010103030404040404040404040b000109000b000109000b000109000b000109000b000109000b000109000b000109010b000109010b0001090103030303030303030404030301010404010404040503030303030301060c0105010102090009010209010900010301090001020104010901010b0001090002060c0308060c070b00010900070b00010901030301030302030302050b000109000305040106060c010303030432010103030404040404040404040b000109000b000109000b000109000b000109000b000109000b000109000b000109010b000109010b000109010303030303030404030301010404010404040505030303030303030306726f7574657204636f696e067369676e657203616d6d0b636c6f625f6d61726b657403666565047574696c0873696d706c69667918737761705f636f696e5f666f725f65786163745f636f696e18737761705f65786163745f636f696e5f666f725f636f696e04436f696e0a616464726573735f6f660a6665655f65786973747316696e697469616c697a655f6665655f64656661756c740b706f6f6c5f6578697374730d6d61726b65745f657869737473086c6f745f73697a6508646563696d616c7303657870047a65726f0c6e5f6269645f6c6576656c730877697468647261771d636f696e5f737761705f636f696e5f666f725f65786163745f636f696e076465706f7369740b626573745f6269645f61750c73756274726163745f66656516706c6163655f6f726465725f666f725f726f757465720c6e5f61736b5f6c6576656c730b626573745f61736b5f6175076164645f6665651d636f696e5f737761705f65786163745f636f696e5f666f725f636f696e49661cd59c0b89440313af587bc99c3d38614d9a52479eb3f7c7c766d580c30b00000000000000000000000000000000000000000000000000000000000000010308c9000000000000000308ca000000000000000308c800000000000000030804000000000000000308070000000000000003086500000000000000030866000000000000000308030000000000000003080200000000000000030864000000000000000308ffffffffffffffff03080500000000000000030801000000000000000308680000000000000003086700000000000000030806000000000000000001000003190a000c030a010c040a01320000000000000000000000000000000022030905120b000a01190c020b010c000b020c0105040b030a001a0b040b001a020101040004ec030a0011030c310a311104200308050a0a0011053800030d0510080c03051238010c030b030c2d38020c2a38030c290a2d0b291f031d05fb0138040c27320a000000000000000000000000000000380535110a0c0e0600000000000000000c320600000000000000000c3438060c1a0a340a0223032f05340a320a01230c060536090c060b06033905eb01380706000000000000000021033e055f0a000a010a321738080c140b000d140d1a0a010a32170a020a34170906000000000000000006000000000000000038090c210c1d0a310b14380a0b320b21160c320b340b1d160c3405eb01380b0c130a310a130811110c120a020a3417350a0e180a121a340c090b090a272303750596010a000a010a321738080c180b000d180d1a0a010a32170a020a34170906000000000000000006000000000000000038090c230c1f0a310b18380a0b320b23160c320b340b1f160c3405eb010a0e0a1211000c250c2b0a000a010a321738080c190a000d190d1a0a010a32170a020a3417080b2b340b253438090c240c200a310b19380a0b320b24160c320b340b20160c340a340a022303c20105c7010a320a01230c0805c901090c080b0803cc0105ea010a020a3417350a0e180b121a340c0a0a000907060b13340b0a3200000000000000000000000000000000380c0c2e0c0d0b320b0d34160c320b340b2e34160c34052a0b320b012503f1010707270b340b022103f7010708270b310b1a380d05eb030a2d0a2a1f03800205b203380e0c28320a000000000000000000000000000000380f35110a0c0f0600000000000000000c330600000000000000000c3538060c1b0a350a02230392020597020a330a01230c04059902090c040b04039c0205a20338100600000000000000002103a10205a402080c0505aa020a020a35170a28230c050b0503ad0205ce020a000a010a331738080c150b000d150d1b0a010a33170a020a35170906000000000000000006000000000000000038090c220c1e0a310b15380a0b330b22160c330b350b1e160c3505a20338110c110a310a110811150c100b100a0f11000c260c2c0a000a010a331738080c160a000d160d1b0a010a33170a020a3517080b2c340b263438090c360c370a310b16380a0b330b36160c330b350b37160c350a350a02230381030586030a330a01230c07058803090c070b07038b0305a1030a000807060b11340a020a3517320000000000000000000000000000000038120c2f0c0b0b330b2f34160c330b350b0b34160c35058d020b330b012503a8030707270b350b022103ae030708270b310b1b380d05eb030b2d03b50305cd030a000a0138080c1738060c1c0b000d170d1c0b010b0209060000000000000000060000000000000000380901010a310b17380a0b310b1c380d05eb030b2a03d00305e7030b00080705070a0a02320000000000000000000000000000000038120c300c0c0b0c340b022103e0030707270b30340b012503eb030708270b0001070c27020201040016d1030a0011030c2b0a2b1104200308050a0a0011053800030d0510080c03051238010c030b030c2738020c2438030c230a270a231f031d05b70138040c21320a000000000000000000000000000000380535110a0c0a0600000000000000000c2d0600000000000000000c2f38060c160a2d0a0123032f05a70138070600000000000000002103340537080c04053d0a010a2d170a21230c040b040340055f0a000a010a2d1738080c100b000d100d160a010a2d170600000000000000000906000000000000000006000000000000000038130c1c0c190a2b0b10380a0b2d0b1c160c2d0b2f0b19160c2f05a701380b0c0f0a2b0a0f0811110c0e0b0e0a0a11000c1f0c250a000a010a2d1738080c140a000d140d160a010a2d17060000000000000000080b25340b1f3438130c310c330a2b0b14380a0b2d0b31160c2d0b2f0b33160c2f0a2d0a012303900105a6010a000907060b0f340a010a2d173200000000000000000000000000000000380c0c280c080b2d0b0834160c2d0b2f0b2834160c2f052a0b2d0b012103ad010707270b2f0b022603b3010708270b2b0b16380d05d0030a270b241f03bc01059403380e0c22320a000000000000000000000000000000380f35110a0c0b0600000000000000000c2e0600000000000000000c3038060c180a2e0a012303ce0105840338100600000000000000002103d30105f2010a000a010a2e1738080c150b000d150d180a010a2e170600000000000000000906000000000000000006000000000000000038130c1d0c1a0a2b0b15380a0b2e0b1d160c2e0b300b1a160c3005840338110c0d0a2b0a0d0811150c0c0a010a2e17350a0b180a0c1a340c050b050a222303880205a7020a000a010a2e1738080c110b000d110d180a010a2e170600000000000000000906000000000000000006000000000000000038130c1e0c1b0a2b0b11380a0b2e0b1e160c2e0b300b1b160c300584030a0b0a0c11000c200c260a000a010a2e1738080c120a000d120d180a010a2e17060000000000000000080b26340b203438130c320c340a2b0b12380a0b2e0b32160c2e0b300b34160c300a2e0a012303d1020583030a010a2e17350a0b180b0c1a340c060a000807060b0d340a06320000000000000000000000000000000038120c2a0c070a07340b062503ee020b0001061111000000000000270a2a340a010a2e172503f9020b0001062222000000000000270b2e0b2a34160c2e0b300b0734160c3005c9010b2e0b0121038a030707270b300b02260390030708270b2b0b18380d05d0030b2703970305b2030a000a0138080c1338060c170a000d130d170b010b0209060000000000000000060000000000000000381301010b0011030c2c0a2c0b17380d0b2c0b13380a05d0030b2303b50305cc030b000907050600000000000000000a013200000000000000000000000000000000380c0c290c090b09340b012103c5030707270b29340b022603d0030708270b0001070c270200 \ No newline at end of file diff --git a/external-crates/move/move-execution/replay_cut/crates/move-abstract-interpreter/Cargo.toml b/external-crates/move/move-execution/replay_cut/crates/move-abstract-interpreter/Cargo.toml new file mode 100644 index 0000000000000..53f5feb067260 --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/move-abstract-interpreter/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "move-abstract-interpreter-replay_cut" +version = "0.1.0" +authors = ["The Move Contributors"] +description = "Move abstract interpreter" +license = "Apache-2.0" +publish = false +edition = "2024" + +[dev-dependencies] +itertools.workspace = true + +[features] +default = [] diff --git a/external-crates/move/move-execution/replay_cut/crates/move-abstract-interpreter/src/absint.rs b/external-crates/move/move-execution/replay_cut/crates/move-abstract-interpreter/src/absint.rs new file mode 100644 index 0000000000000..773112d032e1c --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/move-abstract-interpreter/src/absint.rs @@ -0,0 +1,213 @@ +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +use crate::control_flow_graph::ControlFlowGraph; +use std::collections::BTreeMap; + +#[derive(Debug, Clone)] +pub enum JoinResult { + Changed, + Unchanged, +} + +#[derive(Debug, Clone)] +pub enum BlockPostCondition { + /// Unprocessed block + Unprocessed, + /// Block has been processed + Processed(State), +} + +#[derive(Debug, Clone)] +pub struct BlockInvariant { + /// Precondition of the block + pub pre: State, + /// Postcondition of the block + pub post: BlockPostCondition, +} + +pub type InvariantMap = BTreeMap< + ::BlockId, + BlockInvariant<::State>, +>; + +pub trait AbstractInterpreter { + type Error; + type BlockId: Copy + Ord; + + type State: Clone; + type InstructionIndex: Copy + Ord; + type Instruction; + + /// Joining two states together, this along with `execute` drives the analysis. + fn join( + &mut self, + pre: &mut Self::State, + post: &Self::State, + ) -> Result; + + /// Execute local@instr found at index local@index in the current basic block from pre-state + /// local@pre. + /// Should return an Err if executing the instruction is unsuccessful, and () if + /// the effects of successfully executing local@instr have been reflected by mutating + /// local@pre. + /// Auxiliary data from the analysis that is not part of the abstract state can be collected by + /// mutating local@self. + /// The last instruction index in the current block is local@last_index. Knowing this + /// information allows clients to detect the end of a basic block and special-case appropriately + /// (e.g., normalizing the abstract state before a join). + fn execute( + &mut self, + block_id: Self::BlockId, + bounds: (Self::InstructionIndex, Self::InstructionIndex), + state: &mut Self::State, + offset: Self::InstructionIndex, + instr: &Self::Instruction, + ) -> Result<(), Self::Error>; + + /// A visitor for starting the analysis. This is called before any blocks are processed. + fn start(&mut self) -> Result<(), Self::Error> { + Ok(()) + } + + /// A visitor intended for bookkeeping. They should _not_ be used to modify the state in any + /// way related to the analysis. + fn visit_block_pre_execution( + &mut self, + _block_id: Self::BlockId, + _invariant: &mut BlockInvariant, + ) -> Result<(), Self::Error> { + Ok(()) + } + + /// A visitor intended for bookkeeping. They should _not_ be used to modify the state in any + /// way related to the analysis. + fn visit_block_post_execution( + &mut self, + _block_id: Self::BlockId, + _invariant: &mut BlockInvariant, + ) -> Result<(), Self::Error> { + Ok(()) + } + + /// A visitor intended for bookkeeping. They should _not_ be used to modify the state in any + /// way related to the analysis. + fn visit_successor(&mut self, _block_id: Self::BlockId) -> Result<(), Self::Error> { + Ok(()) + } + + /// A visitor intended for bookkeeping. They should _not_ be used to modify the state in any + /// way related to the analysis. + fn visit_back_edge( + &mut self, + _from: Self::BlockId, + _to: Self::BlockId, + ) -> Result<(), Self::Error> { + Ok(()) + } +} + +/// Analyze procedure local@function_context starting from pre-state local@initial_state. +pub fn analyze_function( + interpreter: &mut A, + cfg: &CFG, + code: &::Instructions, + initial_state: A::State, +) -> Result, A::Error> +where + A: AbstractInterpreter, + CFG: ControlFlowGraph< + BlockId = A::BlockId, + InstructionIndex = A::InstructionIndex, + Instruction = A::Instruction, + >, +{ + interpreter.start()?; + let mut inv_map = BTreeMap::new(); + let entry_block_id = cfg.entry_block_id(); + let mut next_block = Some(entry_block_id); + inv_map.insert( + entry_block_id, + BlockInvariant { + pre: initial_state.clone(), + post: BlockPostCondition::Unprocessed, + }, + ); + + while let Some(block_id) = next_block { + let block_invariant = inv_map.entry(block_id).or_insert_with(|| BlockInvariant { + pre: initial_state.clone(), + post: BlockPostCondition::Unprocessed, + }); + + interpreter.visit_block_pre_execution(block_id, block_invariant)?; + let pre_state = &block_invariant.pre; + let post_state = execute_block(interpreter, cfg, code, block_id, pre_state)?; + block_invariant.post = BlockPostCondition::Processed(post_state.clone()); + interpreter.visit_block_post_execution(block_id, block_invariant)?; + + let mut next_block_candidate = cfg.next_block(block_id); + // propagate postcondition of this block to successor blocks + for successor_block_id in cfg.successors(block_id) { + interpreter.visit_successor(successor_block_id)?; + match inv_map.get_mut(&successor_block_id) { + Some(next_block_invariant) => { + let join_result = + interpreter.join(&mut next_block_invariant.pre, &post_state)?; + match join_result { + JoinResult::Unchanged => { + // Pre is the same after join. Reanalyzing this block would produce + // the same post + } + JoinResult::Changed => { + next_block_invariant.post = BlockPostCondition::Unprocessed; + // If the cur->successor is a back edge, jump back to the beginning + // of the loop, instead of the normal next block + if cfg.is_back_edge(block_id, successor_block_id) { + interpreter.visit_back_edge(block_id, successor_block_id)?; + next_block_candidate = Some(successor_block_id); + break; + } + } + } + } + None => { + // Haven't visited the next block yet. Use the post of the current block as + // its pre + inv_map.insert( + successor_block_id, + BlockInvariant { + pre: post_state.clone(), + post: BlockPostCondition::Unprocessed, + }, + ); + } + } + } + next_block = next_block_candidate; + } + Ok(inv_map) +} + +fn execute_block( + interpreter: &mut A, + cfg: &CFG, + code: &::Instructions, + block_id: A::BlockId, + pre_state: &A::State, +) -> Result +where + A: AbstractInterpreter, + CFG: ControlFlowGraph< + BlockId = A::BlockId, + InstructionIndex = A::InstructionIndex, + Instruction = A::Instruction, + >, +{ + let mut state_acc = pre_state.clone(); + let bounds = (cfg.block_start(block_id), cfg.block_end(block_id)); + for (offset, instr) in cfg.instructions(code, block_id) { + interpreter.execute(block_id, bounds, &mut state_acc, offset, instr)? + } + Ok(state_acc) +} diff --git a/external-crates/move/move-execution/replay_cut/crates/move-abstract-interpreter/src/control_flow_graph.rs b/external-crates/move/move-execution/replay_cut/crates/move-abstract-interpreter/src/control_flow_graph.rs new file mode 100644 index 0000000000000..a408c4366e743 --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/move-abstract-interpreter/src/control_flow_graph.rs @@ -0,0 +1,383 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +//! This module defines the control-flow graph uses for bytecode verification. +use std::collections::{BTreeMap, BTreeSet, btree_map::Entry}; + +/// A trait that specifies the basic requirements for a CFG +pub trait ControlFlowGraph { + type BlockId: Copy + Ord; + type InstructionIndex: Copy + Ord; + type Instruction; + type Instructions: ?Sized; + + /// Start index of the block ID in the bytecode vector + fn block_start(&self, block_id: Self::BlockId) -> Self::InstructionIndex; + + /// End index of the block ID in the bytecode vector + fn block_end(&self, block_id: Self::BlockId) -> Self::InstructionIndex; + + /// Successors of the block ID in the bytecode vector + fn successors(&self, block_id: Self::BlockId) -> impl Iterator; + + /// Return the next block in traversal order + fn next_block(&self, block_id: Self::BlockId) -> Option; + + /// Iterator over the indexes of instructions in this block + fn instructions<'a>( + &'a self, + function_code: &'a Self::Instructions, + block_id: Self::BlockId, + ) -> impl Iterator + where + Self::Instruction: 'a; + + /// Return an iterator over the blocks of the CFG + fn blocks(&self) -> impl Iterator; + + /// Return the number of blocks (vertices) in the control flow graph + fn num_blocks(&self) -> usize; + + /// Return the id of the entry block for this control-flow graph + /// Note: even a CFG with no instructions has an (empty) entry block. + fn entry_block_id(&self) -> Self::BlockId; + + /// Checks if the block ID is a loop head + fn is_loop_head(&self, block_id: Self::BlockId) -> bool; + + /// Checks if the edge from cur->next is a back edge + /// returns false if the edge is not in the cfg + fn is_back_edge(&self, cur: Self::BlockId, next: Self::BlockId) -> bool; +} + +/// Used for the VM control flow graph +pub trait Instruction: Sized { + type Index: Copy + Ord; + type VariantJumpTables: ?Sized; + const ENTRY_BLOCK_ID: Self::Index; + + /// Return the successors of a given instruction + fn get_successors( + pc: Self::Index, + code: &[Self], + jump_tables: &Self::VariantJumpTables, + ) -> Vec; + + /// Return the offsets of jump targets for a given instruction + fn offsets(&self, jump_tables: &Self::VariantJumpTables) -> Vec; + + fn usize_as_index(i: usize) -> Self::Index; + fn index_as_usize(i: Self::Index) -> usize; + + fn is_branch(&self) -> bool; +} + +#[derive(Debug, Clone)] +struct BasicBlock { + exit: InstructionIndex, + successors: Vec, +} + +/// The control flow graph that we build from the bytecode. +/// Assumes a list of bytecode isntructions that satisfy the invariants specified in the VM's +/// file format. +#[derive(Debug, Clone)] +pub struct VMControlFlowGraph { + /// The basic blocks + blocks: BTreeMap>, + /// Basic block ordering for traversal + traversal_successors: BTreeMap, + /// Map of loop heads with all of their back edges + loop_heads: BTreeMap>, +} + +impl BasicBlock { + pub fn display(&self, entry: InstructionIndex) { + println!("+=======================+"); + println!("| Enter: {} |", entry); + println!("+-----------------------+"); + println!("==> Children: {:?}", self.successors); + println!("+-----------------------+"); + println!("| Exit: {} |", self.exit); + println!("+=======================+"); + } +} + +impl VMControlFlowGraph { + pub fn new(code: &[I], jump_tables: &I::VariantJumpTables) -> Self { + use std::collections::{BTreeMap as Map, BTreeSet as Set}; + + let code_len = code.len(); + // First go through and collect block ids, i.e., offsets that begin basic blocks. + // Need to do this first in order to handle backwards edges. + let mut block_ids = BTreeSet::new(); + block_ids.insert(I::ENTRY_BLOCK_ID); + for pc in 0..code.len() { + VMControlFlowGraph::record_block_ids(pc, code, jump_tables, &mut block_ids); + } + + // Create basic blocks + let mut blocks: BTreeMap> = Map::new(); + let mut entry = 0; + let mut exit_to_entry = Map::new(); + for pc in 0..code.len() { + let co_pc = I::usize_as_index(pc); + + // Create a basic block + if Self::is_end_of_block(pc, code, &block_ids) { + let exit = co_pc; + exit_to_entry.insert(exit, entry); + let successors = I::get_successors(co_pc, code, jump_tables); + let bb = BasicBlock { exit, successors }; + blocks.insert(I::usize_as_index(entry), bb); + entry = pc + 1; + } + } + let blocks = blocks; + assert_eq!(entry, code_len); + + // # Loop analysis + // + // This section identifies loops in the control-flow graph, picks a back edge and loop head + // (the basic block the back edge returns to), and decides the order that blocks are + // traversed during abstract interpretation (reverse post-order). + // + // The implementation is based on the algorithm for finding widening points in Section 4.1, + // "Depth-first numbering" of Bourdoncle [1993], "Efficient chaotic iteration strategies + // with widenings." + // + // NB. The comments below refer to a block's sub-graph -- the reflexive transitive closure + // of its successor edges, modulo cycles. + + #[derive(Copy, Clone)] + enum Exploration { + InProgress, + Done, + } + + let mut exploration: Map = Map::new(); + let mut stack = vec![I::ENTRY_BLOCK_ID]; + + // For every loop in the CFG that is reachable from the entry block, there is an entry in + // `loop_heads` mapping to all the back edges pointing to it, and vice versa. + // + // Entry in `loop_heads` implies loop in the CFG is justified by the comments in the loop + // below. Loop in the CFG implies entry in `loop_heads` is justified by considering the + // point at which the first node in that loop, `F` is added to the `exploration` map: + // + // - By definition `F` is part of a loop, meaning there is a block `L` such that: + // + // F - ... -> L -> F + // + // - `F` will not transition to `Done` until all the nodes reachable from it (including `L`) + // have been visited. + // - Because `F` is the first node seen in the loop, all the other nodes in the loop + // (including `L`) will be visited while `F` is `InProgress`. + // - Therefore, we will process the `L -> F` edge while `F` is `InProgress`. + // - Therefore, we will record a back edge to it. + let mut loop_heads: Map> = Map::new(); + + // Blocks appear in `post_order` after all the blocks in their (non-reflexive) sub-graph. + let mut post_order = Vec::with_capacity(blocks.len()); + + while let Some(block) = stack.pop() { + match exploration.entry(block) { + Entry::Vacant(entry) => { + // Record the fact that exploration of this block and its sub-graph has started. + entry.insert(Exploration::InProgress); + + // Push the block back on the stack to finish processing it, and mark it as done + // once its sub-graph has been traversed. + stack.push(block); + + for succ in &blocks[&block].successors { + match exploration.get(succ) { + // This successor has never been visited before, add it to the stack to + // be explored before `block` gets marked `Done`. + None => stack.push(*succ), + + // This block's sub-graph was being explored, meaning it is a (reflexive + // transitive) predecessor of `block` as well as being a successor, + // implying a loop has been detected -- greedily choose the successor + // block as the loop head. + Some(Exploration::InProgress) => { + loop_heads.entry(*succ).or_default().insert(block); + } + + // Cross-edge detected, this block and its entire sub-graph (modulo + // cycles) has already been explored via a different path, and is + // already present in `post_order`. + Some(Exploration::Done) => { /* skip */ } + }; + } + } + + Entry::Occupied(mut entry) => match entry.get() { + // Already traversed the sub-graph reachable from this block, so skip it. + Exploration::Done => continue, + + // Finish up the traversal by adding this block to the post-order traversal + // after its sub-graph (modulo cycles). + Exploration::InProgress => { + post_order.push(block); + entry.insert(Exploration::Done); + } + }, + } + } + + let traversal_order = { + // This reverse post order is akin to a topological sort (ignoring cycles) and is + // different from a pre-order in the presence of diamond patterns in the graph. + post_order.reverse(); + post_order + }; + + // build a mapping from a block id to the next block id in the traversal order + let traversal_successors = traversal_order + .windows(2) + .map(|window| { + debug_assert!(window.len() == 2); + (window[0], window[1]) + }) + .collect(); + + VMControlFlowGraph { + blocks, + traversal_successors, + loop_heads, + } + } + + pub fn display(&self) + where + I::Index: std::fmt::Debug + std::fmt::Display, + { + for (entry, block) in &self.blocks { + block.display(*entry); + } + println!("Traversal: {:#?}", self.traversal_successors); + } + + fn is_end_of_block(pc: usize, code: &[I], block_ids: &BTreeSet) -> bool { + pc + 1 == code.len() || block_ids.contains(&I::usize_as_index(pc + 1)) + } + + fn record_block_ids( + pc: usize, + code: &[I], + jump_tables: &I::VariantJumpTables, + block_ids: &mut BTreeSet, + ) { + let bytecode = &code[pc]; + + block_ids.extend(bytecode.offsets(jump_tables)); + + if bytecode.is_branch() && pc + 1 < code.len() { + block_ids.insert(I::usize_as_index(pc + 1)); + } + } + + /// A utility function that implements BFS-reachability from block_id with + /// respect to get_targets function + fn traverse_by(&self, block_id: I::Index) -> Vec { + let mut ret = Vec::new(); + // We use this index to keep track of our frontier. + let mut index = 0; + // Guard against cycles + let mut seen = BTreeSet::new(); + + ret.push(block_id); + seen.insert(block_id); + + while index < ret.len() { + let block_id = ret[index]; + index += 1; + for block_id in self.successors(block_id) { + if !seen.contains(&block_id) { + ret.push(block_id); + seen.insert(block_id); + } + } + } + + ret + } + + pub fn reachable_from(&self, block_id: I::Index) -> Vec { + self.traverse_by(block_id) + } + + pub fn num_back_edges(&self) -> usize { + self.loop_heads + .iter() + .fold(0, |acc, (_, edges)| acc + edges.len()) + } +} + +impl ControlFlowGraph for VMControlFlowGraph { + type BlockId = I::Index; + type InstructionIndex = I::Index; + type Instruction = I; + type Instructions = [I]; + + // Note: in the following procedures, it's safe not to check bounds because: + // - Every CFG (even one with no instructions) has a block at ENTRY_BLOCK_ID + // - The only way to acquire new BlockId's is via block_successors() + // - block_successors only() returns valid BlockId's + // Note: it is still possible to get a BlockId from one CFG and use it in another CFG where it + // is not valid. The design does not attempt to prevent this abuse of the API. + + fn block_start(&self, block_id: Self::BlockId) -> I::Index { + block_id + } + + fn block_end(&self, block_id: Self::BlockId) -> I::Index { + self.blocks[&block_id].exit + } + + fn successors(&self, block_id: Self::BlockId) -> impl Iterator { + self.blocks[&block_id].successors.iter().copied() + } + + fn next_block(&self, block_id: Self::BlockId) -> Option { + debug_assert!(self.blocks.contains_key(&block_id)); + self.traversal_successors.get(&block_id).copied() + } + + fn instructions<'a>( + &self, + function_code: &'a [I], + block_id: Self::BlockId, + ) -> impl Iterator + where + I: 'a, + { + let start = I::index_as_usize(self.block_start(block_id)); + let end = I::index_as_usize(self.block_end(block_id)); + (start..=end).map(|pc| (I::usize_as_index(pc), &function_code[pc])) + } + + fn blocks(&self) -> impl Iterator { + self.blocks.keys().copied() + } + + fn num_blocks(&self) -> usize { + self.blocks.len() + } + + fn entry_block_id(&self) -> Self::BlockId { + I::ENTRY_BLOCK_ID + } + + fn is_loop_head(&self, block_id: Self::BlockId) -> bool { + self.loop_heads.contains_key(&block_id) + } + + fn is_back_edge(&self, cur: Self::BlockId, next: Self::BlockId) -> bool { + self.loop_heads + .get(&next) + .is_some_and(|back_edges| back_edges.contains(&cur)) + } +} diff --git a/external-crates/move/move-execution/replay_cut/crates/move-abstract-interpreter/src/lib.rs b/external-crates/move/move-execution/replay_cut/crates/move-abstract-interpreter/src/lib.rs new file mode 100644 index 0000000000000..af81395aac83e --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/move-abstract-interpreter/src/lib.rs @@ -0,0 +1,5 @@ +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +pub mod absint; +pub mod control_flow_graph; diff --git a/external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier/Cargo.toml b/external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier/Cargo.toml new file mode 100644 index 0000000000000..b55e4b4283ef9 --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "move-bytecode-verifier-replay_cut" +version = "0.1.0" +authors = ["Diem Association "] +description = "Move bytecode verifier" +repository = "https://github.com/diem/diem" +homepage = "https://diem.com" +license = "Apache-2.0" +publish = false +edition = "2024" + +[dependencies] +petgraph.workspace = true + +move-borrow-graph.workspace = true +move-binary-format.workspace = true +move-bytecode-verifier-meter.workspace = true +move-core-types.workspace = true +move-vm-config.workspace = true +move-abstract-stack.workspace = true +move-abstract-interpreter.workspace = true +move-regex-borrow-graph.workspace = true + +[dev-dependencies] +hex-literal.workspace = true + +[features] +default = [] diff --git a/external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier/README.md b/external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier/README.md new file mode 100644 index 0000000000000..f6e3fdeaeaf13 --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier/README.md @@ -0,0 +1,82 @@ +--- +id: bytecode-verifier +title: Bytecode Verifier +custom_edit_url: https://github.com/move-language/move/edit/main/language/move-bytecode-verifier/README.md +--- + + +## Overview + +The bytecode verifier contains a static analysis tool for rejecting invalid Move bytecode. It checks the safety of stack usage, types, resources, and references. + +The body of each function in a compiled module is verified separately while trusting the correctness of function signatures in the module. Checking that each function signature matches its definition is a separate responsibility. The body of a function is a sequence of bytecode instructions. This instruction sequence is checked in several phases described below. + +## CFG Construction + +A control-flow graph is constructed by decomposing the instruction sequence into a collection of basic blocks. Each basic block contains a contiguous sequence of instructions; the set of all instructions is partitioned among the blocks. Each block ends with a branch or return instruction. The decomposition into blocks guarantees that branch targets land only at the beginning of some block. The decomposition also attempts to ensure that the generated blocks are maximal. However, the soundness of the analysis does not depend on maximality. + +## Stack Safety + +The execution of a block happens in the context of a stack and an array of local variables. The parameters of the function are a prefix of the array of local variables. Arguments and return values are passed across function calls via the stack. When a function starts executing, its arguments are already loaded into its parameters. Suppose the stack height is *n* when a function starts executing; then valid bytecode must enforce the invariant that when execution lands at the beginning of a basic block, the stack height is *n*. Furthermore, at a return instruction, the stack height must be *n*+*k* where *k*, s.t. *k*>=0 is the number of return values. The first phase of the analysis checks that this invariant is maintained by analyzing each block separately, calculating the effect of each instruction in the block on the stack height, checking that the height does not go below *n*, and that is left either at *n* or *n*+*k* (depending on the final instruction of the block and the return type of the function) at the end of the block. + +## Type Safety + +The second phase of the analysis checks that each operation, primitive or defined function, is invoked with arguments of appropriate types. The operands of an operation are values located either in a local variable or on the stack. The types of local variables of a function are already provided in the bytecode. However, the types of stack values are inferred. This inference and the type checking of each operation can be done separately for each block. Since the stack height at the beginning of each block is *n* and does not go below *n* during the execution of the block, we only need to model the suffix of the stack starting at *n* for type checking the block instructions. We model this suffix using a stack of types on which types are pushed and popped as the instruction stream in a block is processed. Only the type stack and the statically-known types of local variables are needed to type check each instruction. + +## Resource Safety + +Resources represent the assets of the blockchain. As such, there are certain restrictions on these types that do not apply to normal values. Intuitively, resource values cannot be copied and must be used by the end of the transaction (this means that they are moved to global storage or destroyed). Concretely, the following restrictions apply: + +* `CopyLoc` and `StLoc` require that the type of local is not of resource kind. +* `WriteRef`, `Eq`, and `Neq` require that the type of the reference is not of resource kind. +* At the end of a function (when `Ret` is reached), no local whose type is of resource kind must be empty, i.e., the value must have been moved out of the local. + +As mentioned above, this last rule around `Ret` implies that the resource *must* have been either: + +* Moved to global storage via `MoveTo`. +* Destroyed via `Unpack`. + +Both `MoveTo` and `Unpack` are internal to the module in which the resource is declared. + +## Reference Safety + +References are first-class in the bytecode language. Fresh references become available to a function in several ways: + +* Inputing parameters. +* Taking the address of the value in a local variable. +* Taking the address of the globally published value in an address. +* Taking the address of a field from a reference to the containing struct. +* Returning value from a function. + +The goal of reference safety checking is to ensure that there are no dangling references. Here are some examples of dangling references: + +* Local variable `y` contains a reference to the value in a local variable `x`; `x` is then moved. +* Local variable `y` contains a reference to the value in a local variable `x`; `x` is then bound to a new value. +* Reference is taken to a local variable that has not been initialized. +* Reference to a value in a local variable is returned from a function. +* Reference `r` is taken to a globally published value `v`; `v` is then unpublished. + +References can be either exclusive or shared; the latter allow read-only access. A secondary goal of reference safety checking is to ensure that in the execution context of the bytecode program, including the entire evaluation stack and all function frames, if there are two distinct storage locations containing references `r1` and `r2` such that `r2` extends `r1`, then both of the following conditions hold: + +* If `r1` is tagged as exclusive, then it must be inactive, i.e. it is impossible to reach a control location where `r1` is dereferenced or mutated. +* If `r1` is shared, then `r2` is shared. + +The two conditions above establish the property of referential transparency, important for scalable program verification, which looks roughly as follows: consider the piece of code `v1 = *r; S; v2 = *r`, where `S` is an arbitrary computation that does not perform any write through the syntactic reference `r` (and no writes to any `r'` that extends `r`). Then `v1 == v2`. + +### Analysis Setup + +The reference safety analysis is set up as a flow analysis (or abstract interpretation). An abstract state is defined for abstractly executing the code of a basic block. A map is maintained from basic blocks to abstract states. Given an abstract state *S* at the beginning of a basic block *B*, the abstract execution of *B* results in state *S'*. This state *S'* is propagated to all successors of *B* and recorded in the map. If a state already existed for a block, the freshly propagated state is “joined” with the existing state. If the join fails an error is reported. If the join succeeds but the abstract state remains unchanged, no further propagation is done. Otherwise, the state is updated and propagated again through the block. An error may also be reported when an instruction is processed during the propagation of abstract state through a block. + +**Errors.** As mentioned earlier, an error is reported by the checker in one of the following situations: + +* An instruction cannot be proven to be safe during the propagation of the abstract state through a block. +* Join of abstract states propagated via different incoming edges into a block fails. + +## How is this module organized? + +```text +* +├── invalid-mutations # Library used by proptests +├── src # Core bytecode verifier files +├── tests # Proptests +``` diff --git a/external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier/src/ability_cache.rs b/external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier/src/ability_cache.rs new file mode 100644 index 0000000000000..e9d34781b3d7a --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier/src/ability_cache.rs @@ -0,0 +1,101 @@ +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +use move_binary_format::{ + CompiledModule, + errors::PartialVMResult, + file_format::{AbilitySet, DatatypeHandleIndex, SignatureToken}, + safe_unwrap, +}; +use move_bytecode_verifier_meter::{Meter, Scope}; +use std::{ + cmp::max, + collections::{BTreeMap, btree_map::Entry}, +}; + +const TYPE_ARG_COST: u128 = 1; + +pub struct AbilityCache<'a> { + module: &'a CompiledModule, + vector_results: BTreeMap, + datatype_results: BTreeMap, AbilitySet>>, +} + +impl<'a> AbilityCache<'a> { + pub fn new(module: &'a CompiledModule) -> Self { + Self { + module, + vector_results: BTreeMap::new(), + datatype_results: BTreeMap::new(), + } + } + + pub fn abilities( + &mut self, + scope: Scope, + meter: &mut (impl Meter + ?Sized), + type_parameter_abilities: &[AbilitySet], + ty: &SignatureToken, + ) -> PartialVMResult { + use SignatureToken as S; + + Ok(match ty { + S::Bool | S::U8 | S::U16 | S::U32 | S::U64 | S::U128 | S::U256 | S::Address => { + AbilitySet::PRIMITIVES + } + + S::Reference(_) | S::MutableReference(_) => AbilitySet::REFERENCES, + S::Signer => AbilitySet::SIGNER, + S::TypeParameter(idx) => *safe_unwrap!(type_parameter_abilities.get(*idx as usize)), + S::Datatype(idx) => { + let sh = self.module.datatype_handle_at(*idx); + sh.abilities + } + S::Vector(inner) => { + let inner_abilities = + self.abilities(scope, meter, type_parameter_abilities, inner)?; + let entry = self.vector_results.entry(inner_abilities); + match entry { + Entry::Occupied(entry) => *entry.get(), + Entry::Vacant(entry) => { + meter.add(scope, TYPE_ARG_COST)?; + let abilities = AbilitySet::polymorphic_abilities( + AbilitySet::VECTOR, + vec![false], + vec![inner_abilities], + )?; + entry.insert(abilities); + abilities + } + } + } + S::DatatypeInstantiation(inst) => { + let (idx, type_args) = &**inst; + let type_arg_abilities = type_args + .iter() + .map(|arg| self.abilities(scope, meter, type_parameter_abilities, arg)) + .collect::>>()?; + let entry = self + .datatype_results + .entry(*idx) + .or_default() + .entry(type_arg_abilities.clone()); + match entry { + Entry::Occupied(entry) => *entry.get(), + Entry::Vacant(entry) => { + meter.add_items(scope, TYPE_ARG_COST, max(type_args.len(), 1))?; + let sh = self.module.datatype_handle_at(*idx); + let declared_abilities = sh.abilities; + let abilities = AbilitySet::polymorphic_abilities( + declared_abilities, + sh.type_parameters.iter().map(|param| param.is_phantom), + type_arg_abilities, + )?; + entry.insert(abilities); + abilities + } + } + } + }) + } +} diff --git a/external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier/src/ability_field_requirements.rs b/external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier/src/ability_field_requirements.rs new file mode 100644 index 0000000000000..d4e7374415526 --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier/src/ability_field_requirements.rs @@ -0,0 +1,100 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +//! This module implements a checker for verifying that all of the struct's fields satisfy the +//! abilities required by the struct's abilities +use crate::ability_cache::AbilityCache; +use move_binary_format::{ + IndexKind, + errors::{Location, PartialVMResult, VMResult, verification_error}, + file_format::{AbilitySet, CompiledModule, StructFieldInformation, TableIndex}, +}; +use move_bytecode_verifier_meter::{Meter, Scope}; +use move_core_types::vm_status::StatusCode; + +pub fn verify_module<'env>( + module: &'env CompiledModule, + ability_cache: &mut AbilityCache<'env>, + meter: &mut (impl Meter + ?Sized), +) -> VMResult<()> { + verify_module_impl(module, ability_cache, meter) + .map_err(|e| e.finish(Location::Module(module.self_id()))) +} + +fn verify_module_impl<'env>( + module: &'env CompiledModule, + ability_cache: &mut AbilityCache<'env>, + meter: &mut (impl Meter + ?Sized), +) -> PartialVMResult<()> { + for (idx, struct_def) in module.struct_defs().iter().enumerate() { + let sh = module.datatype_handle_at(struct_def.struct_handle); + let fields = match &struct_def.field_information { + StructFieldInformation::Native => continue, + StructFieldInformation::Declared(fields) => fields, + }; + let required_abilities = sh + .abilities + .into_iter() + .map(|a| a.requires()) + .fold(AbilitySet::EMPTY, |acc, required| acc | required); + // Assume type parameters have all abilities, as the struct's abilities will be dependent on + // them + let type_parameter_abilities = sh + .type_parameters + .iter() + .map(|_| AbilitySet::ALL) + .collect::>(); + for field in fields { + let field_abilities = ability_cache.abilities( + Scope::Module, + meter, + &type_parameter_abilities, + &field.signature.0, + )?; + if !required_abilities.is_subset(field_abilities) { + return Err(verification_error( + StatusCode::FIELD_MISSING_TYPE_ABILITY, + IndexKind::StructDefinition, + idx as TableIndex, + )); + } + } + } + + for (idx, enum_def) in module.enum_defs().iter().enumerate() { + let sh = module.datatype_handle_at(enum_def.enum_handle); + let required_abilities = sh + .abilities + .into_iter() + .map(|a| a.requires()) + .fold(AbilitySet::EMPTY, |acc, required| acc | required); + // Assume type parameters have all abilities, as the enum's abilities will be dependent on + // them + let type_parameter_abilities = sh + .type_parameters + .iter() + .map(|_| AbilitySet::ALL) + .collect::>(); + for (i, variant) in enum_def.variants.iter().enumerate() { + for (fi, field) in variant.fields.iter().enumerate() { + let field_abilities = ability_cache.abilities( + Scope::Module, + meter, + &type_parameter_abilities, + &field.signature.0, + )?; + if !required_abilities.is_subset(field_abilities) { + return Err(verification_error( + StatusCode::FIELD_MISSING_TYPE_ABILITY, + IndexKind::EnumDefinition, + idx as TableIndex, + ) + .at_index(IndexKind::VariantTag, i as TableIndex) + .at_index(IndexKind::FieldDefinition, fi as TableIndex)); + } + } + } + } + Ok(()) +} diff --git a/external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier/src/absint.rs b/external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier/src/absint.rs new file mode 100644 index 0000000000000..80822d3db9fb9 --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier/src/absint.rs @@ -0,0 +1,207 @@ +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +use move_abstract_interpreter::{absint, control_flow_graph}; +use move_binary_format::{ + CompiledModule, + errors::{PartialVMError, PartialVMResult}, + file_format::{ + AbilitySet, Bytecode, CodeOffset, CodeUnit, FunctionDefinitionIndex, FunctionHandle, + Signature, + }, +}; +use move_bytecode_verifier_meter::{Meter, Scope}; + +//************************************************************************************************** +// New traits and public APIS +//************************************************************************************************** + +pub use absint::JoinResult; +pub type VMControlFlowGraph = control_flow_graph::VMControlFlowGraph; + +pub trait AbstractDomain: Clone + Sized { + fn join( + &mut self, + other: &Self, + meter: &mut (impl Meter + ?Sized), + ) -> PartialVMResult; +} + +pub trait TransferFunctions { + type State: AbstractDomain; + + /// Execute local@instr found at index local@index in the current basic block from pre-state + /// local@pre. + /// Should return an Err if executing the instruction is unsuccessful, and () if + /// the effects of successfully executing local@instr have been reflected by mutating + /// local@pre. + /// Auxiliary data from the analysis that is not part of the abstract state can be collected by + /// mutating local@self. + /// The last instruction index in the current block is local@last_index. Knowing this + /// information allows clients to detect the end of a basic block and special-case appropriately + /// (e.g., normalizing the abstract state before a join). + fn execute( + &mut self, + pre: &mut Self::State, + instr: &Bytecode, + index: CodeOffset, + bounds: (CodeOffset, CodeOffset), + meter: &mut (impl Meter + ?Sized), + ) -> PartialVMResult<()>; +} + +pub fn analyze_function( + function_context: &FunctionContext, + meter: &mut M, + transfer_functions: &mut TF, + initial_state: TF::State, +) -> Result<(), PartialVMError> { + let mut interpreter = AbstractInterpreter { + meter, + transfer_functions, + }; + let _states = absint::analyze_function( + &mut interpreter, + &function_context.cfg, + &function_context.code.code, + initial_state, + )?; + Ok(()) +} + +/// A `FunctionContext` holds all the information needed by the verifier for `FunctionDefinition`.` +/// A control flow graph is built for a function when the `FunctionContext` is created. +pub struct FunctionContext<'a> { + index: Option, + code: &'a CodeUnit, + parameters: &'a Signature, + return_: &'a Signature, + locals: &'a Signature, + type_parameters: &'a [AbilitySet], + cfg: VMControlFlowGraph, +} + +impl<'a> FunctionContext<'a> { + // Creates a `FunctionContext` for a module function. + pub fn new( + module: &'a CompiledModule, + index: FunctionDefinitionIndex, + code: &'a CodeUnit, + function_handle: &'a FunctionHandle, + ) -> Self { + Self { + index: Some(index), + code, + parameters: module.signature_at(function_handle.parameters), + return_: module.signature_at(function_handle.return_), + locals: module.signature_at(code.locals), + type_parameters: &function_handle.type_parameters, + cfg: VMControlFlowGraph::new(&code.code, &code.jump_tables), + } + } + + pub fn index(&self) -> Option { + self.index + } + + pub fn code(&self) -> &'a CodeUnit { + self.code + } + + pub fn parameters(&self) -> &'a Signature { + self.parameters + } + + pub fn return_(&self) -> &'a Signature { + self.return_ + } + + pub fn locals(&self) -> &'a Signature { + self.locals + } + + pub fn type_parameters(&self) -> &'a [AbilitySet] { + self.type_parameters + } + + pub fn cfg(&self) -> &VMControlFlowGraph { + &self.cfg + } +} + +//************************************************************************************************** +// Wrappers around shared absint and control flow graph implementations +//************************************************************************************************** + +/// Costs for metered verification +const ANALYZE_FUNCTION_BASE_COST: u128 = 10; +const EXECUTE_BLOCK_BASE_COST: u128 = 10; +const PER_BACKEDGE_COST: u128 = 10; +const PER_SUCCESSOR_COST: u128 = 10; + +struct AbstractInterpreter<'a, M: Meter + ?Sized, TF: TransferFunctions> { + pub meter: &'a mut M, + pub transfer_functions: &'a mut TF, +} + +impl absint::AbstractInterpreter + for AbstractInterpreter<'_, M, TF> +{ + type Error = PartialVMError; + type BlockId = CodeOffset; + type State = ::State; + type InstructionIndex = CodeOffset; + type Instruction = Bytecode; + + fn start(&mut self) -> Result<(), Self::Error> { + self.meter.add(Scope::Function, ANALYZE_FUNCTION_BASE_COST) + } + + fn join( + &mut self, + pre: &mut Self::State, + post: &Self::State, + ) -> Result { + pre.join(post, self.meter) + } + + fn visit_block_pre_execution( + &mut self, + _block_id: Self::BlockId, + _invariant: &mut absint::BlockInvariant, + ) -> Result<(), Self::Error> { + self.meter.add(Scope::Function, EXECUTE_BLOCK_BASE_COST) + } + + fn visit_block_post_execution( + &mut self, + _block_id: Self::BlockId, + _invariant: &mut absint::BlockInvariant, + ) -> Result<(), Self::Error> { + Ok(()) + } + + fn visit_successor(&mut self, _block_id: Self::BlockId) -> Result<(), Self::Error> { + self.meter.add(Scope::Function, PER_SUCCESSOR_COST) + } + + fn visit_back_edge( + &mut self, + _from: Self::BlockId, + _to: Self::BlockId, + ) -> Result<(), Self::Error> { + self.meter.add(Scope::Function, PER_BACKEDGE_COST) + } + + fn execute( + &mut self, + _block_id: Self::BlockId, + bounds: (CodeOffset, CodeOffset), + state: &mut Self::State, + offset: CodeOffset, + instr: &Bytecode, + ) -> Result<(), Self::Error> { + self.transfer_functions + .execute(state, instr, offset, bounds, self.meter) + } +} diff --git a/external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier/src/acquires_list_verifier.rs b/external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier/src/acquires_list_verifier.rs new file mode 100644 index 0000000000000..2dd0737f5ff62 --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier/src/acquires_list_verifier.rs @@ -0,0 +1,239 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +//! This module implements a checker for verifying properties about the acquires list on function +//! definitions. Function definitions must annotate the global resources (declared in that module) +//! accesssed by `BorrowGlobal`, `MoveFrom`, and any transitive function calls +//! The list of acquired resources (stored in `FunctionDefinition`'s `acquires_global_resources` +//! field) must have: +//! - No duplicate resources (checked by `check_duplication`) +//! - No missing resources (any resource acquired must be present) +//! - No additional resources (no extraneous resources not actually acquired) + +use std::collections::{BTreeSet, HashMap}; + +use move_binary_format::{ + errors::{PartialVMError, PartialVMResult}, + file_format::{ + Bytecode, CodeOffset, CompiledModule, FunctionDefinition, FunctionDefinitionIndex, + FunctionHandle, FunctionHandleIndex, StructDefinitionIndex, + }, + safe_unwrap, +}; +use move_bytecode_verifier_meter::Meter; +use move_core_types::vm_status::StatusCode; +use move_vm_config::verifier::VerifierConfig; + +pub(crate) struct AcquiresVerifier<'a> { + module: &'a CompiledModule, + current_function: FunctionDefinitionIndex, + annotated_acquires: BTreeSet, + actual_acquires: BTreeSet, + handle_to_def: HashMap, +} + +impl<'a> AcquiresVerifier<'a> { + pub(crate) fn verify( + verifier_config: &VerifierConfig, + module: &'a CompiledModule, + index: FunctionDefinitionIndex, + function_definition: &'a FunctionDefinition, + _meter: &mut (impl Meter + ?Sized), // currently unused + ) -> PartialVMResult<()> { + if verifier_config.deprecate_global_storage_ops { + return Ok(()); + } + let annotated_acquires: BTreeSet<_> = function_definition + .acquires_global_resources + .iter() + .cloned() + .collect(); + let mut handle_to_def = HashMap::new(); + for func_def in module.function_defs() { + handle_to_def.insert(func_def.function, func_def); + } + let mut verifier = Self { + module, + current_function: index, + annotated_acquires, + actual_acquires: BTreeSet::new(), + handle_to_def, + }; + + for (offset, instruction) in safe_unwrap!(function_definition.code.as_ref()) + .code + .iter() + .enumerate() + { + verifier.verify_instruction(instruction, offset as CodeOffset)? + } + + for annotation in verifier.annotated_acquires { + if !verifier.actual_acquires.contains(&annotation) { + return Err(PartialVMError::new( + StatusCode::EXTRANEOUS_ACQUIRES_ANNOTATION, + )); + } + + let struct_def = safe_unwrap!(module.struct_defs().get(annotation.0 as usize)); + let struct_handle = module.datatype_handle_at(struct_def.struct_handle); + if !struct_handle.abilities.has_key() { + return Err(PartialVMError::new(StatusCode::INVALID_ACQUIRES_ANNOTATION)); + } + } + + Ok(()) + } + + fn verify_instruction( + &mut self, + instruction: &Bytecode, + offset: CodeOffset, + ) -> PartialVMResult<()> { + match instruction { + Bytecode::Call(idx) => self.call_acquire(*idx, offset), + Bytecode::CallGeneric(idx) => { + let fi = self.module.function_instantiation_at(*idx); + self.call_acquire(fi.handle, offset) + } + Bytecode::MoveFromDeprecated(idx) + | Bytecode::MutBorrowGlobalDeprecated(idx) + | Bytecode::ImmBorrowGlobalDeprecated(idx) => self.struct_acquire(*idx, offset), + Bytecode::MoveFromGenericDeprecated(idx) + | Bytecode::MutBorrowGlobalGenericDeprecated(idx) + | Bytecode::ImmBorrowGlobalGenericDeprecated(idx) => { + let si = self.module.struct_instantiation_at(*idx); + self.struct_acquire(si.def, offset) + } + + Bytecode::Pop + | Bytecode::BrTrue(_) + | Bytecode::BrFalse(_) + | Bytecode::Abort + | Bytecode::Branch(_) + | Bytecode::Nop + | Bytecode::Ret + | Bytecode::StLoc(_) + | Bytecode::MoveLoc(_) + | Bytecode::CopyLoc(_) + | Bytecode::ImmBorrowLoc(_) + | Bytecode::MutBorrowLoc(_) + | Bytecode::FreezeRef + | Bytecode::MutBorrowField(_) + | Bytecode::MutBorrowFieldGeneric(_) + | Bytecode::ImmBorrowField(_) + | Bytecode::ImmBorrowFieldGeneric(_) + | Bytecode::LdU8(_) + | Bytecode::LdU16(_) + | Bytecode::LdU32(_) + | Bytecode::LdU64(_) + | Bytecode::LdU128(_) + | Bytecode::LdU256(_) + | Bytecode::LdConst(_) + | Bytecode::LdTrue + | Bytecode::LdFalse + | Bytecode::Pack(_) + | Bytecode::PackGeneric(_) + | Bytecode::Unpack(_) + | Bytecode::UnpackGeneric(_) + | Bytecode::ReadRef + | Bytecode::WriteRef + | Bytecode::CastU8 + | Bytecode::CastU16 + | Bytecode::CastU32 + | Bytecode::CastU64 + | Bytecode::CastU128 + | Bytecode::CastU256 + | Bytecode::Add + | Bytecode::Sub + | Bytecode::Mul + | Bytecode::Mod + | Bytecode::Div + | Bytecode::BitOr + | Bytecode::BitAnd + | Bytecode::Xor + | Bytecode::Shl + | Bytecode::Shr + | Bytecode::Or + | Bytecode::And + | Bytecode::Not + | Bytecode::Eq + | Bytecode::Neq + | Bytecode::Lt + | Bytecode::Gt + | Bytecode::Le + | Bytecode::Ge + | Bytecode::ExistsDeprecated(_) + | Bytecode::ExistsGenericDeprecated(_) + | Bytecode::MoveToDeprecated(_) + | Bytecode::MoveToGenericDeprecated(_) + | Bytecode::VecPack(..) + | Bytecode::VecLen(_) + | Bytecode::VecImmBorrow(_) + | Bytecode::VecMutBorrow(_) + | Bytecode::VecPushBack(_) + | Bytecode::VecPopBack(_) + | Bytecode::VecUnpack(..) + | Bytecode::VecSwap(_) + | Bytecode::PackVariant(_) + | Bytecode::PackVariantGeneric(_) + | Bytecode::UnpackVariant(_) + | Bytecode::UnpackVariantImmRef(_) + | Bytecode::UnpackVariantMutRef(_) + | Bytecode::UnpackVariantGeneric(_) + | Bytecode::UnpackVariantGenericImmRef(_) + | Bytecode::UnpackVariantGenericMutRef(_) + | Bytecode::VariantSwitch(_) => Ok(()), + } + } + + fn call_acquire( + &mut self, + fh_idx: FunctionHandleIndex, + offset: CodeOffset, + ) -> PartialVMResult<()> { + let function_handle = self.module.function_handle_at(fh_idx); + let mut function_acquired_resources = + self.function_acquired_resources(function_handle, fh_idx); + for acquired_resource in &function_acquired_resources { + if !self.annotated_acquires.contains(acquired_resource) { + return Err(self.error(StatusCode::MISSING_ACQUIRES_ANNOTATION, offset)); + } + } + self.actual_acquires + .append(&mut function_acquired_resources); + Ok(()) + } + + fn struct_acquire( + &mut self, + sd_idx: StructDefinitionIndex, + offset: CodeOffset, + ) -> PartialVMResult<()> { + if self.annotated_acquires.contains(&sd_idx) { + self.actual_acquires.insert(sd_idx); + Ok(()) + } else { + Err(self.error(StatusCode::MISSING_ACQUIRES_ANNOTATION, offset)) + } + } + + fn function_acquired_resources( + &self, + function_handle: &FunctionHandle, + fh_idx: FunctionHandleIndex, + ) -> BTreeSet { + if function_handle.module != self.module.self_handle_idx() { + return BTreeSet::new(); + } + match self.handle_to_def.get(&fh_idx) { + Some(func_def) => func_def.acquires_global_resources.iter().cloned().collect(), + None => BTreeSet::new(), + } + } + + fn error(&self, status: StatusCode, offset: CodeOffset) -> PartialVMError { + PartialVMError::new(status).at_code_offset(self.current_function, offset) + } +} diff --git a/external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier/src/check_duplication.rs b/external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier/src/check_duplication.rs new file mode 100644 index 0000000000000..1eeee00c77589 --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier/src/check_duplication.rs @@ -0,0 +1,412 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +//! This module implements a checker for verifying that each vector in a CompiledModule contains +//! distinct values. Successful verification implies that an index in vector can be used to +//! uniquely name the entry at that index. Additionally, the checker also verifies the +//! following: +//! - struct and field definitions are consistent +//! - the handles in struct and function definitions point to the self module index +//! - all struct and function handles pointing to the self module index have a definition +use move_binary_format::{ + IndexKind, + errors::{Location, PartialVMResult, VMResult, verification_error}, + file_format::{ + CompiledModule, Constant, DatatypeHandle, DatatypeHandleIndex, FunctionHandle, + FunctionHandleIndex, FunctionInstantiation, ModuleHandle, Signature, + StructFieldInformation, TableIndex, VariantHandle, + }, +}; +use move_core_types::{ + account_address::AccountAddress, identifier::Identifier, vm_status::StatusCode, +}; +use std::{collections::HashSet, hash::Hash}; + +pub struct DuplicationChecker<'a> { + module: &'a CompiledModule, +} + +impl<'a> DuplicationChecker<'a> { + pub fn verify_module(module: &'a CompiledModule) -> VMResult<()> { + Self::verify_module_impl(module).map_err(|e| e.finish(Location::Module(module.self_id()))) + } + + fn verify_module_impl(module: &'a CompiledModule) -> PartialVMResult<()> { + Self::check_identifiers(module.identifiers())?; + Self::check_address_identifiers(module.address_identifiers())?; + Self::check_constants(module.constant_pool())?; + Self::check_signatures(module.signatures())?; + Self::check_module_handles(module.module_handles())?; + Self::check_module_handles(module.friend_decls())?; + Self::check_datatype_handles(module.datatype_handles())?; + Self::check_function_handles(module.function_handles())?; + Self::check_function_instantiations(module.function_instantiations())?; + Self::check_variant_handles(module.variant_handles())?; + + let checker = Self { module }; + checker.check_field_handles()?; + checker.check_field_instantiations()?; + checker.check_function_defintions()?; + checker.check_struct_definitions()?; + checker.check_struct_instantiations()?; + checker.check_enum_definitions()?; + checker.check_enum_instantiations()?; + checker.check_datatype_handles_implemented() + } + + fn check_identifiers(identifiers: &[Identifier]) -> PartialVMResult<()> { + match Self::first_duplicate_element(identifiers) { + Some(idx) => Err(verification_error( + StatusCode::DUPLICATE_ELEMENT, + IndexKind::Identifier, + idx, + )), + None => Ok(()), + } + } + + fn check_address_identifiers(address_identifiers: &[AccountAddress]) -> PartialVMResult<()> { + match Self::first_duplicate_element(address_identifiers) { + Some(idx) => Err(verification_error( + StatusCode::DUPLICATE_ELEMENT, + IndexKind::AddressIdentifier, + idx, + )), + None => Ok(()), + } + } + + fn check_constants(constant_pool: &[Constant]) -> PartialVMResult<()> { + match Self::first_duplicate_element(constant_pool) { + Some(idx) => Err(verification_error( + StatusCode::DUPLICATE_ELEMENT, + IndexKind::ConstantPool, + idx, + )), + None => Ok(()), + } + } + + fn check_signatures(signatures: &[Signature]) -> PartialVMResult<()> { + match Self::first_duplicate_element(signatures) { + Some(idx) => Err(verification_error( + StatusCode::DUPLICATE_ELEMENT, + IndexKind::Signature, + idx, + )), + None => Ok(()), + } + } + + fn check_module_handles(module_handles: &[ModuleHandle]) -> PartialVMResult<()> { + match Self::first_duplicate_element(module_handles) { + Some(idx) => Err(verification_error( + StatusCode::DUPLICATE_ELEMENT, + IndexKind::ModuleHandle, + idx, + )), + None => Ok(()), + } + } + + // DatatypeHandles - module and name define uniqueness + fn check_datatype_handles(datatype_handles: &[DatatypeHandle]) -> PartialVMResult<()> { + match Self::first_duplicate_element(datatype_handles.iter().map(|x| (x.module, x.name))) { + Some(idx) => Err(verification_error( + StatusCode::DUPLICATE_ELEMENT, + IndexKind::DatatypeHandle, + idx, + )), + None => Ok(()), + } + } + + fn check_variant_handles(variant_handles: &[VariantHandle]) -> PartialVMResult<()> { + match Self::first_duplicate_element(variant_handles.iter().map(|x| (x.enum_def, x.variant))) + { + Some(idx) => Err(verification_error( + StatusCode::DUPLICATE_ELEMENT, + IndexKind::VariantHandle, + idx, + )), + None => Ok(()), + } + } + + fn check_function_instantiations( + function_instantiations: &[FunctionInstantiation], + ) -> PartialVMResult<()> { + match Self::first_duplicate_element(function_instantiations) { + Some(idx) => Err(verification_error( + StatusCode::DUPLICATE_ELEMENT, + IndexKind::FunctionInstantiation, + idx, + )), + None => Ok(()), + } + } + + // FunctionHandles - module and name define uniqueness + fn check_function_handles(function_handles: &[FunctionHandle]) -> PartialVMResult<()> { + match Self::first_duplicate_element(function_handles.iter().map(|x| (x.module, x.name))) { + Some(idx) => Err(verification_error( + StatusCode::DUPLICATE_ELEMENT, + IndexKind::FunctionHandle, + idx, + )), + None => Ok(()), + } + } + + // + // Module only code + // + + fn check_field_handles(&self) -> PartialVMResult<()> { + match Self::first_duplicate_element(self.module.field_handles()) { + Some(idx) => Err(verification_error( + StatusCode::DUPLICATE_ELEMENT, + IndexKind::FieldHandle, + idx, + )), + None => Ok(()), + } + } + + fn check_struct_instantiations(&self) -> PartialVMResult<()> { + match Self::first_duplicate_element(self.module.struct_instantiations()) { + Some(idx) => Err(verification_error( + StatusCode::DUPLICATE_ELEMENT, + IndexKind::StructDefInstantiation, + idx, + )), + None => Ok(()), + } + } + + fn check_enum_instantiations(&self) -> PartialVMResult<()> { + match Self::first_duplicate_element(self.module.enum_instantiations()) { + Some(idx) => Err(verification_error( + StatusCode::DUPLICATE_ELEMENT, + IndexKind::EnumDefInstantiation, + idx, + )), + None => Ok(()), + } + } + + fn check_field_instantiations(&self) -> PartialVMResult<()> { + if let Some(idx) = Self::first_duplicate_element(self.module.field_instantiations()) { + return Err(verification_error( + StatusCode::DUPLICATE_ELEMENT, + IndexKind::FieldInstantiation, + idx, + )); + } + Ok(()) + } + + fn check_struct_definitions(&self) -> PartialVMResult<()> { + // StructDefinition - contained DatatypeHandle defines uniqueness + if let Some(idx) = + Self::first_duplicate_element(self.module.struct_defs().iter().map(|x| x.struct_handle)) + { + return Err(verification_error( + StatusCode::DUPLICATE_ELEMENT, + IndexKind::StructDefinition, + idx, + )); + } + // Field names in structs must be unique + for (struct_idx, struct_def) in self.module.struct_defs().iter().enumerate() { + let fields = match &struct_def.field_information { + StructFieldInformation::Native => continue, + StructFieldInformation::Declared(fields) => fields, + }; + if fields.is_empty() { + return Err(verification_error( + StatusCode::ZERO_SIZED_STRUCT, + IndexKind::StructDefinition, + struct_idx as TableIndex, + )); + } + if let Some(idx) = Self::first_duplicate_element(fields.iter().map(|x| x.name)) { + return Err(verification_error( + StatusCode::DUPLICATE_ELEMENT, + IndexKind::FieldDefinition, + idx, + )); + } + } + // Check that each struct definition is pointing to the self module + if let Some(idx) = self.module.struct_defs().iter().position(|x| { + self.module.datatype_handle_at(x.struct_handle).module != self.module.self_handle_idx() + }) { + return Err(verification_error( + StatusCode::INVALID_MODULE_HANDLE, + IndexKind::StructDefinition, + idx as TableIndex, + )); + } + Ok(()) + } + + fn check_datatype_handles_implemented(&self) -> PartialVMResult<()> { + let implemented_datatype_handles: HashSet = self + .module + .struct_defs() + .iter() + .map(|x| x.struct_handle) + .chain(self.module.enum_defs().iter().map(|x| x.enum_handle)) + .collect(); + if let Some(idx) = (0..self.module.datatype_handles().len()).position(|x| { + let y = DatatypeHandleIndex::new(x as u16); + self.module.datatype_handle_at(y).module == self.module.self_handle_idx() + && !implemented_datatype_handles.contains(&y) + }) { + return Err(verification_error( + StatusCode::UNIMPLEMENTED_HANDLE, + IndexKind::DatatypeHandle, + idx as TableIndex, + )); + } + Ok(()) + } + + fn check_enum_definitions(&self) -> PartialVMResult<()> { + // EnumDefinition - contained DatatypeHandle defines uniqueness + // NB: We check uniqueness across both enum and struct handles at this point to make sure + // data definitions are not duplicated across struct and enums. + if let Some(idx) = Self::first_duplicate_element( + self.module + .struct_defs() + .iter() + .map(|x| x.struct_handle) + .chain(self.module.enum_defs().iter().map(|x| x.enum_handle)), + ) { + return Err(verification_error( + StatusCode::DUPLICATE_ELEMENT, + IndexKind::EnumDefinition, + idx, + )); + } + // Variant names in enums must be unique + // Field names in each variant must be unique + for (enum_idx, enum_def) in self.module.enum_defs().iter().enumerate() { + let variants = &enum_def.variants; + if variants.is_empty() { + return Err(verification_error( + StatusCode::ZERO_SIZED_ENUM, + IndexKind::EnumDefinition, + enum_idx as TableIndex, + )); + } + if let Some(idx) = + Self::first_duplicate_element(variants.iter().map(|x| x.variant_name)) + { + return Err(verification_error( + StatusCode::DUPLICATE_ELEMENT, + IndexKind::EnumDefinition, + enum_idx as TableIndex, + ) + .at_index(IndexKind::VariantTag, idx as TableIndex)); + } + + // NB: we allow zero-sized variants: since we require non-empty enums we always have a + // tag and therefore a variant with no fields is still non-zero sized whereas a struct + // with zero fields is zero-sized. + for (tag, variant) in variants.iter().enumerate() { + if let Some(idx) = + Self::first_duplicate_element(variant.fields.iter().map(|x| x.name)) + { + return Err(verification_error( + StatusCode::DUPLICATE_ELEMENT, + IndexKind::FieldDefinition, + idx, + ) + .at_index(IndexKind::VariantTag, tag as TableIndex) + .at_index(IndexKind::EnumDefinition, enum_idx as TableIndex)); + } + } + } + // Check that each enum definition is pointing to the self module + if let Some(idx) = self.module.enum_defs().iter().position(|x| { + self.module.datatype_handle_at(x.enum_handle).module != self.module.self_handle_idx() + }) { + return Err(verification_error( + StatusCode::INVALID_MODULE_HANDLE, + IndexKind::EnumDefinition, + idx as TableIndex, + )); + } + Ok(()) + } + + fn check_function_defintions(&self) -> PartialVMResult<()> { + // FunctionDefinition - contained FunctionHandle defines uniqueness + if let Some(idx) = + Self::first_duplicate_element(self.module.function_defs().iter().map(|x| x.function)) + { + return Err(verification_error( + StatusCode::DUPLICATE_ELEMENT, + IndexKind::FunctionDefinition, + idx, + )); + } + // Acquires in function declarations contain unique struct definitions + for (idx, function_def) in self.module.function_defs().iter().enumerate() { + let acquires = function_def.acquires_global_resources.iter(); + if Self::first_duplicate_element(acquires).is_some() { + return Err(verification_error( + StatusCode::DUPLICATE_ACQUIRES_ANNOTATION, + IndexKind::FunctionDefinition, + idx as TableIndex, + )); + } + } + // Check that each function definition is pointing to the self module + if let Some(idx) = self.module.function_defs().iter().position(|x| { + self.module.function_handle_at(x.function).module != self.module.self_handle_idx() + }) { + return Err(verification_error( + StatusCode::INVALID_MODULE_HANDLE, + IndexKind::FunctionDefinition, + idx as TableIndex, + )); + } + // Check that each function handle in self module is implemented (has a declaration) + let implemented_function_handles: HashSet = self + .module + .function_defs() + .iter() + .map(|x| x.function) + .collect(); + if let Some(idx) = (0..self.module.function_handles().len()).position(|x| { + let y = FunctionHandleIndex::new(x as u16); + self.module.function_handle_at(y).module == self.module.self_handle_idx() + && !implemented_function_handles.contains(&y) + }) { + return Err(verification_error( + StatusCode::UNIMPLEMENTED_HANDLE, + IndexKind::FunctionHandle, + idx as TableIndex, + )); + } + Ok(()) + } + + fn first_duplicate_element(iter: T) -> Option + where + T: IntoIterator, + T::Item: Eq + Hash, + { + let mut uniq = HashSet::new(); + for (i, x) in iter.into_iter().enumerate() { + if !uniq.insert(x) { + return Some(i as TableIndex); + } + } + None + } +} diff --git a/external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier/src/code_unit_verifier.rs b/external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier/src/code_unit_verifier.rs new file mode 100644 index 0000000000000..63ab89dfb0758 --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier/src/code_unit_verifier.rs @@ -0,0 +1,241 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +//! This module implements the checker for verifying correctness of function bodies. +//! The overall verification is split between stack_usage_verifier.rs and +//! abstract_interpreter.rs. CodeUnitVerifier simply orchestrates calls into these two files. +use crate::{ + ability_cache::AbilityCache, absint::FunctionContext, acquires_list_verifier::AcquiresVerifier, + control_flow, locals_safety, reference_safety, regex_reference_safety, + stack_usage_verifier::StackUsageVerifier, type_safety, +}; +use move_abstract_interpreter::control_flow_graph::ControlFlowGraph; +use move_binary_format::{ + IndexKind, + errors::{Location, PartialVMError, PartialVMResult, VMResult}, + file_format::{ + CompiledModule, FunctionDefinition, FunctionDefinitionIndex, IdentifierIndex, TableIndex, + }, +}; +use move_bytecode_verifier_meter::{Meter, Scope, bound::BoundMeter}; +use move_core_types::vm_status::StatusCode; +use move_vm_config::verifier::VerifierConfig; +use std::collections::HashMap; + +pub struct CodeUnitVerifier<'env, 'a> { + module: &'env CompiledModule, + function_context: FunctionContext<'env>, + name_def_map: &'a HashMap, +} + +pub fn verify_module<'env>( + verifier_config: &'env VerifierConfig, + module: &'env CompiledModule, + ability_cache: &mut AbilityCache<'env>, + meter: &mut (impl Meter + ?Sized), +) -> VMResult<()> { + let mut regex_reference_safety_meter = + if let Some(limit) = verifier_config.sanity_check_with_regex_reference_safety { + let module_name = module.identifier_at(module.self_handle().name).as_str(); + let mut m = BoundMeter::new(move_vm_config::verifier::MeterConfig { + max_per_fun_meter_units: Some(limit), + max_per_mod_meter_units: Some(limit), + max_per_pkg_meter_units: Some(limit), + }); + m.enter_scope(module_name, Scope::Module); + m + } else { + // unused + BoundMeter::new(move_vm_config::verifier::MeterConfig { + max_per_fun_meter_units: None, + max_per_mod_meter_units: None, + max_per_pkg_meter_units: None, + }) + }; + verify_module_impl( + verifier_config, + module, + ability_cache, + meter, + &mut regex_reference_safety_meter, + ) + .map_err(|e| e.finish(Location::Module(module.self_id()))) +} + +fn verify_module_impl<'env>( + verifier_config: &'env VerifierConfig, + module: &'env CompiledModule, + ability_cache: &mut AbilityCache<'env>, + meter: &mut (impl Meter + ?Sized), + regex_reference_safety_meter: &mut (impl Meter + ?Sized), +) -> PartialVMResult<()> { + let mut name_def_map = HashMap::new(); + for (idx, func_def) in module.function_defs().iter().enumerate() { + let fh = module.function_handle_at(func_def.function); + name_def_map.insert(fh.name, FunctionDefinitionIndex(idx as u16)); + } + let mut total_back_edges = 0; + for (idx, function_definition) in module.function_defs().iter().enumerate() { + let index = FunctionDefinitionIndex(idx as TableIndex); + let num_back_edges = verify_function( + verifier_config, + index, + function_definition, + module, + ability_cache, + &name_def_map, + meter, + regex_reference_safety_meter, + ) + .map_err(|err| err.at_index(IndexKind::FunctionDefinition, index.0))?; + total_back_edges += num_back_edges; + } + if let Some(limit) = verifier_config.max_back_edges_per_module + && total_back_edges > limit + { + return Err(PartialVMError::new(StatusCode::TOO_MANY_BACK_EDGES)); + } + Ok(()) +} + +pub fn verify_function<'env>( + verifier_config: &'env VerifierConfig, + index: FunctionDefinitionIndex, + function_definition: &'env FunctionDefinition, + module: &'env CompiledModule, + ability_cache: &mut AbilityCache<'env>, + name_def_map: &HashMap, + meter: &mut (impl Meter + ?Sized), + regex_reference_safety_meter: &mut (impl Meter + ?Sized), +) -> PartialVMResult { + let function_name = module + .identifier_at(module.function_handle_at(function_definition.function).name) + .as_str(); + meter.enter_scope(function_name, Scope::Function); + regex_reference_safety_meter.enter_scope(function_name, Scope::Function); + // nothing to verify for native function + let code = match &function_definition.code { + Some(code) => code, + None => return Ok(0), + }; + + // create `FunctionContext` and `BinaryIndexedView` + let function_context = control_flow::verify_function( + verifier_config, + module, + index, + function_definition, + code, + meter, + )?; + + if let Some(limit) = verifier_config.max_basic_blocks + && function_context.cfg().blocks().count() > limit + { + return Err(PartialVMError::new(StatusCode::TOO_MANY_BASIC_BLOCKS).at_code_offset(index, 0)); + } + + let num_back_edges = function_context.cfg().num_back_edges(); + if let Some(limit) = verifier_config.max_back_edges_per_function + && num_back_edges > limit + { + return Err(PartialVMError::new(StatusCode::TOO_MANY_BACK_EDGES).at_code_offset(index, 0)); + } + + // verify + let code_unit_verifier = CodeUnitVerifier { + module, + function_context, + name_def_map, + }; + code_unit_verifier.verify_common( + verifier_config, + ability_cache, + meter, + regex_reference_safety_meter, + )?; + AcquiresVerifier::verify(verifier_config, module, index, function_definition, meter)?; + + meter.transfer(Scope::Function, Scope::Module, 1.0)?; + + Ok(num_back_edges) +} + +impl<'env> CodeUnitVerifier<'env, '_> { + fn verify_common( + &self, + verifier_config: &'env VerifierConfig, + ability_cache: &mut AbilityCache<'env>, + meter: &mut (impl Meter + ?Sized), + regex_reference_safety_meter: &mut (impl Meter + ?Sized), + ) -> PartialVMResult<()> { + StackUsageVerifier::verify(verifier_config, self.module, &self.function_context, meter)?; + type_safety::verify( + verifier_config, + self.module, + &self.function_context, + ability_cache, + meter, + )?; + locals_safety::verify(self.module, &self.function_context, ability_cache, meter)?; + let reference_safety_res = reference_safety::verify( + verifier_config, + self.module, + &self.function_context, + self.name_def_map, + meter, + ); + if reference_safety_res.as_ref().is_err_and(|e| { + e.major_status() == StatusCode::CONSTRAINT_NOT_SATISFIED + || e.major_status() == StatusCode::PROGRAM_TOO_COMPLEX + }) { + // skip consistency check on timeout/complexity errors + return reference_safety_res; + } + if verifier_config + .sanity_check_with_regex_reference_safety + .is_some() + { + let regex_res = regex_reference_safety::verify( + verifier_config, + self.module, + &self.function_context, + regex_reference_safety_meter, + ); + if regex_res.as_ref().is_err_and(|e| { + e.major_status() == StatusCode::CONSTRAINT_NOT_SATISFIED + || e.major_status() == StatusCode::PROGRAM_TOO_COMPLEX + }) { + // If the regex based checker fails due to complexity, + // we reject it for being too complex and skip the consistency check. + return Err( + PartialVMError::new(StatusCode::PROGRAM_TOO_COMPLEX).with_message( + regex_res + .unwrap_err() + .finish(Location::Undefined) + .message() + .cloned() + .unwrap_or_default(), + ), + ); + } + // The regular expression based reference safety check should be strictly more + // permissive. So if it errors, the current one should also error. + // As such, we assert: regex err ==> reference safety err + // which is equivalent to: !regex_res.is_err() || reference_safety_res.is_err() + // which is equivalent to: regex_res.is_ok() || reference_safety_res.is_err() + let is_consistent = regex_res.is_ok() || reference_safety_res.is_err(); + if !is_consistent { + return Err( + PartialVMError::new(StatusCode::REFERENCE_SAFETY_INCONSISTENT).with_message( + "regex reference safety should be strictly more permissive \ + than the current" + .to_string(), + ), + ); + } + } + reference_safety_res + } +} diff --git a/external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier/src/constants.rs b/external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier/src/constants.rs new file mode 100644 index 0000000000000..42599115f7fc8 --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier/src/constants.rs @@ -0,0 +1,52 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +//! This module implements a checker for verifying that +//! - a constant's type only refers to primitive types +//! - a constant's data serializes correctly for that type +use move_binary_format::{ + IndexKind, + errors::{Location, PartialVMResult, VMResult, verification_error}, + file_format::{CompiledModule, Constant, SignatureToken, TableIndex}, +}; +use move_core_types::vm_status::StatusCode; + +pub fn verify_module(module: &CompiledModule) -> VMResult<()> { + verify_module_impl(module).map_err(|e| e.finish(Location::Module(module.self_id()))) +} + +fn verify_module_impl(module: &CompiledModule) -> PartialVMResult<()> { + for (idx, constant) in module.constant_pool().iter().enumerate() { + verify_constant(idx, constant)? + } + Ok(()) +} + +fn verify_constant(idx: usize, constant: &Constant) -> PartialVMResult<()> { + verify_constant_type(idx, &constant.type_)?; + verify_constant_data(idx, constant) +} + +fn verify_constant_type(idx: usize, type_: &SignatureToken) -> PartialVMResult<()> { + if type_.is_valid_for_constant() { + Ok(()) + } else { + Err(verification_error( + StatusCode::INVALID_CONSTANT_TYPE, + IndexKind::ConstantPool, + idx as TableIndex, + )) + } +} + +fn verify_constant_data(idx: usize, constant: &Constant) -> PartialVMResult<()> { + match constant.deserialize_constant() { + Some(_) => Ok(()), + None => Err(verification_error( + StatusCode::MALFORMED_CONSTANT_DATA, + IndexKind::ConstantPool, + idx as TableIndex, + )), + } +} diff --git a/external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier/src/control_flow.rs b/external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier/src/control_flow.rs new file mode 100644 index 0000000000000..1319a96cf0b06 --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier/src/control_flow.rs @@ -0,0 +1,164 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +//! This module implements control flow checks. +//! +//! For bytecode versions 6 and up, the following properties are ensured: +//! - The CFG is not empty and the last block ends in an unconditional jump, so it's not possible to +//! fall off the end of a function. +//! - The CFG is reducible (and optionally max loop depth is bounded), to limit the potential for +//! pathologically long abstract interpretation runtimes (through poor choice of loop heads and +//! back edges). +//! +//! For bytecode versions 5 and below, delegates to `control_flow_v5`. +use crate::{ + absint::FunctionContext, + control_flow_v5, + loop_summary::{LoopPartition, LoopSummary}, +}; +use move_binary_format::{ + CompiledModule, + errors::{PartialVMError, PartialVMResult}, + file_format::{CodeOffset, CodeUnit, FunctionDefinition, FunctionDefinitionIndex}, +}; +use move_bytecode_verifier_meter::Meter; +use move_core_types::vm_status::StatusCode; +use move_vm_config::verifier::VerifierConfig; +use std::collections::BTreeSet; + +/// Perform control flow verification on the compiled function, returning its `FunctionContext` if +/// verification was successful. +pub fn verify_function<'env>( + verifier_config: &VerifierConfig, + module: &'env CompiledModule, + index: FunctionDefinitionIndex, + function_definition: &'env FunctionDefinition, + code: &'env CodeUnit, + _meter: &mut (impl Meter + ?Sized), // TODO: metering +) -> PartialVMResult> { + let function_handle = module.function_handle_at(function_definition.function); + + if module.version() <= 5 { + control_flow_v5::verify(verifier_config, Some(index), code)?; + Ok(FunctionContext::new(module, index, code, function_handle)) + } else { + verify_fallthrough(Some(index), code)?; + let function_context = FunctionContext::new(module, index, code, function_handle); + verify_reducibility(verifier_config, &function_context)?; + Ok(function_context) + } +} + +/// Check to make sure that the bytecode vector is non-empty and ends with a branching instruction. +fn verify_fallthrough( + current_function_opt: Option, + code: &CodeUnit, +) -> PartialVMResult<()> { + let current_function = current_function_opt.unwrap_or(FunctionDefinitionIndex(0)); + match code.code.last() { + None => Err(PartialVMError::new(StatusCode::EMPTY_CODE_UNIT)), + Some(last) if !last.is_unconditional_branch() => { + Err(PartialVMError::new(StatusCode::INVALID_FALL_THROUGH) + .at_code_offset(current_function, (code.code.len() - 1) as CodeOffset)) + } + Some(_) => Ok(()), + } +} + +/// Test that `function_context`'s control-flow graph is reducible using Tarjan's algorithm [1]. +/// Optionally test loop depth bounded by `verifier_config.max_loop_depth`. +/// +/// A CFG, `G`, with starting block `s` is reducible if and only if [2] any of the following +/// equivalent properties hold: +/// +/// 1. G has a unique set of back-edges `u -> v` where `v` dominates `u`, that corresponds to the +/// set of back-edges for any depth-first spanning tree of G. +/// +/// 2. Every loop in G contains a unique node `h` (the "head") which dominates all other nodes in +/// the loop. +/// +/// 3. G has a unique maximal (in terms of number of edges) acyclic sub-graph. +/// +/// 4. G can be reduced to a CFG containing just `s` through a sequence of the following two +/// operations: +/// a. Delete a cyclic edge `v -> v` +/// b. For an edge `e: u -> v` where `e` is the only incident edge to `v`, collapse `v` into `u` +/// by deleting `e` and `v` and replacing all `v -> w` edges with `u -> w` edges. +/// +/// Reducibility means that a control-flow graph can be decomposed into a series of nested loops +/// (strongly connected subgraphs), which leads to more predictable abstract interpretation +/// performance. +/// +/// ## References +/// +/// 1. Tarjan, R. 1974. Testing Flow Graph Reducibility. +/// 2. Hecht, M. S., Ullman J. D. 1974. Characterizations of Reducible Flow Graphs. +fn verify_reducibility<'a>( + verifier_config: &VerifierConfig, + function_context: &'a FunctionContext<'a>, +) -> PartialVMResult<()> { + let current_function = function_context + .index() + .unwrap_or(FunctionDefinitionIndex(0)); + let err = move |code: StatusCode, offset: CodeOffset| { + Err(PartialVMError::new(code).at_code_offset(current_function, offset)) + }; + + let summary = LoopSummary::new(function_context.cfg()); + let mut partition = LoopPartition::new(&summary); + + // Iterate through nodes in reverse pre-order so more deeply nested loops (which would appear + // later in the pre-order) are processed first. + for head in summary.preorder().rev() { + // If a node has no back edges, it is not a loop head, so doesn't need to be processed. + let back = summary.back_edges(head); + if back.is_empty() { + continue; + } + + // Collect the rest of the nodes in `head`'s loop, in `body`. Start with the nodes that + // jump back to the head, and grow `body` by repeatedly following predecessor edges until + // `head` is found again. + + let mut body = BTreeSet::new(); + for node in back { + let node = partition.containing_loop(*node); + + if node != head { + body.insert(node); + } + } + + let mut frontier: Vec<_> = body.iter().copied().collect(); + while let Some(node) = frontier.pop() { + for pred in summary.pred_edges(node) { + let pred = partition.containing_loop(*pred); + + // `pred` can eventually jump back to `head`, so is part of its body. If it is not + // a descendant of `head`, it implies that `head` does not dominate a node in its + // loop, therefore the CFG is not reducible, according to Property 1 (see doc + // comment). + if !summary.is_descendant(/* ancestor */ head, /* descendant */ pred) { + return err(StatusCode::INVALID_LOOP_SPLIT, summary.block(pred)); + } + + let body_extended = pred != head && body.insert(pred); + if body_extended { + frontier.push(pred); + } + } + } + + // Collapse all the nodes in `body` into `head`, so it appears as one node when processing + // outer loops (this performs a sequence of Operation 4(b), followed by a 4(a)). + let depth = partition.collapse_loop(head, &body); + if let Some(max_depth) = verifier_config.max_loop_depth + && depth as usize > max_depth + { + return err(StatusCode::LOOP_MAX_DEPTH_REACHED, summary.block(head)); + } + } + + Ok(()) +} diff --git a/external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier/src/control_flow_v5.rs b/external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier/src/control_flow_v5.rs new file mode 100644 index 0000000000000..eb705b9498b8b --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier/src/control_flow_v5.rs @@ -0,0 +1,283 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +//! This module implements a checker to verify control flow in bytecode version 5 and below. The +//! following properties are ensured: +//! - All forward jumps do not enter into the middle of a loop +//! - All "breaks" (forward, loop-exiting jumps) go to the "end" of the loop +//! - All "continues" (back jumps in a loop) are only to the current loop +use move_binary_format::{ + errors::{PartialVMError, PartialVMResult}, + file_format::{Bytecode, CodeOffset, CodeUnit, FunctionDefinitionIndex}, + safe_unwrap, +}; +use move_core_types::vm_status::StatusCode; +use move_vm_config::verifier::VerifierConfig; +use std::{collections::HashSet, convert::TryInto}; + +pub fn verify( + verifier_config: &VerifierConfig, + current_function_opt: Option, + code: &CodeUnit, +) -> PartialVMResult<()> { + let current_function = current_function_opt.unwrap_or(FunctionDefinitionIndex(0)); + + // check fallthrough + verify_fallthrough(current_function, &code.code)?; + + // check jumps + let context = &ControlFlowVerifier { + current_function, + code: &code.code, + }; + let labels = instruction_labels(context); + check_jumps(verifier_config, context, labels) +} + +fn verify_fallthrough( + current_function: FunctionDefinitionIndex, + code: &[Bytecode], +) -> PartialVMResult<()> { + // Check to make sure that the bytecode vector ends with a branching instruction. + match code.last() { + None => Err(PartialVMError::new(StatusCode::EMPTY_CODE_UNIT)), + Some(last) if !last.is_unconditional_branch() => { + Err(PartialVMError::new(StatusCode::INVALID_FALL_THROUGH) + .at_code_offset(current_function, (code.len() - 1) as CodeOffset)) + } + Some(_) => Ok(()), + } +} + +#[derive(Clone, Copy)] +enum Label { + Loop { last_continue: u16 }, + Code, +} + +struct ControlFlowVerifier<'a> { + current_function: FunctionDefinitionIndex, + code: &'a Vec, +} + +impl<'a> ControlFlowVerifier<'a> { + fn code(&self) -> impl Iterator { + self.code + .iter() + .enumerate() + .map(|(idx, instr)| (idx.try_into().unwrap(), instr)) + } + + fn labeled_code<'b: 'a>( + &self, + labels: &'b [Label], + ) -> impl Iterator { + self.code() + .zip(labels) + .map(|((i, instr), lbl)| (i, instr, lbl)) + } + + fn error(&self, status: StatusCode, offset: CodeOffset) -> PartialVMError { + PartialVMError::new(status).at_code_offset(self.current_function, offset) + } +} + +fn instruction_labels(context: &ControlFlowVerifier) -> Vec
+ ( + Signature(vec![SignatureToken::Vector(Box::new( + SignatureToken::Address, + ))]), + vec![MoveValue::Vector(vec![MoveValue::Bool(true)])], + StatusCode::FAILED_TO_DESERIALIZE_ARGUMENT, + ), + // u128 passed for vec
+ ( + Signature(vec![SignatureToken::Vector(Box::new( + SignatureToken::Address, + ))]), + vec![MoveValue::U128(12)], + StatusCode::FAILED_TO_DESERIALIZE_ARGUMENT, + ), + // u8 passed for vector> + ( + Signature(vec![SignatureToken::Vector(Box::new( + SignatureToken::Vector(Box::new(SignatureToken::U8)), + ))]), + vec![MoveValue::U8(12)], + StatusCode::FAILED_TO_DESERIALIZE_ARGUMENT, + ), + ] +} + +fn general_cases() -> Vec<( + Signature, + Vec, + Vec, + Option, +)> { + vec![ + // too few signers (0) + ( + Signature(vec![SignatureToken::Signer, SignatureToken::Signer]), + vec![], + vec![], + Some(StatusCode::NUMBER_OF_ARGUMENTS_MISMATCH), + ), + // too few signers (1) + ( + Signature(vec![SignatureToken::Signer, SignatureToken::Signer]), + vec![], + vec![AccountAddress::random()], + Some(StatusCode::NUMBER_OF_ARGUMENTS_MISMATCH), + ), + // too few signers (3) + ( + Signature(vec![SignatureToken::Signer, SignatureToken::Signer]), + vec![], + vec![ + AccountAddress::random(), + AccountAddress::random(), + AccountAddress::random(), + ], + Some(StatusCode::NUMBER_OF_ARGUMENTS_MISMATCH), + ), + // correct number of signers (2) + ( + Signature(vec![SignatureToken::Signer, SignatureToken::Signer]), + vec![], + vec![AccountAddress::random(), AccountAddress::random()], + None, + ), + // too many signers (1) in a script that expects 0 is no longer ok + ( + Signature(vec![SignatureToken::U8]), + vec![MoveValue::U8(0)], + vec![AccountAddress::random()], + Some(StatusCode::NUMBER_OF_ARGUMENTS_MISMATCH), + ), + // signer + ( + Signature(vec![ + SignatureToken::Signer, + SignatureToken::Bool, + SignatureToken::Address, + ]), + vec![ + MoveValue::Bool(false), + MoveValue::Address(AccountAddress::random()), + ], + vec![AccountAddress::random()], + None, + ), + ] +} + +#[test] +fn check_script_function() { + // + // Bad signatures + // + for signature in deprecated_bad_signatures() { + let num_args = signature.0.len(); + let dummy_args = vec![MoveValue::Bool(false); num_args]; + let (module, function_name) = make_script_function(signature); + let res = call_script_function(module, function_name, serialize_values(&dummy_args)) + .err() + .unwrap(); + // either the dummy arg matches so abort, or it fails to match + // but the important thing is that the signature was accepted + assert!( + res.major_status() == StatusCode::ABORTED + || res.major_status() == StatusCode::FAILED_TO_DESERIALIZE_ARGUMENT + ) + } + + // + // Good signatures + // + for (signature, args) in good_signatures_and_arguments() { + // Body of the script is just an abort, so `ABORTED` means the script was accepted and ran + let expected_status = StatusCode::ABORTED; + let (module, function_name) = make_script_function(signature); + assert_eq!( + call_script_function(module, function_name, serialize_values(&args)) + .err() + .unwrap() + .major_status(), + expected_status + ) + } + + // + // Mismatched Cases + // + for (signature, args, error) in mismatched_cases() { + let (module, function_name) = make_script_function(signature); + assert_eq!( + call_script_function(module, function_name, serialize_values(&args)) + .err() + .unwrap() + .major_status(), + error + ); + } + + for (signature, args, signers, expected_status_opt) in general_cases() { + // Body of the script is just an abort, so `ABORTED` means the script was accepted and ran + let expected_status = expected_status_opt.unwrap_or(StatusCode::ABORTED); + let (module, function_name) = make_script_function(signature); + assert_eq!( + call_script_function_with_args_ty_args_signers( + module, + function_name, + serialize_values(&args), + vec![], + signers + ) + .err() + .unwrap() + .major_status(), + expected_status + ); + } + + // + // Non script visible + // DEPRECATED this check must now be done by the adapter + // + // public + let (module, function_name) = make_module_with_function( + Visibility::Public, + false, + Signature(vec![]), + Signature(vec![]), + vec![], + ); + assert_eq!( + call_script_function_with_args_ty_args_signers( + module, + function_name, + vec![], + vec![], + vec![], + ) + .err() + .unwrap() + .major_status(), + StatusCode::ABORTED, + ); + // private + let (module, function_name) = make_module_with_function( + Visibility::Private, + false, + Signature(vec![]), + Signature(vec![]), + vec![], + ); + assert_eq!( + call_script_function_with_args_ty_args_signers( + module, + function_name, + vec![], + vec![], + vec![], + ) + .err() + .unwrap() + .major_status(), + StatusCode::ABORTED, + ); +} + +#[test] +fn call_missing_item() { + let module = empty_module(); + let id = &module.self_id(); + let function_name = IdentStr::new("foo").unwrap(); + // missing module + let move_vm = MoveVM::new(vec![]).unwrap(); + let mut remote_view = RemoteStore::new(); + let mut session = move_vm.new_session(&remote_view); + let error = session + .execute_function_bypass_visibility( + id, + function_name, + vec![], + Vec::>::new(), + &mut UnmeteredGasMeter, + None, + ) + .err() + .unwrap(); + assert_eq!(error.major_status(), StatusCode::LINKER_ERROR); + assert_eq!(error.status_type(), StatusType::Verification); + drop(session); + + // missing function + remote_view.add_module(module); + let mut session = move_vm.new_session(&remote_view); + let error = session + .execute_function_bypass_visibility( + id, + function_name, + vec![], + Vec::>::new(), + &mut UnmeteredGasMeter, + None, + ) + .err() + .unwrap(); + assert_eq!( + error.major_status(), + StatusCode::FUNCTION_RESOLUTION_FAILURE + ); + assert_eq!(error.status_type(), StatusType::Verification); +} diff --git a/external-crates/move/move-execution/replay_cut/crates/move-vm-types/Cargo.toml b/external-crates/move/move-execution/replay_cut/crates/move-vm-types/Cargo.toml new file mode 100644 index 0000000000000..384d815b19ab2 --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/move-vm-types/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "move-vm-types-replay_cut" +version = "0.1.0" +authors = ["Diem Association "] +description = "Types for Move VM" +repository = "https://github.com/diem/diem" +homepage = "https://diem.com" +license = "Apache-2.0" +publish = false +edition = "2024" + +[dependencies] +proptest = { workspace = true, optional = true } +serde = { workspace = true, features = ["derive", "rc"] } +smallvec.workspace = true + +bcs.workspace = true + +move-core-types.workspace = true +move-binary-format.workspace = true +move-vm-profiler.workspace = true + +[dev-dependencies] +proptest.workspace = true +move-core-types = { workspace = true, features = ["fuzzing"] } + +[features] +default = [] +fuzzing = ["proptest", "move-binary-format/fuzzing"] diff --git a/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/data_store.rs b/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/data_store.rs new file mode 100644 index 0000000000000..534de042b1c78 --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/data_store.rs @@ -0,0 +1,104 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +use move_binary_format::errors::{PartialVMResult, VMResult}; +use move_core_types::{ + account_address::AccountAddress, identifier::IdentStr, language_storage::ModuleId, + resolver::ModuleResolver, +}; +use std::fmt::Debug; + +/// Provide an implementation for bytecodes related to data with a given data store. +/// +/// The `DataStore` is a generic concept that includes both data and events. +/// A default implementation of the `DataStore` is `TransactionDataCache` which provides +/// an in memory cache for a given transaction and the atomic transactional changes +/// proper of a script execution (transaction). +pub trait DataStore { + /// The link context identifies the mapping from runtime `ModuleId`s to the `ModuleId`s in + /// storage that they are loaded from as returned by `relocate`. Implementors of `DataStore` + /// are required to keep the link context stable for the duration of + /// `Interpreter::execute_main`. + fn link_context(&self) -> PartialVMResult; + + /// Translate the runtime `module_id` to the on-chain `ModuleId` that it should be loaded from. + fn relocate(&self, module_id: &ModuleId) -> PartialVMResult; + + /// Translate the runtime fully-qualified struct name to the on-chain `ModuleId` that originally + /// defined that type. + fn defining_module( + &self, + module_id: &ModuleId, + struct_: &IdentStr, + ) -> PartialVMResult; + + /// Get the serialized format of a `CompiledModule` given a `ModuleId`. + fn load_module(&self, module_id: &ModuleId) -> VMResult>; + + /// Publish a module. + fn publish_module(&mut self, module_id: &ModuleId, blob: Vec) -> VMResult<()>; +} + +/// A persistent storage implementation that can resolve both resources and modules +pub trait MoveResolver: + LinkageResolver + ModuleResolver +{ + type Err: Debug; +} + +impl + ModuleResolver + ?Sized> MoveResolver + for T +{ + type Err = E; +} + +/// An execution context that remaps the modules referred to at runtime according to a linkage +/// table, allowing the same module in storage to be run against different dependencies. +/// +/// Default implementation does no re-linking (Module IDs are unchanged by relocation and the +/// link context is a constant value). +pub trait LinkageResolver { + type Error: Debug; + + /// The link context identifies the mapping from runtime `ModuleId`s to the `ModuleId`s in + /// storage that they are loaded from as returned by `relocate`. + fn link_context(&self) -> AccountAddress { + AccountAddress::ZERO + } + + /// Translate the runtime `module_id` to the on-chain `ModuleId` that it should be loaded from. + fn relocate(&self, module_id: &ModuleId) -> Result { + Ok(module_id.clone()) + } + + /// Translate the runtime fully-qualified struct name to the on-chain `ModuleId` that originally + /// defined that type. + fn defining_module( + &self, + module_id: &ModuleId, + _struct: &IdentStr, + ) -> Result { + Ok(module_id.clone()) + } +} + +impl LinkageResolver for &T { + type Error = T::Error; + + fn link_context(&self) -> AccountAddress { + (**self).link_context() + } + + fn relocate(&self, module_id: &ModuleId) -> Result { + (**self).relocate(module_id) + } + + fn defining_module( + &self, + module_id: &ModuleId, + struct_: &IdentStr, + ) -> Result { + (**self).defining_module(module_id, struct_) + } +} diff --git a/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/effects.rs b/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/effects.rs new file mode 100644 index 0000000000000..8009794ae3847 --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/effects.rs @@ -0,0 +1,165 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +use move_core_types::{ + account_address::AccountAddress, identifier::Identifier, language_storage::ModuleId, +}; +use std::collections::btree_map::{self, BTreeMap}; + +/// A storage operation. +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub enum Op { + /// Inserts some new data into an empty slot. + New(T), + /// Modifies some data that currently exists. + Modify(T), + /// Deletes some data that currently exists. + Delete, +} + +impl Op { + pub fn as_ref(&self) -> Op<&T> { + use Op::*; + + match self { + New(data) => New(data), + Modify(data) => Modify(data), + Delete => Delete, + } + } + + pub fn map(self, f: F) -> Op + where + F: FnOnce(T) -> U, + { + use Op::*; + + match self { + New(data) => New(f(data)), + Modify(data) => Modify(f(data)), + Delete => Delete, + } + } + + pub fn ok(self) -> Option { + use Op::*; + + match self { + New(data) | Modify(data) => Some(data), + Delete => None, + } + } +} + +/// A collection of resource and module operations on a Move account. +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub struct AccountChangeSet { + runtime_id: AccountAddress, + modules: BTreeMap>>, +} + +impl AccountChangeSet { + pub fn from_modules( + runtime_id: AccountAddress, + modules: BTreeMap>>, + ) -> Self { + Self { + runtime_id, + modules, + } + } + + pub fn new(runtime_id: AccountAddress) -> Self { + Self { + runtime_id, + modules: BTreeMap::new(), + } + } + + pub fn into_inner(self) -> BTreeMap>> { + self.modules + } + + pub fn into_modules(self) -> BTreeMap>> { + self.modules + } + + pub fn modules(&self) -> &BTreeMap>> { + &self.modules + } + + pub fn is_empty(&self) -> bool { + self.modules.is_empty() + } + + pub fn runtime_id(&self) -> AccountAddress { + self.runtime_id + } +} + +// TODO: ChangeSet does not have a canonical representation so the derived Ord is not sound. + +/// A collection of changes to a Move state. Each AccountChangeSet in the domain of `accounts` +/// is guaranteed to be nonempty +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub struct ChangeSet { + accounts: BTreeMap, +} + +impl Default for ChangeSet { + fn default() -> Self { + Self::new() + } +} + +impl ChangeSet { + pub fn new() -> Self { + Self { + accounts: BTreeMap::new(), + } + } + + pub fn add_account_changeset( + &mut self, + addr: AccountAddress, + account_changeset: AccountChangeSet, + ) { + match self.accounts.entry(addr) { + btree_map::Entry::Occupied(_) => panic!( + "Failed to add account change set. Account {} already exists.", + addr + ), + btree_map::Entry::Vacant(entry) => { + entry.insert(account_changeset); + } + } + } + + pub fn accounts(&self) -> &BTreeMap { + &self.accounts + } + + pub fn into_inner(self) -> BTreeMap { + self.accounts + } + + pub fn into_modules(self) -> impl Iterator>)> { + self.accounts.into_iter().flat_map(|(addr, account)| { + account + .modules + .into_iter() + .map(move |(module_name, blob_opt)| (ModuleId::new(addr, module_name), blob_opt)) + }) + } + + pub fn modules(&self) -> impl Iterator)> { + self.accounts.iter().flat_map(|(addr, account)| { + let addr = *addr; + account + .modules + .iter() + .map(move |(module_name, op)| (addr, module_name, op.as_ref().map(|v| v.as_ref()))) + }) + } +} diff --git a/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/gas.rs b/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/gas.rs new file mode 100644 index 0000000000000..9008ceb8422b9 --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/gas.rs @@ -0,0 +1,364 @@ +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +use crate::views::{TypeView, ValueView}; +use move_binary_format::errors::PartialVMResult; +use move_core_types::{ + gas_algebra::{InternalGas, NumArgs, NumBytes}, + language_storage::ModuleId, +}; + +/// Enum of instructions that do not need extra information for gas metering. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum SimpleInstruction { + Nop, + Ret, + + BrTrue, + BrFalse, + Branch, + + LdU8, + LdU64, + LdU128, + LdTrue, + LdFalse, + + FreezeRef, + MutBorrowLoc, + ImmBorrowLoc, + ImmBorrowField, + MutBorrowField, + ImmBorrowFieldGeneric, + MutBorrowFieldGeneric, + + CastU8, + CastU64, + CastU128, + + Add, + Sub, + Mul, + Mod, + Div, + + BitOr, + BitAnd, + Xor, + Shl, + Shr, + + Or, + And, + Not, + + Lt, + Gt, + Le, + Ge, + + Abort, + + LdU16, + LdU32, + LdU256, + CastU16, + CastU32, + CastU256, +} + +/// Trait that defines a generic gas meter interface, allowing clients of the Move VM to implement +/// their own metering scheme. +pub trait GasMeter { + /// Charge an instruction and fail if not enough gas units are left. + fn charge_simple_instr(&mut self, instr: SimpleInstruction) -> PartialVMResult<()>; + + fn charge_pop(&mut self, popped_val: impl ValueView) -> PartialVMResult<()>; + + fn charge_call( + &mut self, + module_id: &ModuleId, + func_name: &str, + args: impl ExactSizeIterator, + num_locals: NumArgs, + ) -> PartialVMResult<()>; + + fn charge_call_generic( + &mut self, + module_id: &ModuleId, + func_name: &str, + ty_args: impl ExactSizeIterator, + args: impl ExactSizeIterator, + num_locals: NumArgs, + ) -> PartialVMResult<()>; + + fn charge_ld_const(&mut self, size: NumBytes) -> PartialVMResult<()>; + + fn charge_ld_const_after_deserialization(&mut self, val: impl ValueView) + -> PartialVMResult<()>; + + fn charge_copy_loc(&mut self, val: impl ValueView) -> PartialVMResult<()>; + + fn charge_move_loc(&mut self, val: impl ValueView) -> PartialVMResult<()>; + + fn charge_store_loc(&mut self, val: impl ValueView) -> PartialVMResult<()>; + + fn charge_pack( + &mut self, + is_generic: bool, + args: impl ExactSizeIterator, + ) -> PartialVMResult<()>; + + fn charge_unpack( + &mut self, + is_generic: bool, + args: impl ExactSizeIterator, + ) -> PartialVMResult<()>; + + fn charge_variant_switch(&mut self, val: impl ValueView) -> PartialVMResult<()>; + + fn charge_read_ref(&mut self, val: impl ValueView) -> PartialVMResult<()>; + + fn charge_write_ref( + &mut self, + new_val: impl ValueView, + old_val: impl ValueView, + ) -> PartialVMResult<()>; + + fn charge_eq(&mut self, lhs: impl ValueView, rhs: impl ValueView) -> PartialVMResult<()>; + + fn charge_neq(&mut self, lhs: impl ValueView, rhs: impl ValueView) -> PartialVMResult<()>; + + fn charge_vec_pack<'a>( + &mut self, + ty: impl TypeView + 'a, + args: impl ExactSizeIterator, + ) -> PartialVMResult<()>; + + fn charge_vec_len(&mut self, ty: impl TypeView) -> PartialVMResult<()>; + + fn charge_vec_borrow( + &mut self, + is_mut: bool, + ty: impl TypeView, + is_success: bool, + ) -> PartialVMResult<()>; + + fn charge_vec_push_back( + &mut self, + ty: impl TypeView, + val: impl ValueView, + ) -> PartialVMResult<()>; + + fn charge_vec_pop_back( + &mut self, + ty: impl TypeView, + val: Option, + ) -> PartialVMResult<()>; + + // TODO(Gas): Expose the elements + fn charge_vec_unpack( + &mut self, + ty: impl TypeView, + expect_num_elements: NumArgs, + elems: impl ExactSizeIterator, + ) -> PartialVMResult<()>; + + // TODO(Gas): Expose the two elements + fn charge_vec_swap(&mut self, ty: impl TypeView) -> PartialVMResult<()>; + + fn charge_native_function( + &mut self, + amount: InternalGas, + ret_vals: Option>, + ) -> PartialVMResult<()>; + + fn charge_native_function_before_execution( + &mut self, + ty_args: impl ExactSizeIterator, + args: impl ExactSizeIterator, + ) -> PartialVMResult<()>; + + fn charge_drop_frame( + &mut self, + locals: impl Iterator, + ) -> PartialVMResult<()>; + + /// Returns the gas left + fn remaining_gas(&self) -> InternalGas; +} + +/// A dummy gas meter that does not meter anything. +/// Charge operations will always succeed. +pub struct UnmeteredGasMeter; + +impl GasMeter for UnmeteredGasMeter { + fn charge_simple_instr(&mut self, _instr: SimpleInstruction) -> PartialVMResult<()> { + Ok(()) + } + + fn charge_pop(&mut self, _popped_val: impl ValueView) -> PartialVMResult<()> { + Ok(()) + } + + fn charge_call( + &mut self, + _module_id: &ModuleId, + _func_name: &str, + _args: impl IntoIterator, + _num_locals: NumArgs, + ) -> PartialVMResult<()> { + Ok(()) + } + + fn charge_call_generic( + &mut self, + _module_id: &ModuleId, + _func_name: &str, + _ty_args: impl ExactSizeIterator, + _args: impl ExactSizeIterator, + _num_locals: NumArgs, + ) -> PartialVMResult<()> { + Ok(()) + } + + fn charge_ld_const(&mut self, _size: NumBytes) -> PartialVMResult<()> { + Ok(()) + } + + fn charge_ld_const_after_deserialization( + &mut self, + _val: impl ValueView, + ) -> PartialVMResult<()> { + Ok(()) + } + + fn charge_copy_loc(&mut self, _val: impl ValueView) -> PartialVMResult<()> { + Ok(()) + } + + fn charge_move_loc(&mut self, _val: impl ValueView) -> PartialVMResult<()> { + Ok(()) + } + + fn charge_store_loc(&mut self, _val: impl ValueView) -> PartialVMResult<()> { + Ok(()) + } + + fn charge_pack( + &mut self, + _is_generic: bool, + _args: impl ExactSizeIterator, + ) -> PartialVMResult<()> { + Ok(()) + } + + fn charge_unpack( + &mut self, + _is_generic: bool, + _args: impl ExactSizeIterator, + ) -> PartialVMResult<()> { + Ok(()) + } + + fn charge_variant_switch(&mut self, _val: impl ValueView) -> PartialVMResult<()> { + Ok(()) + } + + fn charge_read_ref(&mut self, _val: impl ValueView) -> PartialVMResult<()> { + Ok(()) + } + + fn charge_write_ref( + &mut self, + _new_val: impl ValueView, + _old_val: impl ValueView, + ) -> PartialVMResult<()> { + Ok(()) + } + + fn charge_eq(&mut self, _lhs: impl ValueView, _rhs: impl ValueView) -> PartialVMResult<()> { + Ok(()) + } + + fn charge_neq(&mut self, _lhs: impl ValueView, _rhs: impl ValueView) -> PartialVMResult<()> { + Ok(()) + } + + fn charge_vec_pack<'a>( + &mut self, + _ty: impl TypeView + 'a, + _args: impl ExactSizeIterator, + ) -> PartialVMResult<()> { + Ok(()) + } + + fn charge_vec_len(&mut self, _ty: impl TypeView) -> PartialVMResult<()> { + Ok(()) + } + + fn charge_vec_borrow( + &mut self, + _is_mut: bool, + _ty: impl TypeView, + _is_success: bool, + ) -> PartialVMResult<()> { + Ok(()) + } + + fn charge_vec_push_back( + &mut self, + _ty: impl TypeView, + _val: impl ValueView, + ) -> PartialVMResult<()> { + Ok(()) + } + + fn charge_vec_pop_back( + &mut self, + _ty: impl TypeView, + _val: Option, + ) -> PartialVMResult<()> { + Ok(()) + } + + fn charge_vec_unpack( + &mut self, + _ty: impl TypeView, + _expect_num_elements: NumArgs, + _elems: impl ExactSizeIterator, + ) -> PartialVMResult<()> { + Ok(()) + } + + fn charge_vec_swap(&mut self, _ty: impl TypeView) -> PartialVMResult<()> { + Ok(()) + } + + fn charge_native_function( + &mut self, + _amount: InternalGas, + _ret_vals: Option>, + ) -> PartialVMResult<()> { + Ok(()) + } + + fn charge_native_function_before_execution( + &mut self, + _ty_args: impl ExactSizeIterator, + _args: impl ExactSizeIterator, + ) -> PartialVMResult<()> { + Ok(()) + } + + fn charge_drop_frame( + &mut self, + _locals: impl Iterator, + ) -> PartialVMResult<()> { + Ok(()) + } + + fn remaining_gas(&self) -> InternalGas { + InternalGas::new(u64::MAX) + } +} diff --git a/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/lib.rs b/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/lib.rs new file mode 100644 index 0000000000000..d7943418c378a --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/lib.rs @@ -0,0 +1,34 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +#![forbid(unsafe_code)] + +macro_rules! debug_write { + ($($toks: tt)*) => { + write!($($toks)*).map_err(|_| + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("failed to write to buffer".to_string()) + ) + }; +} + +macro_rules! debug_writeln { + ($($toks: tt)*) => { + writeln!($($toks)*).map_err(|_| + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("failed to write to buffer".to_string()) + ) + }; +} + +pub mod data_store; +pub mod effects; +pub mod gas; +pub mod loaded_data; +pub mod natives; +pub mod values; +pub mod views; + +#[cfg(test)] +mod unit_tests; diff --git a/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/loaded_data/mod.rs b/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/loaded_data/mod.rs new file mode 100644 index 0000000000000..f9c2369eaf477 --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/loaded_data/mod.rs @@ -0,0 +1,8 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 +//! Loaded definition of code data used in runtime. +//! +//! This module contains the loaded definition of code data used in runtime. + +pub mod runtime_types; diff --git a/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/loaded_data/runtime_types.rs b/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/loaded_data/runtime_types.rs new file mode 100644 index 0000000000000..b524d7421e1fa --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/loaded_data/runtime_types.rs @@ -0,0 +1,385 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +use move_binary_format::{ + errors::{PartialVMError, PartialVMResult}, + file_format::{ + AbilitySet, DatatypeTyParameter, EnumDefinitionIndex, SignatureToken, + StructDefinitionIndex, TypeParameterIndex, VariantTag, + }, +}; +use move_core_types::{ + gas_algebra::AbstractMemorySize, identifier::Identifier, language_storage::ModuleId, + vm_status::StatusCode, +}; +use std::fmt::Debug; +use std::{cmp::max, collections::BTreeMap}; + +pub const TYPE_DEPTH_MAX: usize = 256; + +#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug)] +/// A formula for the maximum depth of the value for a type +/// max(Ti + Ci, ..., CBase) +pub struct DepthFormula { + /// The terms for each type parameter, if present. + /// Ti + Ci + pub terms: Vec<(TypeParameterIndex, u64)>, + /// The depth for any non type parameter term, if one exists. + /// CBase + pub constant: Option, +} + +impl DepthFormula { + /// A value with no type parameters + pub fn constant(constant: u64) -> Self { + Self { + terms: vec![], + constant: Some(constant), + } + } + + /// A stand alone type parameter value + pub fn type_parameter(tparam: TypeParameterIndex) -> Self { + Self { + terms: vec![(tparam, 0)], + constant: None, + } + } + + /// We `max` over a list of formulas, and we normalize it to deal with duplicate terms, e.g. + /// `max(max(t1 + 1, t2 + 2, 2), max(t1 + 3, t2 + 1, 4))` becomes + /// `max(t1 + 3, t2 + 2, 4)` + pub fn normalize(formulas: Vec) -> Self { + let mut var_map = BTreeMap::new(); + let mut constant_acc = None; + for formula in formulas { + let Self { terms, constant } = formula; + for (var, cur_factor) in terms { + var_map + .entry(var) + .and_modify(|prev_factor| *prev_factor = max(cur_factor, *prev_factor)) + .or_insert(cur_factor); + } + match (constant_acc, constant) { + (_, None) => (), + (None, Some(_)) => constant_acc = constant, + (Some(c1), Some(c2)) => constant_acc = Some(max(c1, c2)), + } + } + Self { + terms: var_map.into_iter().collect(), + constant: constant_acc, + } + } + + /// Substitute in formulas for each type parameter and normalize the final formula + pub fn subst( + &self, + mut map: BTreeMap, + ) -> PartialVMResult { + let Self { terms, constant } = self; + let mut formulas = vec![]; + if let Some(constant) = constant { + formulas.push(DepthFormula::constant(*constant)) + } + for (t_i, c_i) in terms { + let Some(mut u_form) = map.remove(t_i) else { + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message(format!("{t_i:?} missing mapping")), + ); + }; + u_form.add(*c_i); + formulas.push(u_form) + } + Ok(DepthFormula::normalize(formulas)) + } + + /// Given depths for each type parameter, solve the formula giving the max depth for the type + pub fn solve(&self, tparam_depths: &[u64]) -> PartialVMResult { + let Self { terms, constant } = self; + let mut depth = constant.as_ref().copied().unwrap_or(0); + for (t_i, c_i) in terms { + match tparam_depths.get(*t_i as usize) { + None => { + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message(format!("{t_i:?} missing mapping")), + ); + } + Some(ty_depth) => depth = max(depth, ty_depth.saturating_add(*c_i)), + } + } + Ok(depth) + } + + // `max(t_0 + c_0, ..., t_n + c_n, c_base) + c`. But our representation forces us to distribute + // the addition, so it becomes `max(t_0 + c_0 + c, ..., t_n + c_n + c, c_base + c)` + pub fn add(&mut self, c: u64) { + let Self { terms, constant } = self; + for (_t_i, c_i) in terms { + *c_i = (*c_i).saturating_add(c); + } + if let Some(cbase) = constant.as_mut() { + *cbase = (*cbase).saturating_add(c); + } + } +} + +#[derive(Debug, Clone, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct CachedDatatype { + pub abilities: AbilitySet, + pub type_parameters: Vec, + pub name: Identifier, + pub defining_id: ModuleId, + pub runtime_id: ModuleId, + pub depth: Option, + pub datatype_info: Datatype, +} + +#[derive(Debug, Clone, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum Datatype { + Enum(EnumType), + Struct(StructType), +} + +#[derive(Debug, Clone, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct EnumType { + pub variants: Vec, + pub enum_def: EnumDefinitionIndex, +} + +#[derive(Debug, Clone, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct VariantType { + pub variant_name: Identifier, + pub fields: Vec, + pub field_names: Vec, + pub enum_def: EnumDefinitionIndex, + pub variant_tag: VariantTag, +} + +#[derive(Debug, Clone, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct StructType { + pub fields: Vec, + pub field_names: Vec, + pub struct_def: StructDefinitionIndex, +} + +impl CachedDatatype { + pub fn get_struct(&self) -> PartialVMResult<&StructType> { + match &self.datatype_info { + Datatype::Struct(struct_type) => Ok(struct_type), + x @ Datatype::Enum(_) => Err(PartialVMError::new( + StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR, + ) + .with_message(format!("Expected struct type but got {:?}", x))), + } + } + + pub fn get_enum(&self) -> PartialVMResult<&EnumType> { + match &self.datatype_info { + Datatype::Enum(enum_type) => Ok(enum_type), + x @ Datatype::Struct(_) => Err(PartialVMError::new( + StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR, + ) + .with_message(format!("Expected enum type but got {:?}", x))), + } + } +} + +impl CachedDatatype { + pub fn type_param_constraints(&self) -> impl ExactSizeIterator { + self.type_parameters.iter().map(|param| ¶m.constraints) + } +} + +#[derive(Debug, Copy, Clone, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct CachedTypeIndex(pub usize); + +#[derive(Debug, Clone, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum Type { + Bool, + U8, + U64, + U128, + Address, + Signer, + Vector(Box), + Datatype(CachedTypeIndex), + DatatypeInstantiation(Box<(CachedTypeIndex, Vec)>), + Reference(Box), + MutableReference(Box), + TyParam(u16), + U16, + U32, + U256, +} + +impl Type { + fn clone_impl(&self, depth: usize) -> PartialVMResult { + self.apply_subst(|idx, _| Ok(Type::TyParam(idx)), depth) + } + + fn apply_subst(&self, subst: F, depth: usize) -> PartialVMResult + where + F: Fn(u16, usize) -> PartialVMResult + Copy, + { + if depth > TYPE_DEPTH_MAX { + return Err(PartialVMError::new(StatusCode::VM_MAX_TYPE_DEPTH_REACHED)); + } + let res = match self { + Type::TyParam(idx) => subst(*idx, depth)?, + Type::Bool => Type::Bool, + Type::U8 => Type::U8, + Type::U16 => Type::U16, + Type::U32 => Type::U32, + Type::U64 => Type::U64, + Type::U128 => Type::U128, + Type::U256 => Type::U256, + Type::Address => Type::Address, + Type::Signer => Type::Signer, + Type::Vector(ty) => Type::Vector(Box::new(ty.apply_subst(subst, depth + 1)?)), + Type::Reference(ty) => Type::Reference(Box::new(ty.apply_subst(subst, depth + 1)?)), + Type::MutableReference(ty) => { + Type::MutableReference(Box::new(ty.apply_subst(subst, depth + 1)?)) + } + Type::Datatype(def_idx) => Type::Datatype(*def_idx), + Type::DatatypeInstantiation(def_inst) => { + let (def_idx, instantiation) = &**def_inst; + let mut inst = vec![]; + for ty in instantiation { + inst.push(ty.apply_subst(subst, depth + 1)?) + } + Type::DatatypeInstantiation(Box::new((*def_idx, inst))) + } + }; + Ok(res) + } + + pub fn subst(&self, ty_args: &[Type]) -> PartialVMResult { + self.apply_subst( + |idx, depth| match ty_args.get(idx as usize) { + Some(ty) => ty.clone_impl(depth), + None => Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message(format!( + "type substitution failed: index out of bounds -- len {} got {}", + ty_args.len(), + idx + )), + ), + }, + 1, + ) + } + + #[allow(deprecated)] + const LEGACY_BASE_MEMORY_SIZE: AbstractMemorySize = AbstractMemorySize::new(1); + + /// Returns the abstract memory size the data structure occupies. + /// + /// This kept only for legacy reasons. + /// New applications should not use this. + pub fn size(&self) -> AbstractMemorySize { + use Type::*; + + match self { + TyParam(_) | Bool | U8 | U16 | U32 | U64 | U128 | U256 | Address | Signer => { + Self::LEGACY_BASE_MEMORY_SIZE + } + Vector(ty) | Reference(ty) | MutableReference(ty) => { + Self::LEGACY_BASE_MEMORY_SIZE + ty.size() + } + Datatype(_) => Self::LEGACY_BASE_MEMORY_SIZE, + DatatypeInstantiation(inst) => { + let (_, tys) = &**inst; + tys.iter() + .fold(Self::LEGACY_BASE_MEMORY_SIZE, |acc, ty| acc + ty.size()) + } + } + } + + pub fn from_const_signature(constant_signature: &SignatureToken) -> PartialVMResult { + use SignatureToken as S; + use Type as L; + + Ok(match constant_signature { + S::Bool => L::Bool, + S::U8 => L::U8, + S::U16 => L::U16, + S::U32 => L::U32, + S::U64 => L::U64, + S::U128 => L::U128, + S::U256 => L::U256, + S::Address => L::Address, + S::Vector(inner) => L::Vector(Box::new(Self::from_const_signature(inner)?)), + // Not yet supported + S::Datatype(_) | S::DatatypeInstantiation(_) => { + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("Unable to load const type signature".to_string()), + ); + } + // Not allowed/Not meaningful + S::TypeParameter(_) | S::Reference(_) | S::MutableReference(_) | S::Signer => { + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("Unable to load const type signature".to_string()), + ); + } + }) + } + + pub fn check_vec_ref(&self, inner_ty: &Type, is_mut: bool) -> PartialVMResult { + match self { + Type::MutableReference(inner) => match &**inner { + Type::Vector(inner) => { + inner.check_eq(inner_ty)?; + Ok(inner.as_ref().clone()) + } + _ => Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("VecMutBorrow expects a vector reference".to_string()), + ), + }, + Type::Reference(inner) if !is_mut => match &**inner { + Type::Vector(inner) => { + inner.check_eq(inner_ty)?; + Ok(inner.as_ref().clone()) + } + _ => Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("VecMutBorrow expects a vector reference".to_string()), + ), + }, + _ => Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("VecMutBorrow expects a vector reference".to_string()), + ), + } + } + + pub fn check_eq(&self, other: &Self) -> PartialVMResult<()> { + if self != other { + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR).with_message( + format!("Type mismatch: expected {:?}, got {:?}", self, other), + ), + ); + } + Ok(()) + } + + pub fn check_ref_eq(&self, expected_inner: &Self) -> PartialVMResult<()> { + match self { + Type::MutableReference(inner) | Type::Reference(inner) => { + inner.check_eq(expected_inner) + } + _ => Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("VecMutBorrow expects a vector reference".to_string()), + ), + } + } +} diff --git a/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/natives/function.rs b/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/natives/function.rs new file mode 100644 index 0000000000000..1409c87c7c72d --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/natives/function.rs @@ -0,0 +1,123 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +//! Native Function Support +//! +//! All Move native functions have the following signature: +//! +//! `pub fn native_function( +//! context: &mut impl NativeContext, +//! ty_args: Vec, +//! mut arguments: VecDeque, +//! ) -> PartialVMResult;` +//! +//! arguments are passed with first argument at position 0 and so forth. +//! Popping values from `arguments` gives the aguments in reverse order (last first). +//! This module contains the declarations and utilities to implement a native +//! function. + +use crate::values::Value; +use smallvec::{SmallVec, smallvec}; + +pub use move_binary_format::errors::{PartialVMError, PartialVMResult}; +pub use move_core_types::{gas_algebra::InternalGas, vm_status::StatusCode}; + +/// Result of a native function execution requires charges for execution cost. +/// +/// An execution that causes an invariant violation would not return a `NativeResult` but +/// return a `PartialVMError` error directly. +/// All native functions must return a `PartialVMResult` where an `Err` is returned +/// when an error condition is met that should not charge for the execution. A common example +/// is a VM invariant violation which should have been forbidden by the verifier. +/// Errors (typically user errors and aborts) that are logically part of the function execution +/// must be expressed in a `NativeResult` with a cost and a VMStatus. +pub struct NativeResult { + /// Result of execution. This is either the return values or the error to report. + pub cost: InternalGas, + pub result: Result, u64>, +} + +impl NativeResult { + /// Return values of a successful execution. + pub fn ok(cost: InternalGas, values: SmallVec<[Value; 1]>) -> Self { + NativeResult { + cost, + result: Ok(values), + } + } + + /// Failed execution. The failure is a runtime failure in the function and not an invariant + /// failure of the VM which would raise a `PartialVMError` error directly. + /// The only thing the function can specify is its abort code, as if it had invoked the `Abort` + /// bytecode instruction + pub fn err(cost: InternalGas, abort_code: u64) -> Self { + NativeResult { + cost, + result: Err(abort_code), + } + } + + /// Convert a PartialVMResult<()> into a PartialVMResult + pub fn map_partial_vm_result_empty( + cost: InternalGas, + res: PartialVMResult<()>, + ) -> PartialVMResult { + let result = match res { + Ok(_) => NativeResult::ok(cost, smallvec![]), + Err(err) if err.major_status() == StatusCode::ABORTED => { + let (_, abort_code, _, _, _, _) = err.all_data(); + NativeResult::err( + cost, + abort_code.unwrap_or(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR as u64), + ) + } + Err(err) => { + return Err(err); + } + }; + Ok(result) + } + + /// Convert a PartialVMResult into a PartialVMResult + pub fn map_partial_vm_result_one( + cost: InternalGas, + res: PartialVMResult, + ) -> PartialVMResult { + let result = match res { + Ok(val) => NativeResult::ok(cost, smallvec![val]), + Err(err) if err.major_status() == StatusCode::ABORTED => { + let (_, abort_code, _, _, _, _) = err.all_data(); + NativeResult::err( + cost, + abort_code.unwrap_or(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR as u64), + ) + } + Err(err) => { + return Err(err); + } + }; + Ok(result) + } +} + +/// Return the argument at the top of the stack. +/// +/// Arguments are passed to a native as a stack with first arg at the bottom of the stack. +/// Calling this API can help in making the code more readable. +/// It's good practice to pop all arguments in locals of the native function on function entry. +#[macro_export] +macro_rules! pop_arg { + ($arguments:ident, $t:ty) => {{ + use $crate::natives::function::{NativeResult, PartialVMError, StatusCode}; + match $arguments.pop_back().map(|v| v.value_as::<$t>()) { + None => { + return Err(PartialVMError::new( + StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR, + )); + } + Some(Err(e)) => return Err(e), + Some(Ok(v)) => v, + } + }}; +} diff --git a/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/natives/mod.rs b/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/natives/mod.rs new file mode 100644 index 0000000000000..ccaf1d72a1fab --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/natives/mod.rs @@ -0,0 +1,5 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +pub mod function; diff --git a/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/unit_tests/identifier_prop_tests.rs b/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/unit_tests/identifier_prop_tests.rs new file mode 100644 index 0000000000000..61fa67e2aaf98 --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/unit_tests/identifier_prop_tests.rs @@ -0,0 +1,18 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +use move_binary_format::file_format::CompiledModule; +use proptest::prelude::*; + +proptest! { + #[test] + fn identifier_serializer_roundtrip(module in CompiledModule::valid_strategy(20)) { + let module_id = module.self_id(); + let deserialized_module_id = { + let serialized_key = bcs::to_bytes(&module_id).unwrap(); + bcs::from_bytes(&serialized_key).expect("Deserialize should work") + }; + prop_assert_eq!(module_id, deserialized_module_id); + } +} diff --git a/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/unit_tests/mod.rs b/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/unit_tests/mod.rs new file mode 100644 index 0000000000000..bb36fe00ca57f --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/unit_tests/mod.rs @@ -0,0 +1,6 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(feature = "fuzzing")] +mod identifier_prop_tests; diff --git a/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/values/mod.rs b/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/values/mod.rs new file mode 100644 index 0000000000000..fb0b7be7efc1a --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/values/mod.rs @@ -0,0 +1,13 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +pub mod values_impl; + +#[cfg(test)] +mod value_tests; + +#[cfg(any(test, feature = "fuzzing"))] +mod value_prop_tests; + +pub use values_impl::*; diff --git a/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/values/value_prop_tests.rs b/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/values/value_prop_tests.rs new file mode 100644 index 0000000000000..20fbd555dcfce --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/values/value_prop_tests.rs @@ -0,0 +1,25 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +use crate::values::{Value, prop::layout_and_value_strategy}; +use move_core_types::runtime_value::MoveValue; +use proptest::prelude::*; + +proptest! { + #[test] + fn serializer_round_trip((layout, value) in layout_and_value_strategy()) { + let blob = value.typed_serialize(&layout).expect("must serialize"); + + let value_deserialized = Value::simple_deserialize(&blob, &layout).expect("must deserialize"); + assert!(value.equals(&value_deserialized).unwrap()); + + let move_value = value.as_move_value(&layout); + + let blob2 = move_value.simple_serialize().expect("must serialize"); + assert_eq!(blob, blob2); + + let move_value_deserialized = MoveValue::simple_deserialize(&blob2, &layout).expect("must deserialize."); + assert_eq!(move_value, move_value_deserialized); + } +} diff --git a/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/values/value_tests.rs b/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/values/value_tests.rs new file mode 100644 index 0000000000000..c5e58a6d1218c --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/values/value_tests.rs @@ -0,0 +1,254 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +use crate::{loaded_data::runtime_types::Type, values::*, views::*}; +use move_binary_format::errors::*; +use move_core_types::{account_address::AccountAddress, runtime_value, u256::U256}; + +#[cfg(test)] +const SIZE_CONFIG: SizeConfig = SizeConfig { + traverse_references: false, + include_vector_size: false, +}; + +#[test] +fn locals() -> PartialVMResult<()> { + const LEN: usize = 4; + let mut locals = Locals::new(LEN); + for i in 0..LEN { + assert!(locals.copy_loc(i).is_err()); + assert!(locals.move_loc(i, true).is_err()); + assert!(locals.borrow_loc(i).is_err()); + } + locals.store_loc(1, Value::u64(42), true)?; + + assert!(locals.copy_loc(1)?.equals(&Value::u64(42))?); + let r = locals.borrow_loc(1)?.value_as::()?; + assert!(r.read_ref()?.equals(&Value::u64(42))?); + assert!(locals.move_loc(1, true)?.equals(&Value::u64(42))?); + + assert!(locals.copy_loc(1).is_err()); + assert!(locals.move_loc(1, true).is_err()); + assert!(locals.borrow_loc(1).is_err()); + + assert!(locals.copy_loc(LEN + 1).is_err()); + assert!(locals.move_loc(LEN + 1, true).is_err()); + assert!(locals.borrow_loc(LEN + 1).is_err()); + + Ok(()) +} + +#[test] +fn struct_pack_and_unpack() -> PartialVMResult<()> { + let vals = [ + Value::u8(10), + Value::u16(12), + Value::u32(15), + Value::u64(20), + Value::u128(30), + Value::u256(U256::max_value()), + ]; + let s = Struct::pack([ + Value::u8(10), + Value::u16(12), + Value::u32(15), + Value::u64(20), + Value::u128(30), + Value::u256(U256::max_value()), + ]); + let unpacked: Vec<_> = s.unpack()?.collect(); + + assert!(vals.len() == unpacked.len()); + for (v1, v2) in vals.iter().zip(unpacked.iter()) { + assert!(v1.equals(v2)?); + } + + Ok(()) +} + +#[test] +fn struct_borrow_field() -> PartialVMResult<()> { + let mut locals = Locals::new(1); + locals.store_loc( + 0, + Value::struct_(Struct::pack(vec![Value::u8(10), Value::bool(false)])), + true, + )?; + let r: StructRef = locals.borrow_loc(0)?.value_as()?; + + { + let f: Reference = r.borrow_field(1)?.value_as()?; + assert!(f.read_ref()?.equals(&Value::bool(false))?); + } + + { + let f: Reference = r.borrow_field(1)?.value_as()?; + f.write_ref(Value::bool(true))?; + } + + { + let f: Reference = r.borrow_field(1)?.value_as()?; + assert!(f.read_ref()?.equals(&Value::bool(true))?); + } + + Ok(()) +} + +#[test] +fn struct_borrow_nested() -> PartialVMResult<()> { + let mut locals = Locals::new(1); + + fn inner(x: u64) -> Value { + Value::struct_(Struct::pack(vec![Value::u64(x)])) + } + fn outer(x: u64) -> Value { + Value::struct_(Struct::pack(vec![Value::u8(10), inner(x)])) + } + + locals.store_loc(0, outer(20), true)?; + let r1: StructRef = locals.borrow_loc(0)?.value_as()?; + let r2: StructRef = r1.borrow_field(1)?.value_as()?; + + { + let r3: Reference = r2.borrow_field(0)?.value_as()?; + assert!(r3.read_ref()?.equals(&Value::u64(20))?); + } + + { + let r3: Reference = r2.borrow_field(0)?.value_as()?; + r3.write_ref(Value::u64(30))?; + } + + { + let r3: Reference = r2.borrow_field(0)?.value_as()?; + assert!(r3.read_ref()?.equals(&Value::u64(30))?); + } + + assert!(r2.read_ref()?.equals(&inner(30))?); + assert!(r1.read_ref()?.equals(&outer(30))?); + + Ok(()) +} + +#[test] +fn global_value_non_struct() -> PartialVMResult<()> { + assert!(GlobalValue::cached(Value::u64(100)).is_err()); + assert!(GlobalValue::cached(Value::bool(false)).is_err()); + + let mut locals = Locals::new(1); + locals.store_loc(0, Value::u8(0), true)?; + let r = locals.borrow_loc(0)?; + assert!(GlobalValue::cached(r).is_err()); + + Ok(()) +} + +#[test] +fn leagacy_ref_abstract_memory_size_consistency() -> PartialVMResult<()> { + let mut locals = Locals::new(10); + + locals.store_loc(0, Value::u128(0), true)?; + let r = locals.borrow_loc(0)?; + assert_eq!(r.abstract_memory_size(&SIZE_CONFIG), r.legacy_size()); + + locals.store_loc(1, Value::vector_u8([1, 2, 3]), true)?; + let r = locals.borrow_loc(1)?; + assert_eq!(r.abstract_memory_size(&SIZE_CONFIG), r.legacy_size()); + + let r: VectorRef = r.value_as()?; + let r = r.borrow_elem(0, &Type::U8)?; + assert_eq!(r.abstract_memory_size(&SIZE_CONFIG), r.legacy_size()); + + locals.store_loc(2, Value::struct_(Struct::pack([])), true)?; + let r: Reference = locals.borrow_loc(2)?.value_as()?; + assert_eq!(r.abstract_memory_size(&SIZE_CONFIG), r.legacy_size()); + + Ok(()) +} + +#[test] +fn legacy_struct_abstract_memory_size_consistenty() -> PartialVMResult<()> { + let structs = [ + Struct::pack([]), + Struct::pack([Value::struct_(Struct::pack([Value::u8(0), Value::u64(0)]))]), + ]; + + for s in &structs { + assert_eq!(s.abstract_memory_size(&SIZE_CONFIG), s.legacy_size()); + } + + Ok(()) +} + +#[test] +fn legacy_val_abstract_memory_size_consistency() -> PartialVMResult<()> { + let vals = [ + Value::u8(0), + Value::u16(0), + Value::u32(0), + Value::u64(0), + Value::u128(0), + Value::u256(U256::zero()), + Value::bool(true), + Value::address(AccountAddress::ZERO), + Value::vector_u8([0, 1, 2]), + Value::vector_u16([0, 1, 2]), + Value::vector_u32([0, 1, 2]), + Value::vector_u64([]), + Value::vector_u128([1, 2, 3, 4]), + Value::vector_u256([1, 2, 3, 4].iter().map(|q| U256::from(*q as u64))), + Value::struct_(Struct::pack([])), + Value::struct_(Struct::pack([Value::u8(0), Value::bool(false)])), + Vector::pack(VectorSpecialization::Container, [])?, + Vector::pack(VectorSpecialization::U8, [Value::u8(0), Value::u8(1)])?, + ]; + + let mut locals = Locals::new(vals.len()); + for (idx, val) in vals.iter().enumerate() { + locals.store_loc(idx, val.copy_value()?, true)?; + + let val_size_new = val.abstract_memory_size(&SIZE_CONFIG); + let val_size_old = val.legacy_size(); + + assert_eq!(val_size_new, val_size_old); + + let val_size_through_ref = locals + .borrow_loc(idx)? + .value_as::()? + .value_view() + .abstract_memory_size(&SIZE_CONFIG); + + assert_eq!(val_size_through_ref, val_size_old) + } + + Ok(()) +} + +#[test] +fn test_vm_value_vector_u64_casting() { + assert_eq!( + vec![1, 2, 3], + Value::vector_u64([1, 2, 3]).value_as::>().unwrap() + ); +} + +#[test] +fn assert_sizes() { + assert_eq!(size_of::(), 16); +} + +#[test] +fn signer_equivalence() -> PartialVMResult<()> { + let addr = AccountAddress::TWO; + let signer = Value::signer(addr); + + assert_eq!( + signer.typed_serialize(&runtime_value::MoveTypeLayout::Signer), + signer.typed_serialize(&runtime_value::MoveTypeLayout::Struct(Box::new( + runtime_value::MoveStructLayout(Box::new(vec![runtime_value::MoveTypeLayout::Address])) + ))) + ); + + Ok(()) +} diff --git a/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/values/values_impl.rs b/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/values/values_impl.rs new file mode 100644 index 0000000000000..e19ca908ddc48 --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/values/values_impl.rs @@ -0,0 +1,4465 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + effects::Op, + loaded_data::runtime_types::Type, + views::{ValueView, ValueVisitor}, +}; +use move_binary_format::{ + errors::*, + file_format::{Constant, SignatureToken, VariantTag}, +}; +use move_core_types::{VARIANT_TAG_MAX_VALUE, annotated_value as A}; +use move_core_types::{ + account_address::AccountAddress, + gas_algebra::AbstractMemorySize, + runtime_value::{MoveEnumLayout, MoveStructLayout, MoveTypeLayout}, + u256, + vm_status::{StatusCode, sub_status::NFE_VECTOR_ERROR_BASE}, +}; +use std::{ + cell::RefCell, + fmt::{self, Debug, Display}, + iter, + ops::Add, + rc::Rc, +}; + +/*************************************************************************************** + * + * Internal Types + * + * Internal representation of the Move value calculus. These types are abstractions + * over the concrete Move concepts and may carry additional information that is not + * defined by the language, but required by the implementation. + * + **************************************************************************************/ + +/// Runtime representation of a Move value. +#[derive(Debug)] +enum ValueImpl { + Invalid, + + U8(u8), + U16(u16), + U32(u32), + U64(u64), + U128(Box), + U256(Box), + Bool(bool), + Address(Box), + + Container(Container), + + ContainerRef(Box), + IndexedRef(Box), +} + +/// A container is a collection of values. It is used to represent data structures like a +/// Move vector or struct. +/// +/// There is one general container that can be used to store an array of any values, same +/// type or not, and a few specialized flavors to offer compact memory layout for small +/// primitive types. +/// +/// Except when not owned by the VM stack, a container always lives inside an Rc>, +/// making it possible to be shared by references. +#[derive(Debug, Clone)] +enum Container { + Locals(Rc>>), + Vec(Rc>>), + Struct(Rc>>), + VecU8(Rc>>), + VecU64(Rc>>), + VecU128(Rc>>), + VecBool(Rc>>), + VecAddress(Rc>>), + VecU16(Rc>>), + VecU32(Rc>>), + VecU256(Rc>>), + Variant(Rc)>>), +} + +/// A ContainerRef is a direct reference to a container, which could live either in the frame +/// or in global storage. In the latter case, it also keeps a status flag indicating whether +/// the container has been possibly modified. +#[derive(Debug)] +enum ContainerRef { + Local(Container), + Global { + status: Rc>, + container: Container, + }, +} + +/// Status for global (on-chain) data: +/// Clean - the data was only read. +/// Dirty - the data was possibly modified. +#[derive(Debug, Clone, Copy)] +enum GlobalDataStatus { + Clean, + Dirty, +} + +/// A Move reference pointing to an element in a container. +#[derive(Debug)] +struct IndexedRef { + idx: usize, + container_ref: ContainerRef, +} + +/// An umbrella enum for references. It is used to hide the internals of the public type +/// Reference. +#[derive(Debug)] +enum ReferenceImpl { + IndexedRef(IndexedRef), + ContainerRef(ContainerRef), +} + +/*************************************************************************************** + * + * Public Types + * + * Types visible from outside the module. They are almost exclusively wrappers around + * the internal representation, acting as public interfaces. The methods they provide + * closely resemble the Move concepts their names suggest: move_local, borrow_field, + * pack, unpack, etc. + * + * They are opaque to an external caller by design -- no knowledge about the internal + * representation is given and they can only be manipulated via the public methods, + * which is to ensure no arbitrary invalid states can be created unless some crucial + * internal invariants are violated. + * + **************************************************************************************/ +/// A Move value -- a wrapper around `ValueImpl` which can be created only through valid +/// means. +#[derive(Debug)] +pub struct Value(ValueImpl); + +/// An integer value in Move. +#[derive(Debug)] +pub enum IntegerValue { + U8(u8), + U16(u16), + U32(u32), + U64(u64), + U128(u128), + U256(u256::U256), +} + +/// A Move struct. +#[derive(Debug)] +pub struct Struct { + fields: Vec, +} + +// A vector. This is an alias for a Container for now but we may change +// it once Containers are restructured. +// It's used from vector native functions to get a vector and operate on that. +// There is an impl for Vector which implements the API private to this module. +#[derive(Debug)] +pub struct Vector(Container); + +/// A reference to a Move struct that allows you to take a reference to one of its fields. +#[derive(Debug)] +pub struct StructRef(ContainerRef); + +/// A generic Move reference that offers two functionalities: read_ref & write_ref. +#[derive(Debug)] +pub struct Reference(ReferenceImpl); + +// A reference to a signer. Clients can attempt a cast to this struct if they are +// expecting a Signer on the stack or as an argument. +#[derive(Debug)] +pub struct SignerRef(ContainerRef); + +// A reference to a vector. This is an alias for a ContainerRef for now but we may change +// it once Containers are restructured. +// It's used from vector native functions to get a reference to a vector and operate on that. +// There is an impl for VectorRef which implements the API private to this module. +#[derive(Debug)] +pub struct VectorRef(ContainerRef); + +/// A special "slot" in global storage that can hold a resource. It also keeps track of the status +/// of the resource relative to the global state, which is necessary to compute the effects to emit +/// at the end of transaction execution. +#[derive(Debug)] +enum GlobalValueImpl { + /// No resource resides in this slot or in storage. + None, + /// A resource has been published to this slot and it did not previously exist in storage. + Fresh { fields: Rc>> }, + /// A resource resides in this slot and also in storage. The status flag indicates whether + /// it has potentially been altered. + Cached { + fields: Rc>>, + status: Rc>, + }, + /// A resource used to exist in storage but has been deleted by the current transaction. + Deleted, +} + +/// A wrapper around `GlobalValueImpl`, representing a "slot" in global storage that can +/// hold a resource. +#[derive(Debug)] +pub struct GlobalValue(GlobalValueImpl); + +/// The locals for a function frame. It allows values to be read, written or taken +/// reference from. +#[derive(Debug)] +pub struct Locals(Rc>>); + +/// A Move enum value (aka a variant). +#[derive(Debug)] +pub struct Variant { + tag: VariantTag, + fields: Vec, +} + +#[derive(Debug)] +pub struct VariantRef(ContainerRef); + +/*************************************************************************************** + * + * Misc + * + * Miscellaneous helper functions. + * + **************************************************************************************/ + +impl Container { + fn len(&self) -> usize { + match self { + Self::Locals(r) | Self::Struct(r) | Self::Vec(r) => r.borrow().len(), + Self::VecU8(r) => r.borrow().len(), + Self::VecU16(r) => r.borrow().len(), + Self::VecU32(r) => r.borrow().len(), + Self::VecU64(r) => r.borrow().len(), + Self::VecU128(r) => r.borrow().len(), + Self::VecU256(r) => r.borrow().len(), + Self::VecBool(r) => r.borrow().len(), + Self::VecAddress(r) => r.borrow().len(), + Self::Variant(r) => r.borrow().1.len(), + } + } + + fn rc_count(&self) -> usize { + match self { + Self::Locals(r) | Self::Struct(r) | Self::Vec(r) => Rc::strong_count(r), + Self::VecU8(r) => Rc::strong_count(r), + Self::VecU16(r) => Rc::strong_count(r), + Self::VecU32(r) => Rc::strong_count(r), + Self::VecU64(r) => Rc::strong_count(r), + Self::VecU128(r) => Rc::strong_count(r), + Self::VecU256(r) => Rc::strong_count(r), + Self::VecBool(r) => Rc::strong_count(r), + Self::VecAddress(r) => Rc::strong_count(r), + Self::Variant(r) => Rc::strong_count(r), + } + } + + fn signer(x: AccountAddress) -> Self { + Container::Struct(Rc::new(RefCell::new(vec![ValueImpl::Address(Box::new(x))]))) + } +} + +/*************************************************************************************** + * + * Borrows (Internal) + * + * Helper functions to handle Rust borrows. When borrowing from a RefCell, we want + * to return an error instead of panicking. + * + **************************************************************************************/ + +fn take_unique_ownership(r: Rc>) -> PartialVMResult { + match Rc::try_unwrap(r) { + Ok(cell) => Ok(cell.into_inner()), + Err(r) => Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message(format!("moving value {:?} with dangling references", r)), + ), + } +} + +impl ContainerRef { + fn container(&self) -> &Container { + match self { + Self::Local(container) | Self::Global { container, .. } => container, + } + } + + fn mark_dirty(&self) { + if let Self::Global { status, .. } = self { + *status.borrow_mut() = GlobalDataStatus::Dirty + } + } +} + +/*************************************************************************************** + * + * Reference Conversions (Internal) + * + * Helpers to obtain a Rust reference to a value via a VM reference. Required for + * equalities. + * + **************************************************************************************/ +trait VMValueRef { + fn value_ref(&self) -> PartialVMResult<&T>; +} + +macro_rules! impl_vm_value_ref { + ($ty: ty, $tc: ident) => { + impl VMValueRef<$ty> for ValueImpl { + fn value_ref(&self) -> PartialVMResult<&$ty> { + match self { + ValueImpl::$tc(x) => Ok(x), + _ => Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR) + .with_message(format!("cannot take {:?} as &{}", self, stringify!($ty)))), + } + } + } + }; +} + +impl_vm_value_ref!(u8, U8); +impl_vm_value_ref!(u16, U16); +impl_vm_value_ref!(u32, U32); +impl_vm_value_ref!(u64, U64); +impl_vm_value_ref!(u128, U128); +impl_vm_value_ref!(u256::U256, U256); +impl_vm_value_ref!(bool, Bool); +impl_vm_value_ref!(AccountAddress, Address); + +impl ValueImpl { + fn as_value_ref(&self) -> PartialVMResult<&T> + where + Self: VMValueRef, + { + VMValueRef::value_ref(self) + } +} + +/*************************************************************************************** + * + * Copy Value + * + * Implementation of Move copy. Extra care needs to be taken when copying references. + * It is intentional we avoid implementing the standard library trait Clone, to prevent + * surprising behaviors from happening. + * + **************************************************************************************/ +impl ValueImpl { + fn copy_value(&self) -> PartialVMResult { + use ValueImpl::*; + + Ok(match self { + Invalid => Invalid, + + U8(x) => U8(*x), + U16(x) => U16(*x), + U32(x) => U32(*x), + U64(x) => U64(*x), + U128(x) => U128(x.clone()), + U256(x) => U256(x.clone()), + Bool(x) => Bool(*x), + Address(x) => Address(x.clone()), + + ContainerRef(r) => ContainerRef(Box::new(r.copy_value())), + IndexedRef(r) => IndexedRef(Box::new(r.copy_value())), + + // When cloning a container, we need to make sure we make a deep + // copy of the data instead of a shallow copy of the Rc. + Container(c) => Container(c.copy_value()?), + }) + } +} + +impl Container { + fn copy_value(&self) -> PartialVMResult { + let copy_rc_ref_vec_val = |r: &Rc>>| { + Ok(Rc::new(RefCell::new( + r.borrow() + .iter() + .map(|v| v.copy_value()) + .collect::>()?, + ))) + }; + + Ok(match self { + Self::Vec(r) => Self::Vec(copy_rc_ref_vec_val(r)?), + Self::Struct(r) => Self::Struct(copy_rc_ref_vec_val(r)?), + Self::Variant(r) => { + let (tag, values) = &*r.borrow(); + let values = values + .iter() + .map(|v| v.copy_value()) + .collect::>()?; + Self::Variant(Rc::new(RefCell::new((*tag, values)))) + } + + Self::VecU8(r) => Self::VecU8(Rc::new(RefCell::new(r.borrow().clone()))), + Self::VecU16(r) => Self::VecU16(Rc::new(RefCell::new(r.borrow().clone()))), + Self::VecU32(r) => Self::VecU32(Rc::new(RefCell::new(r.borrow().clone()))), + Self::VecU64(r) => Self::VecU64(Rc::new(RefCell::new(r.borrow().clone()))), + Self::VecU128(r) => Self::VecU128(Rc::new(RefCell::new(r.borrow().clone()))), + Self::VecU256(r) => Self::VecU256(Rc::new(RefCell::new(r.borrow().clone()))), + Self::VecBool(r) => Self::VecBool(Rc::new(RefCell::new(r.borrow().clone()))), + Self::VecAddress(r) => Self::VecAddress(Rc::new(RefCell::new(r.borrow().clone()))), + + Self::Locals(_) => { + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("cannot copy a Locals container".to_string()), + ); + } + }) + } + + fn copy_by_ref(&self) -> Self { + match self { + Self::Vec(r) => Self::Vec(Rc::clone(r)), + Self::Struct(r) => Self::Struct(Rc::clone(r)), + Self::VecU8(r) => Self::VecU8(Rc::clone(r)), + Self::VecU16(r) => Self::VecU16(Rc::clone(r)), + Self::VecU32(r) => Self::VecU32(Rc::clone(r)), + Self::VecU64(r) => Self::VecU64(Rc::clone(r)), + Self::VecU128(r) => Self::VecU128(Rc::clone(r)), + Self::VecU256(r) => Self::VecU256(Rc::clone(r)), + Self::VecBool(r) => Self::VecBool(Rc::clone(r)), + Self::VecAddress(r) => Self::VecAddress(Rc::clone(r)), + Self::Locals(r) => Self::Locals(Rc::clone(r)), + Self::Variant(r) => Self::Variant(Rc::clone(r)), + } + } +} + +impl IndexedRef { + fn copy_value(&self) -> Self { + Self { + idx: self.idx, + container_ref: self.container_ref.copy_value(), + } + } +} + +impl ContainerRef { + fn copy_value(&self) -> Self { + match self { + Self::Local(container) => Self::Local(container.copy_by_ref()), + Self::Global { status, container } => Self::Global { + status: Rc::clone(status), + container: container.copy_by_ref(), + }, + } + } +} + +impl Value { + pub fn copy_value(&self) -> PartialVMResult { + Ok(Self(self.0.copy_value()?)) + } +} + +/*************************************************************************************** + * + * Equality + * + * Equality tests of Move values. Errors are raised when types mismatch. + * + * It is intended to NOT use or even implement the standard library traits Eq and + * Partial Eq due to: + * 1. They do not allow errors to be returned. + * 2. They can be invoked without the user being noticed thanks to operator + * overloading. + * + * Eq and Partial Eq must also NOT be derived for the reasons above plus that the + * derived implementation differs from the semantics we want. + * + **************************************************************************************/ + +impl ValueImpl { + fn equals(&self, other: &Self) -> PartialVMResult { + use ValueImpl::*; + + let res = match (self, other) { + (U8(l), U8(r)) => l == r, + (U16(l), U16(r)) => l == r, + (U32(l), U32(r)) => l == r, + (U64(l), U64(r)) => l == r, + (U128(l), U128(r)) => l == r, + (U256(l), U256(r)) => l == r, + (Bool(l), Bool(r)) => l == r, + (Address(l), Address(r)) => l == r, + + (Container(l), Container(r)) => l.equals(r)?, + + (ContainerRef(l), ContainerRef(r)) => l.equals(r)?, + (IndexedRef(l), IndexedRef(r)) => l.equals(r)?, + (Invalid, _) + | (U8(_), _) + | (U16(_), _) + | (U32(_), _) + | (U64(_), _) + | (U128(_), _) + | (U256(_), _) + | (Bool(_), _) + | (Address(_), _) + | (Container(_), _) + | (ContainerRef(_), _) + | (IndexedRef(_), _) => { + return Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR) + .with_message(format!("cannot compare values: {:?}, {:?}", self, other))); + } + }; + + Ok(res) + } +} + +impl Container { + fn equals(&self, other: &Self) -> PartialVMResult { + use Container::*; + + let res = match (self, other) { + (Vec(l), Vec(r)) | (Struct(l), Struct(r)) => { + let l = &l.borrow(); + let r = &r.borrow(); + + if l.len() != r.len() { + return Ok(false); + } + for (v1, v2) in l.iter().zip(r.iter()) { + if !v1.equals(v2)? { + return Ok(false); + } + } + true + } + (VecU8(l), VecU8(r)) => l.borrow().eq(&*r.borrow()), + (VecU16(l), VecU16(r)) => l.borrow().eq(&*r.borrow()), + (VecU32(l), VecU32(r)) => l.borrow().eq(&*r.borrow()), + (VecU64(l), VecU64(r)) => l.borrow().eq(&*r.borrow()), + (VecU128(l), VecU128(r)) => l.borrow().eq(&*r.borrow()), + (VecU256(l), VecU256(r)) => l.borrow().eq(&*r.borrow()), + (VecBool(l), VecBool(r)) => l.borrow().eq(&*r.borrow()), + (VecAddress(l), VecAddress(r)) => l.borrow().eq(&*r.borrow()), + (Variant(l), Variant(r)) => { + let (tagl, valuesl) = &*l.borrow(); + let (tagr, valuesr) = &*r.borrow(); + if tagl != tagr { + return Ok(false); + } + + if valuesl.len() != valuesr.len() { + return Ok(false); + } + for (v1, v2) in valuesl.iter().zip(valuesr) { + if !v1.equals(v2)? { + return Ok(false); + } + } + true + } + + (Locals(_), _) + | (Vec(_), _) + | (Struct(_), _) + | (VecU8(_), _) + | (VecU16(_), _) + | (VecU32(_), _) + | (VecU64(_), _) + | (VecU128(_), _) + | (VecU256(_), _) + | (VecBool(_), _) + | (VecAddress(_), _) + | (Variant { .. }, _) => { + return Err( + PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR).with_message(format!( + "cannot compare container values: {:?}, {:?}", + self, other + )), + ); + } + }; + + Ok(res) + } +} + +impl ContainerRef { + fn equals(&self, other: &Self) -> PartialVMResult { + self.container().equals(other.container()) + } +} + +impl IndexedRef { + fn equals(&self, other: &Self) -> PartialVMResult { + use Container::*; + + let res = match ( + self.container_ref.container(), + other.container_ref.container(), + ) { + // VecC <=> VecR impossible + (Vec(r1), Vec(r2)) + | (Vec(r1), Struct(r2)) + | (Vec(r1), Locals(r2)) + | (Struct(r1), Vec(r2)) + | (Struct(r1), Struct(r2)) + | (Struct(r1), Locals(r2)) + | (Locals(r1), Vec(r2)) + | (Locals(r1), Struct(r2)) + | (Locals(r1), Locals(r2)) => r1.borrow()[self.idx].equals(&r2.borrow()[other.idx])?, + (Struct(r1), Variant(r2)) | (Locals(r1), Variant(r2)) => { + r1.borrow()[self.idx].equals(&r2.borrow().1[other.idx])? + } + (Variant(r1), Vec(r2)) | (Variant(r1), Struct(r2)) | (Variant(r1), Locals(r2)) => { + r1.borrow().1[self.idx].equals(&r2.borrow()[other.idx])? + } + (Variant(r1), Variant(r2)) => { + r1.borrow().1[self.idx].equals(&r2.borrow().1[other.idx])? + } + + (VecU8(r1), VecU8(r2)) => r1.borrow()[self.idx] == r2.borrow()[other.idx], + (VecU16(r1), VecU16(r2)) => r1.borrow()[self.idx] == r2.borrow()[other.idx], + (VecU32(r1), VecU32(r2)) => r1.borrow()[self.idx] == r2.borrow()[other.idx], + (VecU64(r1), VecU64(r2)) => r1.borrow()[self.idx] == r2.borrow()[other.idx], + (VecU128(r1), VecU128(r2)) => r1.borrow()[self.idx] == r2.borrow()[other.idx], + (VecU256(r1), VecU256(r2)) => r1.borrow()[self.idx] == r2.borrow()[other.idx], + (VecBool(r1), VecBool(r2)) => r1.borrow()[self.idx] == r2.borrow()[other.idx], + (VecAddress(r1), VecAddress(r2)) => r1.borrow()[self.idx] == r2.borrow()[other.idx], + + // Equality between a generic and a specialized container. + (Locals(r1), VecU8(r2)) | (Struct(r1), VecU8(r2)) => { + *r1.borrow()[self.idx].as_value_ref::()? == r2.borrow()[other.idx] + } + (VecU8(r1), Locals(r2)) | (VecU8(r1), Struct(r2)) => { + r1.borrow()[self.idx] == *r2.borrow()[other.idx].as_value_ref::()? + } + (Variant(r1), VecU8(r2)) => { + *r1.borrow().1[self.idx].as_value_ref::()? == r2.borrow()[other.idx] + } + (VecU8(r1), Variant(r2)) => { + r1.borrow()[self.idx] == *r2.borrow().1[other.idx].as_value_ref::()? + } + + (Locals(r1), VecU16(r2)) | (Struct(r1), VecU16(r2)) => { + *r1.borrow()[self.idx].as_value_ref::()? == r2.borrow()[other.idx] + } + (VecU16(r1), Locals(r2)) | (VecU16(r1), Struct(r2)) => { + r1.borrow()[self.idx] == *r2.borrow()[other.idx].as_value_ref::()? + } + (Variant(r1), VecU16(r2)) => { + *r1.borrow().1[self.idx].as_value_ref::()? == r2.borrow()[other.idx] + } + (VecU16(r1), Variant(r2)) => { + r1.borrow()[self.idx] == *r2.borrow().1[other.idx].as_value_ref::()? + } + + (Locals(r1), VecU32(r2)) | (Struct(r1), VecU32(r2)) => { + *r1.borrow()[self.idx].as_value_ref::()? == r2.borrow()[other.idx] + } + (VecU32(r1), Locals(r2)) | (VecU32(r1), Struct(r2)) => { + r1.borrow()[self.idx] == *r2.borrow()[other.idx].as_value_ref::()? + } + (Variant(r1), VecU32(r2)) => { + *r1.borrow().1[self.idx].as_value_ref::()? == r2.borrow()[other.idx] + } + (VecU32(r1), Variant(r2)) => { + r1.borrow()[self.idx] == *r2.borrow().1[other.idx].as_value_ref::()? + } + + (Locals(r1), VecU64(r2)) | (Struct(r1), VecU64(r2)) => { + *r1.borrow()[self.idx].as_value_ref::()? == r2.borrow()[other.idx] + } + (VecU64(r1), Locals(r2)) | (VecU64(r1), Struct(r2)) => { + r1.borrow()[self.idx] == *r2.borrow()[other.idx].as_value_ref::()? + } + (Variant(r1), VecU64(r2)) => { + *r1.borrow().1[self.idx].as_value_ref::()? == r2.borrow()[other.idx] + } + (VecU64(r1), Variant(r2)) => { + r1.borrow()[self.idx] == *r2.borrow().1[other.idx].as_value_ref::()? + } + + (Locals(r1), VecU128(r2)) | (Struct(r1), VecU128(r2)) => { + *r1.borrow()[self.idx].as_value_ref::()? == r2.borrow()[other.idx] + } + (VecU128(r1), Locals(r2)) | (VecU128(r1), Struct(r2)) => { + r1.borrow()[self.idx] == *r2.borrow()[other.idx].as_value_ref::()? + } + (Variant(r1), VecU128(r2)) => { + *r1.borrow().1[self.idx].as_value_ref::()? == r2.borrow()[other.idx] + } + (VecU128(r1), Variant(r2)) => { + r1.borrow()[self.idx] == *r2.borrow().1[other.idx].as_value_ref::()? + } + + (Locals(r1), VecU256(r2)) | (Struct(r1), VecU256(r2)) => { + *r1.borrow()[self.idx].as_value_ref::()? == r2.borrow()[other.idx] + } + (VecU256(r1), Locals(r2)) | (VecU256(r1), Struct(r2)) => { + r1.borrow()[self.idx] == *r2.borrow()[other.idx].as_value_ref::()? + } + (Variant(r1), VecU256(r2)) => { + *r1.borrow().1[self.idx].as_value_ref::()? == r2.borrow()[other.idx] + } + (VecU256(r1), Variant(r2)) => { + r1.borrow()[self.idx] == *r2.borrow().1[other.idx].as_value_ref::()? + } + + (Locals(r1), VecBool(r2)) | (Struct(r1), VecBool(r2)) => { + *r1.borrow()[self.idx].as_value_ref::()? == r2.borrow()[other.idx] + } + (VecBool(r1), Locals(r2)) | (VecBool(r1), Struct(r2)) => { + r1.borrow()[self.idx] == *r2.borrow()[other.idx].as_value_ref::()? + } + (Variant(r1), VecBool(r2)) => { + *r1.borrow().1[self.idx].as_value_ref::()? == r2.borrow()[other.idx] + } + (VecBool(r1), Variant(r2)) => { + r1.borrow()[self.idx] == *r2.borrow().1[other.idx].as_value_ref::()? + } + + (Locals(r1), VecAddress(r2)) | (Struct(r1), VecAddress(r2)) => { + *r1.borrow()[self.idx].as_value_ref::()? == r2.borrow()[other.idx] + } + (VecAddress(r1), Locals(r2)) | (VecAddress(r1), Struct(r2)) => { + r1.borrow()[self.idx] == *r2.borrow()[other.idx].as_value_ref::()? + } + (Variant(r1), VecAddress(r2)) => { + *r1.borrow().1[self.idx].as_value_ref::()? == r2.borrow()[other.idx] + } + (VecAddress(r1), Variant(r2)) => { + r1.borrow()[self.idx] + == *r2.borrow().1[other.idx].as_value_ref::()? + } + + // All other combinations are illegal. + (Vec(_), _) + | (VecU8(_), _) + | (VecU16(_), _) + | (VecU32(_), _) + | (VecU64(_), _) + | (VecU128(_), _) + | (VecU256(_), _) + | (VecBool(_), _) + | (VecAddress(_), _) => { + return Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR) + .with_message(format!("cannot compare references {:?}, {:?}", self, other))); + } + }; + Ok(res) + } +} + +impl Value { + pub fn equals(&self, other: &Self) -> PartialVMResult { + self.0.equals(&other.0) + } +} + +/*************************************************************************************** + * + * Read Ref + * + * Implementation of the Move operation read ref. + * + **************************************************************************************/ + +impl ContainerRef { + fn read_ref(self) -> PartialVMResult { + Ok(Value(ValueImpl::Container(self.container().copy_value()?))) + } +} + +impl IndexedRef { + fn read_ref(self) -> PartialVMResult { + use Container::*; + + let res = match self.container_ref.container() { + Locals(r) | Vec(r) | Struct(r) => r.borrow()[self.idx].copy_value()?, + Variant(r) => r.borrow().1[self.idx].copy_value()?, + VecU8(r) => ValueImpl::U8(r.borrow()[self.idx]), + VecU16(r) => ValueImpl::U16(r.borrow()[self.idx]), + VecU32(r) => ValueImpl::U32(r.borrow()[self.idx]), + VecU64(r) => ValueImpl::U64(r.borrow()[self.idx]), + VecU128(r) => ValueImpl::U128(Box::new(r.borrow()[self.idx])), + VecU256(r) => ValueImpl::U256(Box::new(r.borrow()[self.idx])), + VecBool(r) => ValueImpl::Bool(r.borrow()[self.idx]), + VecAddress(r) => ValueImpl::Address(Box::new(r.borrow()[self.idx])), + }; + + Ok(Value(res)) + } +} + +impl ReferenceImpl { + fn read_ref(self) -> PartialVMResult { + match self { + Self::ContainerRef(r) => r.read_ref(), + Self::IndexedRef(r) => r.read_ref(), + } + } +} + +impl StructRef { + pub fn read_ref(self) -> PartialVMResult { + self.0.read_ref() + } +} + +impl Reference { + pub fn read_ref(self) -> PartialVMResult { + self.0.read_ref() + } +} + +/*************************************************************************************** + * + * Write Ref + * + * Implementation of the Move operation write ref. + * + **************************************************************************************/ + +impl ContainerRef { + fn write_ref(self, v: Value) -> PartialVMResult<()> { + match v.0 { + ValueImpl::Container(c) => { + macro_rules! assign { + ($r1: expr, $tc: ident) => {{ + let r = match c { + Container::$tc(v) => v, + _ => { + return Err(PartialVMError::new( + StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR, + ) + .with_message( + "failed to write_ref: container type mismatch".to_string(), + )); + } + }; + *$r1.borrow_mut() = take_unique_ownership(r)?; + }}; + } + + match self.container() { + Container::Struct(r) => assign!(r, Struct), + Container::Variant(r) => match c { + Container::Variant(new) => { + *r.borrow_mut() = take_unique_ownership(new)?; + } + _ => { + return Err(PartialVMError::new( + StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR, + ) + .with_message( + "failed to write_ref: container type mismatch".to_string(), + )); + } + }, + Container::Vec(r) => assign!(r, Vec), + Container::VecU8(r) => assign!(r, VecU8), + Container::VecU16(r) => assign!(r, VecU16), + Container::VecU32(r) => assign!(r, VecU32), + Container::VecU64(r) => assign!(r, VecU64), + Container::VecU128(r) => assign!(r, VecU128), + Container::VecU256(r) => assign!(r, VecU256), + Container::VecBool(r) => assign!(r, VecBool), + Container::VecAddress(r) => assign!(r, VecAddress), + Container::Locals(_) => { + return Err(PartialVMError::new( + StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR, + ) + .with_message("cannot overwrite Container::Locals".to_string())); + } + } + self.mark_dirty(); + } + _ => { + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message(format!( + "cannot write value {:?} to container ref {:?}", + v, self + )), + ); + } + } + Ok(()) + } +} + +impl IndexedRef { + fn write_ref(self, x: Value) -> PartialVMResult<()> { + match &x.0 { + ValueImpl::IndexedRef(_) + | ValueImpl::ContainerRef(_) + | ValueImpl::Invalid + | ValueImpl::Container(_) => { + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message(format!( + "cannot write value {:?} to indexed ref {:?}", + x, self + )), + ); + } + _ => (), + } + + match (self.container_ref.container(), &x.0) { + (Container::Locals(r), _) | (Container::Vec(r), _) | (Container::Struct(r), _) => { + let mut v = r.borrow_mut(); + v[self.idx] = x.0; + } + (Container::Variant(r), _) => { + r.borrow_mut().1[self.idx] = x.0; + } + (Container::VecU8(r), ValueImpl::U8(x)) => r.borrow_mut()[self.idx] = *x, + (Container::VecU16(r), ValueImpl::U16(x)) => r.borrow_mut()[self.idx] = *x, + (Container::VecU32(r), ValueImpl::U32(x)) => r.borrow_mut()[self.idx] = *x, + (Container::VecU64(r), ValueImpl::U64(x)) => r.borrow_mut()[self.idx] = *x, + (Container::VecU128(r), ValueImpl::U128(x)) => r.borrow_mut()[self.idx] = **x, + (Container::VecU256(r), ValueImpl::U256(x)) => r.borrow_mut()[self.idx] = **x, + (Container::VecBool(r), ValueImpl::Bool(x)) => r.borrow_mut()[self.idx] = *x, + (Container::VecAddress(r), ValueImpl::Address(x)) => r.borrow_mut()[self.idx] = **x, + + (Container::VecU8(_), _) + | (Container::VecU16(_), _) + | (Container::VecU32(_), _) + | (Container::VecU64(_), _) + | (Container::VecU128(_), _) + | (Container::VecU256(_), _) + | (Container::VecBool(_), _) + | (Container::VecAddress(_), _) => { + return Err( + PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR).with_message(format!( + "cannot write value {:?} to indexed ref {:?}", + x, self + )), + ); + } + } + self.container_ref.mark_dirty(); + Ok(()) + } +} + +impl ReferenceImpl { + fn write_ref(self, x: Value) -> PartialVMResult<()> { + match self { + Self::ContainerRef(r) => r.write_ref(x), + Self::IndexedRef(r) => r.write_ref(x), + } + } +} + +impl Reference { + pub fn write_ref(self, x: Value) -> PartialVMResult<()> { + self.0.write_ref(x) + } +} + +/*************************************************************************************** + * + * Borrows (Move) + * + * Implementation of borrowing in Move: borrow field, borrow local and infrastructure + * to support borrowing an element from a vector. + * + **************************************************************************************/ + +impl ContainerRef { + fn borrow_elem(&self, idx: usize) -> PartialVMResult { + let len = self.container().len(); + if idx >= len { + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR).with_message( + format!( + "index out of bounds when borrowing container element: got: {}, len: {}", + idx, len + ), + ), + ); + } + + let res = match self.container() { + c @ Container::Locals(_) + | c @ Container::Vec(_) + | c @ Container::Struct(_) + | c @ Container::Variant(_) => { + let rc_r; + let rc_values; + let v = match c { + Container::Locals(r) | Container::Vec(r) | Container::Struct(r) => { + rc_r = r.borrow(); + &*rc_r + } + Container::Variant(r) => { + rc_values = r.borrow(); + &*rc_values.1 + } + _ => unreachable!(), + }; + match &v[idx] { + // TODO: check for the impossible combinations. + ValueImpl::Container(container) => { + let r = match self { + Self::Local(_) => Self::Local(container.copy_by_ref()), + Self::Global { status, .. } => Self::Global { + status: Rc::clone(status), + container: container.copy_by_ref(), + }, + }; + ValueImpl::ContainerRef(Box::new(r)) + } + _ => ValueImpl::IndexedRef(Box::new(IndexedRef { + idx, + container_ref: self.copy_value(), + })), + } + } + + Container::VecU8(_) + | Container::VecU16(_) + | Container::VecU32(_) + | Container::VecU64(_) + | Container::VecU128(_) + | Container::VecU256(_) + | Container::VecAddress(_) + | Container::VecBool(_) => ValueImpl::IndexedRef(Box::new(IndexedRef { + idx, + container_ref: self.copy_value(), + })), + }; + + Ok(res) + } +} + +impl StructRef { + pub fn borrow_field(&self, idx: usize) -> PartialVMResult { + Ok(Value(self.0.borrow_elem(idx)?)) + } +} + +impl VariantRef { + pub fn get_tag(&self) -> PartialVMResult { + match self.0.container() { + Container::Variant(r) => Ok(r.borrow().0), + Container::Locals(_) + | Container::Vec(_) + | Container::Struct(_) + | Container::VecU8(_) + | Container::VecU64(_) + | Container::VecU128(_) + | Container::VecBool(_) + | Container::VecAddress(_) + | Container::VecU16(_) + | Container::VecU32(_) + | Container::VecU256(_) => Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR) + .with_message(format!( + "expected variant container, got {:?}", + self.0.container() + ))), + } + } + pub fn check_tag(&self, expected_tag: VariantTag) -> PartialVMResult<()> { + let tag = self.get_tag()?; + if tag != expected_tag { + Err( + PartialVMError::new(StatusCode::VARIANT_TAG_MISMATCH).with_message(format!( + "Variant tag mismatch: expected {}, got {}", + expected_tag, tag + )), + ) + } else { + Ok(()) + } + } + + pub fn unpack_variant(&self) -> PartialVMResult> { + match self.0.container() { + Container::Variant(r) => { + let values = &*r.borrow().1; + let mut res = vec![]; + for (idx, v) in values.iter().enumerate() { + let ref_ = match v { + ValueImpl::Container(container) => { + let r = match &self.0 { + ContainerRef::Local(_) => { + ContainerRef::Local(container.copy_by_ref()) + } + ContainerRef::Global { status, .. } => ContainerRef::Global { + status: Rc::clone(status), + container: container.copy_by_ref(), + }, + }; + ValueImpl::ContainerRef(Box::new(r)) + } + ValueImpl::U8(_) + | ValueImpl::U16(_) + | ValueImpl::U32(_) + | ValueImpl::U64(_) + | ValueImpl::U128(_) + | ValueImpl::U256(_) + | ValueImpl::Bool(_) + | ValueImpl::Address(_) => ValueImpl::IndexedRef(Box::new(IndexedRef { + idx, + container_ref: self.0.copy_value(), + })), + x @ (ValueImpl::ContainerRef(_) + | ValueImpl::IndexedRef(_) + | ValueImpl::Invalid) => return Err(PartialVMError::new( + StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR, + ) + .with_message(format!( + "cannot unpack a reference value {:?} held inside a variant ref {:?}", + x, self + ))), + }; + res.push(Value(ref_)); + } + Ok(res) + } + Container::Locals(_) + | Container::Vec(_) + | Container::Struct(_) + | Container::VecU8(_) + | Container::VecU64(_) + | Container::VecU128(_) + | Container::VecBool(_) + | Container::VecAddress(_) + | Container::VecU16(_) + | Container::VecU32(_) + | Container::VecU256(_) => Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR) + .with_message(format!( + "expected variant container, got {:?}", + self.0.container() + ))), + } + } +} + +impl Locals { + pub fn borrow_loc(&self, idx: usize) -> PartialVMResult { + // TODO: this is very similar to SharedContainer::borrow_elem. Find a way to + // reuse that code? + + let v = self.0.borrow(); + if idx >= v.len() { + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR).with_message( + format!( + "index out of bounds when borrowing local: got: {}, len: {}", + idx, + v.len() + ), + ), + ); + } + + match &v[idx] { + ValueImpl::Container(c) => Ok(Value(ValueImpl::ContainerRef(Box::new( + ContainerRef::Local(c.copy_by_ref()), + )))), + + ValueImpl::U8(_) + | ValueImpl::U16(_) + | ValueImpl::U32(_) + | ValueImpl::U64(_) + | ValueImpl::U128(_) + | ValueImpl::U256(_) + | ValueImpl::Bool(_) + | ValueImpl::Address(_) => Ok(Value(ValueImpl::IndexedRef(Box::new(IndexedRef { + container_ref: ContainerRef::Local(Container::Locals(Rc::clone(&self.0))), + idx, + })))), + + ValueImpl::ContainerRef(_) | ValueImpl::Invalid | ValueImpl::IndexedRef(_) => Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message(format!("cannot borrow local {:?}", &v[idx])), + ), + } + } +} + +impl SignerRef { + pub fn borrow_signer(&self) -> PartialVMResult { + Ok(Value(self.0.borrow_elem(0)?)) + } +} + +/*************************************************************************************** + * + * Locals + * + * Public APIs for Locals to support reading, writing and moving of values. + * + **************************************************************************************/ +impl Locals { + pub fn new(n: usize) -> Self { + Self(Rc::new(RefCell::new( + iter::repeat_with(|| ValueImpl::Invalid).take(n).collect(), + ))) + } + + pub fn copy_loc(&self, idx: usize) -> PartialVMResult { + let v = self.0.borrow(); + match v.get(idx) { + Some(ValueImpl::Invalid) => Err(PartialVMError::new( + StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR, + ) + .with_message(format!("cannot copy invalid value at index {}", idx))), + Some(v) => Ok(Value(v.copy_value()?)), + None => Err( + PartialVMError::new(StatusCode::VERIFIER_INVARIANT_VIOLATION).with_message( + format!("local index out of bounds: got {}, len: {}", idx, v.len()), + ), + ), + } + } + + fn swap_loc(&mut self, idx: usize, x: Value, violation_check: bool) -> PartialVMResult { + let mut v = self.0.borrow_mut(); + match v.get_mut(idx) { + Some(v) => { + if violation_check + && let ValueImpl::Container(c) = v + && c.rc_count() > 1 + { + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("moving container with dangling references".to_string()), + ); + } + Ok(Value(std::mem::replace(v, x.0))) + } + None => Err( + PartialVMError::new(StatusCode::VERIFIER_INVARIANT_VIOLATION).with_message( + format!("local index out of bounds: got {}, len: {}", idx, v.len()), + ), + ), + } + } + + pub fn move_loc(&mut self, idx: usize, violation_check: bool) -> PartialVMResult { + match self.swap_loc(idx, Value(ValueImpl::Invalid), violation_check)? { + Value(ValueImpl::Invalid) => Err(PartialVMError::new( + StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR, + ) + .with_message(format!("cannot move invalid value at index {}", idx))), + v => Ok(v), + } + } + + pub fn store_loc( + &mut self, + idx: usize, + x: Value, + violation_check: bool, + ) -> PartialVMResult<()> { + self.swap_loc(idx, x, violation_check)?; + Ok(()) + } + + /// Drop all Move values onto a different Vec to avoid leaking memory. + /// References are excluded since they may point to invalid data. + pub fn drop_all_values(&mut self) -> impl Iterator { + let mut locals = self.0.borrow_mut(); + let mut res = vec![]; + + for idx in 0..locals.len() { + match &locals[idx] { + ValueImpl::Invalid => (), + ValueImpl::ContainerRef(_) | ValueImpl::IndexedRef(_) => { + locals[idx] = ValueImpl::Invalid; + } + _ => res.push(( + idx, + Value(std::mem::replace(&mut locals[idx], ValueImpl::Invalid)), + )), + } + } + + res.into_iter() + } + + pub fn is_invalid(&self, idx: usize) -> PartialVMResult { + let v = self.0.borrow(); + match v.get(idx) { + Some(ValueImpl::Invalid) => Ok(true), + Some(_) => Ok(false), + None => Err( + PartialVMError::new(StatusCode::VERIFIER_INVARIANT_VIOLATION).with_message( + format!("local index out of bounds: got {}, len: {}", idx, v.len()), + ), + ), + } + } +} + +/*************************************************************************************** + * + * Public Value Constructors + * + * Constructors to allow values to be created outside this module. + * + **************************************************************************************/ +impl Value { + pub fn u8(x: u8) -> Self { + Self(ValueImpl::U8(x)) + } + + pub fn u16(x: u16) -> Self { + Self(ValueImpl::U16(x)) + } + + pub fn u32(x: u32) -> Self { + Self(ValueImpl::U32(x)) + } + + pub fn u64(x: u64) -> Self { + Self(ValueImpl::U64(x)) + } + + pub fn u128(x: u128) -> Self { + Self(ValueImpl::U128(Box::new(x))) + } + + pub fn u256(x: u256::U256) -> Self { + Self(ValueImpl::U256(Box::new(x))) + } + + pub fn bool(x: bool) -> Self { + Self(ValueImpl::Bool(x)) + } + + pub fn address(x: AccountAddress) -> Self { + Self(ValueImpl::Address(Box::new(x))) + } + + pub fn signer(x: AccountAddress) -> Self { + Self(ValueImpl::Container(Container::signer(x))) + } + + /// Create a "unowned" reference to a signer value (&signer) for populating the &signer in + /// execute function + pub fn signer_reference(x: AccountAddress) -> Self { + Self(ValueImpl::ContainerRef(Box::new(ContainerRef::Local( + Container::signer(x), + )))) + } + + pub fn struct_(s: Struct) -> Self { + Self(ValueImpl::Container(Container::Struct(Rc::new( + RefCell::new(s.fields), + )))) + } + + pub fn variant(s: Variant) -> Self { + Self(ValueImpl::Container(Container::Variant(Rc::new( + RefCell::new((s.tag, s.fields)), + )))) + } + + pub fn vector_u8(it: impl IntoIterator) -> Self { + Self(ValueImpl::Container(Container::VecU8(Rc::new( + RefCell::new(it.into_iter().collect()), + )))) + } + + pub fn vector_u16(it: impl IntoIterator) -> Self { + Self(ValueImpl::Container(Container::VecU16(Rc::new( + RefCell::new(it.into_iter().collect()), + )))) + } + + pub fn vector_u32(it: impl IntoIterator) -> Self { + Self(ValueImpl::Container(Container::VecU32(Rc::new( + RefCell::new(it.into_iter().collect()), + )))) + } + + pub fn vector_u64(it: impl IntoIterator) -> Self { + Self(ValueImpl::Container(Container::VecU64(Rc::new( + RefCell::new(it.into_iter().collect()), + )))) + } + + pub fn vector_u128(it: impl IntoIterator) -> Self { + Self(ValueImpl::Container(Container::VecU128(Rc::new( + RefCell::new(it.into_iter().collect()), + )))) + } + + pub fn vector_u256(it: impl IntoIterator) -> Self { + Self(ValueImpl::Container(Container::VecU256(Rc::new( + RefCell::new(it.into_iter().collect()), + )))) + } + + pub fn vector_bool(it: impl IntoIterator) -> Self { + Self(ValueImpl::Container(Container::VecBool(Rc::new( + RefCell::new(it.into_iter().collect()), + )))) + } + + pub fn vector_address(it: impl IntoIterator) -> Self { + Self(ValueImpl::Container(Container::VecAddress(Rc::new( + RefCell::new(it.into_iter().collect()), + )))) + } +} + +/*************************************************************************************** + * + * Casting + * + * Due to the public value types being opaque to an external user, the following + * public APIs are required to enable conversion between types in order to gain access + * to specific operations certain more refined types offer. + * For example, one must convert a `Value` to a `Struct` before unpack can be called. + * + * It is expected that the caller will keep track of the invariants and guarantee + * the conversion will succeed. An error will be raised in case of a violation. + * + **************************************************************************************/ +pub trait VMValueCast { + fn cast(self) -> PartialVMResult; +} + +macro_rules! impl_vm_value_cast { + ($ty: ty, $tc: ident) => { + impl VMValueCast<$ty> for Value { + fn cast(self) -> PartialVMResult<$ty> { + match self.0 { + ValueImpl::$tc(x) => Ok(x), + v => Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR) + .with_message(format!("cannot cast {:?} to {}", v, stringify!($ty)))), + } + } + } + }; +} + +macro_rules! impl_vm_value_cast_boxed { + ($ty: ty, $tc: ident) => { + impl VMValueCast<$ty> for Value { + fn cast(self) -> PartialVMResult<$ty> { + match self.0 { + ValueImpl::$tc(x) => Ok(*x), + v => Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR) + .with_message(format!("cannot cast {:?} to {}", v, stringify!($ty)))), + } + } + } + }; +} + +impl VMValueCast for Value { + fn cast(self) -> PartialVMResult { + Ok(self) + } +} + +impl_vm_value_cast!(u8, U8); +impl_vm_value_cast!(u16, U16); +impl_vm_value_cast!(u32, U32); +impl_vm_value_cast!(u64, U64); +impl_vm_value_cast_boxed!(u128, U128); +impl_vm_value_cast_boxed!(u256::U256, U256); +impl_vm_value_cast!(bool, Bool); +impl_vm_value_cast_boxed!(AccountAddress, Address); +impl_vm_value_cast_boxed!(ContainerRef, ContainerRef); +impl_vm_value_cast_boxed!(IndexedRef, IndexedRef); + +impl VMValueCast for Value { + fn cast(self) -> PartialVMResult { + match self.0 { + ValueImpl::U8(x) => Ok(IntegerValue::U8(x)), + ValueImpl::U16(x) => Ok(IntegerValue::U16(x)), + ValueImpl::U32(x) => Ok(IntegerValue::U32(x)), + ValueImpl::U64(x) => Ok(IntegerValue::U64(x)), + ValueImpl::U128(x) => Ok(IntegerValue::U128(*x)), + ValueImpl::U256(x) => Ok(IntegerValue::U256(*x)), + v => Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR) + .with_message(format!("cannot cast {:?} to integer", v,))), + } + } +} + +impl VMValueCast for Value { + fn cast(self) -> PartialVMResult { + match self.0 { + ValueImpl::ContainerRef(r) => Ok(Reference(ReferenceImpl::ContainerRef(*r))), + ValueImpl::IndexedRef(r) => Ok(Reference(ReferenceImpl::IndexedRef(*r))), + v => Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR) + .with_message(format!("cannot cast {:?} to reference", v,))), + } + } +} + +impl VMValueCast for Value { + fn cast(self) -> PartialVMResult { + match self.0 { + ValueImpl::Container(c) => Ok(c), + v => Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR) + .with_message(format!("cannot cast {:?} to container", v,))), + } + } +} + +impl VMValueCast for Value { + fn cast(self) -> PartialVMResult { + match self.0 { + ValueImpl::Container(Container::Struct(r)) => Ok(Struct { + fields: take_unique_ownership(r)?, + }), + v => Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR) + .with_message(format!("cannot cast {:?} to struct", v,))), + } + } +} + +impl VMValueCast for Value { + fn cast(self) -> PartialVMResult { + match self.0 { + ValueImpl::Container(Container::Variant(r)) => { + let (tag, fields) = take_unique_ownership(r)?; + Ok(Variant { tag, fields }) + } + v => Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR) + .with_message(format!("cannot cast {:?} to enum variant", v,))), + } + } +} + +impl VMValueCast for Value { + fn cast(self) -> PartialVMResult { + Ok(StructRef(VMValueCast::cast(self)?)) + } +} + +impl VMValueCast for Value { + fn cast(self) -> PartialVMResult { + Ok(VariantRef(VMValueCast::cast(self)?)) + } +} + +impl VMValueCast> for Value { + fn cast(self) -> PartialVMResult> { + match self.0 { + ValueImpl::Container(Container::VecU8(r)) => take_unique_ownership(r), + v => Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR) + .with_message(format!("cannot cast {:?} to vector", v,))), + } + } +} + +impl VMValueCast> for Value { + fn cast(self) -> PartialVMResult> { + match self.0 { + ValueImpl::Container(Container::VecU16(r)) => take_unique_ownership(r), + v => Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR) + .with_message(format!("cannot cast {:?} to vector", v,))), + } + } +} + +impl VMValueCast> for Value { + fn cast(self) -> PartialVMResult> { + match self.0 { + ValueImpl::Container(Container::VecU32(r)) => take_unique_ownership(r), + v => Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR) + .with_message(format!("cannot cast {:?} to vector", v,))), + } + } +} + +impl VMValueCast> for Value { + fn cast(self) -> PartialVMResult> { + match self.0 { + ValueImpl::Container(Container::VecU64(r)) => take_unique_ownership(r), + v => Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR) + .with_message(format!("cannot cast {:?} to vector", v,))), + } + } +} + +impl VMValueCast> for Value { + fn cast(self) -> PartialVMResult> { + match self.0 { + ValueImpl::Container(Container::VecU128(r)) => take_unique_ownership(r), + v => Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR) + .with_message(format!("cannot cast {:?} to vector", v,))), + } + } +} + +impl VMValueCast> for Value { + fn cast(self) -> PartialVMResult> { + match self.0 { + ValueImpl::Container(Container::VecU256(r)) => take_unique_ownership(r), + v => Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR) + .with_message(format!("cannot cast {:?} to vector", v,))), + } + } +} + +impl VMValueCast> for Value { + fn cast(self) -> PartialVMResult> { + match self.0 { + ValueImpl::Container(Container::VecAddress(r)) => take_unique_ownership(r), + v => Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR) + .with_message(format!("cannot cast {:?} to vector
", v,))), + } + } +} + +impl VMValueCast> for Value { + fn cast(self) -> PartialVMResult> { + match self.0 { + ValueImpl::Container(Container::Vec(c)) => { + Ok(take_unique_ownership(c)?.into_iter().map(Value).collect()) + } + ValueImpl::Address(_) + | ValueImpl::Bool(_) + | ValueImpl::U8(_) + | ValueImpl::U16(_) + | ValueImpl::U32(_) + | ValueImpl::U64(_) + | ValueImpl::U128(_) + | ValueImpl::U256(_) => Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR) + .with_message( + "cannot cast a specialized vector into a non-specialized one".to_string(), + )), + v => Err( + PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR).with_message(format!( + "cannot cast {:?} to vector", + v, + )), + ), + } + } +} + +impl VMValueCast for Value { + fn cast(self) -> PartialVMResult { + match self.0 { + ValueImpl::ContainerRef(r) => Ok(SignerRef(*r)), + v => Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR) + .with_message(format!("cannot cast {:?} to Signer reference", v,))), + } + } +} + +impl VMValueCast for Value { + fn cast(self) -> PartialVMResult { + match self.0 { + ValueImpl::ContainerRef(r) => Ok(VectorRef(*r)), + v => Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR) + .with_message(format!("cannot cast {:?} to vector reference", v,))), + } + } +} + +impl VMValueCast for Value { + fn cast(self) -> PartialVMResult { + match self.0 { + ValueImpl::Container(c) => Ok(Vector(c)), + v => Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR) + .with_message(format!("cannot cast {:?} to vector", v,))), + } + } +} + +impl Value { + pub fn value_as(self) -> PartialVMResult + where + Self: VMValueCast, + { + VMValueCast::cast(self) + } +} + +impl VMValueCast for IntegerValue { + fn cast(self) -> PartialVMResult { + match self { + Self::U8(x) => Ok(x), + v => Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR) + .with_message(format!("cannot cast {:?} to u8", v,))), + } + } +} + +impl VMValueCast for IntegerValue { + fn cast(self) -> PartialVMResult { + match self { + Self::U16(x) => Ok(x), + v => Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR) + .with_message(format!("cannot cast {:?} to u16", v,))), + } + } +} + +impl VMValueCast for IntegerValue { + fn cast(self) -> PartialVMResult { + match self { + Self::U32(x) => Ok(x), + v => Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR) + .with_message(format!("cannot cast {:?} to u32", v,))), + } + } +} + +impl VMValueCast for IntegerValue { + fn cast(self) -> PartialVMResult { + match self { + Self::U64(x) => Ok(x), + v => Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR) + .with_message(format!("cannot cast {:?} to u64", v,))), + } + } +} + +impl VMValueCast for IntegerValue { + fn cast(self) -> PartialVMResult { + match self { + Self::U128(x) => Ok(x), + v => Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR) + .with_message(format!("cannot cast {:?} to u128", v,))), + } + } +} + +impl VMValueCast for IntegerValue { + fn cast(self) -> PartialVMResult { + match self { + Self::U256(x) => Ok(x), + v => Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR) + .with_message(format!("cannot cast {:?} to u256", v,))), + } + } +} + +impl IntegerValue { + pub fn value_as(self) -> PartialVMResult + where + Self: VMValueCast, + { + VMValueCast::cast(self) + } +} + +/*************************************************************************************** + * + * Integer Operations + * + * Arithmetic operations and conversions for integer values. + * + **************************************************************************************/ +impl IntegerValue { + pub fn add_checked(self, other: Self) -> PartialVMResult { + use IntegerValue::*; + let res = match (self, other) { + (U8(l), U8(r)) => u8::checked_add(l, r).map(IntegerValue::U8), + (U16(l), U16(r)) => u16::checked_add(l, r).map(IntegerValue::U16), + (U32(l), U32(r)) => u32::checked_add(l, r).map(IntegerValue::U32), + (U64(l), U64(r)) => u64::checked_add(l, r).map(IntegerValue::U64), + (U128(l), U128(r)) => u128::checked_add(l, r).map(IntegerValue::U128), + (U256(l), U256(r)) => u256::U256::checked_add(l, r).map(IntegerValue::U256), + (l, r) => { + let msg = format!("Cannot add {:?} and {:?}", l, r); + return Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR).with_message(msg)); + } + }; + res.ok_or_else(|| PartialVMError::new(StatusCode::ARITHMETIC_ERROR)) + } + + pub fn sub_checked(self, other: Self) -> PartialVMResult { + use IntegerValue::*; + let res = match (self, other) { + (U8(l), U8(r)) => u8::checked_sub(l, r).map(IntegerValue::U8), + (U16(l), U16(r)) => u16::checked_sub(l, r).map(IntegerValue::U16), + (U32(l), U32(r)) => u32::checked_sub(l, r).map(IntegerValue::U32), + (U64(l), U64(r)) => u64::checked_sub(l, r).map(IntegerValue::U64), + (U128(l), U128(r)) => u128::checked_sub(l, r).map(IntegerValue::U128), + (U256(l), U256(r)) => u256::U256::checked_sub(l, r).map(IntegerValue::U256), + (l, r) => { + let msg = format!("Cannot sub {:?} from {:?}", r, l); + return Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR).with_message(msg)); + } + }; + res.ok_or_else(|| PartialVMError::new(StatusCode::ARITHMETIC_ERROR)) + } + + pub fn mul_checked(self, other: Self) -> PartialVMResult { + use IntegerValue::*; + let res = match (self, other) { + (U8(l), U8(r)) => u8::checked_mul(l, r).map(IntegerValue::U8), + (U16(l), U16(r)) => u16::checked_mul(l, r).map(IntegerValue::U16), + (U32(l), U32(r)) => u32::checked_mul(l, r).map(IntegerValue::U32), + (U64(l), U64(r)) => u64::checked_mul(l, r).map(IntegerValue::U64), + (U128(l), U128(r)) => u128::checked_mul(l, r).map(IntegerValue::U128), + (U256(l), U256(r)) => u256::U256::checked_mul(l, r).map(IntegerValue::U256), + (l, r) => { + let msg = format!("Cannot mul {:?} and {:?}", l, r); + return Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR).with_message(msg)); + } + }; + res.ok_or_else(|| PartialVMError::new(StatusCode::ARITHMETIC_ERROR)) + } + + pub fn div_checked(self, other: Self) -> PartialVMResult { + use IntegerValue::*; + let res = match (self, other) { + (U8(l), U8(r)) => u8::checked_div(l, r).map(IntegerValue::U8), + (U16(l), U16(r)) => u16::checked_div(l, r).map(IntegerValue::U16), + (U32(l), U32(r)) => u32::checked_div(l, r).map(IntegerValue::U32), + (U64(l), U64(r)) => u64::checked_div(l, r).map(IntegerValue::U64), + (U128(l), U128(r)) => u128::checked_div(l, r).map(IntegerValue::U128), + (U256(l), U256(r)) => u256::U256::checked_div(l, r).map(IntegerValue::U256), + (l, r) => { + let msg = format!("Cannot div {:?} by {:?}", l, r); + return Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR).with_message(msg)); + } + }; + res.ok_or_else(|| PartialVMError::new(StatusCode::ARITHMETIC_ERROR)) + } + + pub fn rem_checked(self, other: Self) -> PartialVMResult { + use IntegerValue::*; + let res = match (self, other) { + (U8(l), U8(r)) => u8::checked_rem(l, r).map(IntegerValue::U8), + (U16(l), U16(r)) => u16::checked_rem(l, r).map(IntegerValue::U16), + (U32(l), U32(r)) => u32::checked_rem(l, r).map(IntegerValue::U32), + (U64(l), U64(r)) => u64::checked_rem(l, r).map(IntegerValue::U64), + (U128(l), U128(r)) => u128::checked_rem(l, r).map(IntegerValue::U128), + (U256(l), U256(r)) => u256::U256::checked_rem(l, r).map(IntegerValue::U256), + (l, r) => { + let msg = format!("Cannot rem {:?} by {:?}", l, r); + return Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR).with_message(msg)); + } + }; + res.ok_or_else(|| PartialVMError::new(StatusCode::ARITHMETIC_ERROR)) + } + + pub fn bit_or(self, other: Self) -> PartialVMResult { + use IntegerValue::*; + Ok(match (self, other) { + (U8(l), U8(r)) => IntegerValue::U8(l | r), + (U16(l), U16(r)) => IntegerValue::U16(l | r), + (U32(l), U32(r)) => IntegerValue::U32(l | r), + (U64(l), U64(r)) => IntegerValue::U64(l | r), + (U128(l), U128(r)) => IntegerValue::U128(l | r), + (U256(l), U256(r)) => IntegerValue::U256(l | r), + (l, r) => { + let msg = format!("Cannot bit_or {:?} and {:?}", l, r); + return Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR).with_message(msg)); + } + }) + } + + pub fn bit_and(self, other: Self) -> PartialVMResult { + use IntegerValue::*; + Ok(match (self, other) { + (U8(l), U8(r)) => IntegerValue::U8(l & r), + (U16(l), U16(r)) => IntegerValue::U16(l & r), + (U32(l), U32(r)) => IntegerValue::U32(l & r), + (U64(l), U64(r)) => IntegerValue::U64(l & r), + (U128(l), U128(r)) => IntegerValue::U128(l & r), + (U256(l), U256(r)) => IntegerValue::U256(l & r), + (l, r) => { + let msg = format!("Cannot bit_and {:?} and {:?}", l, r); + return Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR).with_message(msg)); + } + }) + } + + pub fn bit_xor(self, other: Self) -> PartialVMResult { + use IntegerValue::*; + Ok(match (self, other) { + (U8(l), U8(r)) => IntegerValue::U8(l ^ r), + (U16(l), U16(r)) => IntegerValue::U16(l ^ r), + (U32(l), U32(r)) => IntegerValue::U32(l ^ r), + (U64(l), U64(r)) => IntegerValue::U64(l ^ r), + (U128(l), U128(r)) => IntegerValue::U128(l ^ r), + (U256(l), U256(r)) => IntegerValue::U256(l ^ r), + (l, r) => { + let msg = format!("Cannot bit_xor {:?} and {:?}", l, r); + return Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR).with_message(msg)); + } + }) + } + + pub fn shl_checked(self, n_bits: u8) -> PartialVMResult { + use IntegerValue::*; + + Ok(match self { + U8(x) => { + if n_bits >= 8 { + return Err(PartialVMError::new(StatusCode::ARITHMETIC_ERROR)); + } + IntegerValue::U8(x << n_bits) + } + U16(x) => { + if n_bits >= 16 { + return Err(PartialVMError::new(StatusCode::ARITHMETIC_ERROR)); + } + IntegerValue::U16(x << n_bits) + } + U32(x) => { + if n_bits >= 32 { + return Err(PartialVMError::new(StatusCode::ARITHMETIC_ERROR)); + } + IntegerValue::U32(x << n_bits) + } + U64(x) => { + if n_bits >= 64 { + return Err(PartialVMError::new(StatusCode::ARITHMETIC_ERROR)); + } + IntegerValue::U64(x << n_bits) + } + U128(x) => { + if n_bits >= 128 { + return Err(PartialVMError::new(StatusCode::ARITHMETIC_ERROR)); + } + IntegerValue::U128(x << n_bits) + } + U256(x) => IntegerValue::U256(x << n_bits), + }) + } + + pub fn shr_checked(self, n_bits: u8) -> PartialVMResult { + use IntegerValue::*; + + Ok(match self { + U8(x) => { + if n_bits >= 8 { + return Err(PartialVMError::new(StatusCode::ARITHMETIC_ERROR)); + } + IntegerValue::U8(x >> n_bits) + } + U16(x) => { + if n_bits >= 16 { + return Err(PartialVMError::new(StatusCode::ARITHMETIC_ERROR)); + } + IntegerValue::U16(x >> n_bits) + } + U32(x) => { + if n_bits >= 32 { + return Err(PartialVMError::new(StatusCode::ARITHMETIC_ERROR)); + } + IntegerValue::U32(x >> n_bits) + } + U64(x) => { + if n_bits >= 64 { + return Err(PartialVMError::new(StatusCode::ARITHMETIC_ERROR)); + } + IntegerValue::U64(x >> n_bits) + } + U128(x) => { + if n_bits >= 128 { + return Err(PartialVMError::new(StatusCode::ARITHMETIC_ERROR)); + } + IntegerValue::U128(x >> n_bits) + } + U256(x) => IntegerValue::U256(x >> n_bits), + }) + } + + pub fn lt(self, other: Self) -> PartialVMResult { + use IntegerValue::*; + + Ok(match (self, other) { + (U8(l), U8(r)) => l < r, + (U16(l), U16(r)) => l < r, + (U32(l), U32(r)) => l < r, + (U64(l), U64(r)) => l < r, + (U128(l), U128(r)) => l < r, + (U256(l), U256(r)) => l < r, + (l, r) => { + let msg = format!( + "Cannot compare {:?} and {:?}: incompatible integer types", + l, r + ); + return Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR).with_message(msg)); + } + }) + } + + pub fn le(self, other: Self) -> PartialVMResult { + use IntegerValue::*; + + Ok(match (self, other) { + (U8(l), U8(r)) => l <= r, + (U16(l), U16(r)) => l <= r, + (U32(l), U32(r)) => l <= r, + (U64(l), U64(r)) => l <= r, + (U128(l), U128(r)) => l <= r, + (U256(l), U256(r)) => l <= r, + (l, r) => { + let msg = format!( + "Cannot compare {:?} and {:?}: incompatible integer types", + l, r + ); + return Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR).with_message(msg)); + } + }) + } + + pub fn gt(self, other: Self) -> PartialVMResult { + use IntegerValue::*; + + Ok(match (self, other) { + (U8(l), U8(r)) => l > r, + (U16(l), U16(r)) => l > r, + (U32(l), U32(r)) => l > r, + (U64(l), U64(r)) => l > r, + (U128(l), U128(r)) => l > r, + (U256(l), U256(r)) => l > r, + (l, r) => { + let msg = format!( + "Cannot compare {:?} and {:?}: incompatible integer types", + l, r + ); + return Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR).with_message(msg)); + } + }) + } + + pub fn ge(self, other: Self) -> PartialVMResult { + use IntegerValue::*; + + Ok(match (self, other) { + (U8(l), U8(r)) => l >= r, + (U16(l), U16(r)) => l >= r, + (U32(l), U32(r)) => l >= r, + (U64(l), U64(r)) => l >= r, + (U128(l), U128(r)) => l >= r, + (U256(l), U256(r)) => l >= r, + (l, r) => { + let msg = format!( + "Cannot compare {:?} and {:?}: incompatible integer types", + l, r + ); + return Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR).with_message(msg)); + } + }) + } + + pub fn into_value(self) -> Value { + use IntegerValue::*; + + match self { + U8(x) => Value::u8(x), + U16(x) => Value::u16(x), + U32(x) => Value::u32(x), + U64(x) => Value::u64(x), + U128(x) => Value::u128(x), + U256(x) => Value::u256(x), + } + } +} + +impl IntegerValue { + pub fn cast_u8(self) -> PartialVMResult { + use IntegerValue::*; + + match self { + U8(x) => Ok(x), + U16(x) => { + if x > (u8::MAX as u16) { + Err(PartialVMError::new(StatusCode::ARITHMETIC_ERROR) + .with_message(format!("Cannot cast u16({}) to u8", x))) + } else { + Ok(x as u8) + } + } + U32(x) => { + if x > (u8::MAX as u32) { + Err(PartialVMError::new(StatusCode::ARITHMETIC_ERROR) + .with_message(format!("Cannot cast u32({}) to u8", x))) + } else { + Ok(x as u8) + } + } + U64(x) => { + if x > (u8::MAX as u64) { + Err(PartialVMError::new(StatusCode::ARITHMETIC_ERROR) + .with_message(format!("Cannot cast u64({}) to u8", x))) + } else { + Ok(x as u8) + } + } + U128(x) => { + if x > (u8::MAX as u128) { + Err(PartialVMError::new(StatusCode::ARITHMETIC_ERROR) + .with_message(format!("Cannot cast u128({}) to u8", x))) + } else { + Ok(x as u8) + } + } + U256(x) => { + if x > (u256::U256::from(u8::MAX)) { + Err(PartialVMError::new(StatusCode::ARITHMETIC_ERROR) + .with_message(format!("Cannot cast u256({}) to u8", x))) + } else { + Ok(x.unchecked_as_u8()) + } + } + } + } + + pub fn cast_u16(self) -> PartialVMResult { + use IntegerValue::*; + + match self { + U8(x) => Ok(x as u16), + U16(x) => Ok(x), + U32(x) => { + if x > (u16::MAX as u32) { + Err(PartialVMError::new(StatusCode::ARITHMETIC_ERROR) + .with_message(format!("Cannot cast u32({}) to u16", x))) + } else { + Ok(x as u16) + } + } + U64(x) => { + if x > (u16::MAX as u64) { + Err(PartialVMError::new(StatusCode::ARITHMETIC_ERROR) + .with_message(format!("Cannot cast u64({}) to u16", x))) + } else { + Ok(x as u16) + } + } + U128(x) => { + if x > (u16::MAX as u128) { + Err(PartialVMError::new(StatusCode::ARITHMETIC_ERROR) + .with_message(format!("Cannot cast u128({}) to u16", x))) + } else { + Ok(x as u16) + } + } + U256(x) => { + if x > (u256::U256::from(u16::MAX)) { + Err(PartialVMError::new(StatusCode::ARITHMETIC_ERROR) + .with_message(format!("Cannot cast u256({}) to u16", x))) + } else { + Ok(x.unchecked_as_u16()) + } + } + } + } + + pub fn cast_u32(self) -> PartialVMResult { + use IntegerValue::*; + + match self { + U8(x) => Ok(x as u32), + U16(x) => Ok(x as u32), + U32(x) => Ok(x), + U64(x) => { + if x > (u32::MAX as u64) { + Err(PartialVMError::new(StatusCode::ARITHMETIC_ERROR) + .with_message(format!("Cannot cast u64({}) to u32", x))) + } else { + Ok(x as u32) + } + } + U128(x) => { + if x > (u32::MAX as u128) { + Err(PartialVMError::new(StatusCode::ARITHMETIC_ERROR) + .with_message(format!("Cannot cast u128({}) to u32", x))) + } else { + Ok(x as u32) + } + } + U256(x) => { + if x > (u256::U256::from(u32::MAX)) { + Err(PartialVMError::new(StatusCode::ARITHMETIC_ERROR) + .with_message(format!("Cannot cast u128({}) to u32", x))) + } else { + Ok(x.unchecked_as_u32()) + } + } + } + } + + pub fn cast_u64(self) -> PartialVMResult { + use IntegerValue::*; + + match self { + U8(x) => Ok(x as u64), + U16(x) => Ok(x as u64), + U32(x) => Ok(x as u64), + U64(x) => Ok(x), + U128(x) => { + if x > (u64::MAX as u128) { + Err(PartialVMError::new(StatusCode::ARITHMETIC_ERROR) + .with_message(format!("Cannot cast u128({}) to u64", x))) + } else { + Ok(x as u64) + } + } + U256(x) => { + if x > (u256::U256::from(u64::MAX)) { + Err(PartialVMError::new(StatusCode::ARITHMETIC_ERROR) + .with_message(format!("Cannot cast u256({}) to u64", x))) + } else { + Ok(x.unchecked_as_u64()) + } + } + } + } + + pub fn cast_u128(self) -> PartialVMResult { + use IntegerValue::*; + + match self { + U8(x) => Ok(x as u128), + U16(x) => Ok(x as u128), + U32(x) => Ok(x as u128), + U64(x) => Ok(x as u128), + U128(x) => Ok(x), + U256(x) => { + if x > (u256::U256::from(u128::MAX)) { + Err(PartialVMError::new(StatusCode::ARITHMETIC_ERROR) + .with_message(format!("Cannot cast u256({}) to u128", x))) + } else { + Ok(x.unchecked_as_u128()) + } + } + } + } + + pub fn cast_u256(self) -> PartialVMResult { + use IntegerValue::*; + + Ok(match self { + U8(x) => u256::U256::from(x), + U16(x) => u256::U256::from(x), + U32(x) => u256::U256::from(x), + U64(x) => u256::U256::from(x), + U128(x) => u256::U256::from(x), + U256(x) => x, + }) + } +} + +/*************************************************************************************** +* +* Vector +* +* Implemented as a built-in data type. +* +**************************************************************************************/ + +pub const INDEX_OUT_OF_BOUNDS: u64 = NFE_VECTOR_ERROR_BASE + 1; +pub const POP_EMPTY_VEC: u64 = NFE_VECTOR_ERROR_BASE + 2; +pub const VEC_UNPACK_PARITY_MISMATCH: u64 = NFE_VECTOR_ERROR_BASE + 3; +pub const VEC_SIZE_LIMIT_REACHED: u64 = NFE_VECTOR_ERROR_BASE + 4; + +fn check_elem_layout(ty: &Type, v: &Container) -> PartialVMResult<()> { + match (ty, v) { + (Type::U8, Container::VecU8(_)) + | (Type::U64, Container::VecU64(_)) + | (Type::U16, Container::VecU16(_)) + | (Type::U32, Container::VecU32(_)) + | (Type::U128, Container::VecU128(_)) + | (Type::U256, Container::VecU256(_)) + | (Type::Bool, Container::VecBool(_)) + | (Type::Address, Container::VecAddress(_)) + | (Type::Signer, Container::Struct(_)) => Ok(()), + + (Type::Vector(_), Container::Vec(_)) => Ok(()), + + (Type::Datatype(_), Container::Vec(_)) + | (Type::Signer, Container::Vec(_)) + | (Type::DatatypeInstantiation(_), Container::Vec(_)) => Ok(()), + + (Type::Reference(_), _) | (Type::MutableReference(_), _) | (Type::TyParam(_), _) => Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message(format!("invalid type param for vector: {:?}", ty)), + ), + + (Type::U8, _) + | (Type::U64, _) + | (Type::U16, _) + | (Type::U32, _) + | (Type::U128, _) + | (Type::U256, _) + | (Type::Bool, _) + | (Type::Address, _) + | (Type::Signer, _) + | (Type::Vector(_), _) + | (Type::Datatype(_), _) + | (Type::DatatypeInstantiation(_), _) => Err(PartialVMError::new( + StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR, + ) + .with_message(format!( + "vector elem layout mismatch, expected {:?}, got {:?}", + ty, v + ))), + } +} + +impl VectorRef { + pub fn len(&self, type_param: &Type) -> PartialVMResult { + let c = self.0.container(); + check_elem_layout(type_param, c)?; + + let len = match c { + Container::VecU8(r) => r.borrow().len(), + Container::VecU16(r) => r.borrow().len(), + Container::VecU32(r) => r.borrow().len(), + Container::VecU64(r) => r.borrow().len(), + Container::VecU128(r) => r.borrow().len(), + Container::VecU256(r) => r.borrow().len(), + Container::VecBool(r) => r.borrow().len(), + Container::VecAddress(r) => r.borrow().len(), + Container::Vec(r) => r.borrow().len(), + Container::Locals(_) | Container::Struct(_) | Container::Variant { .. } => { + unreachable!() + } + }; + Ok(Value::u64(len as u64)) + } + + pub fn push_back(&self, e: Value, type_param: &Type, capacity: u64) -> PartialVMResult<()> { + let c = self.0.container(); + check_elem_layout(type_param, c)?; + + let size = match c { + Container::VecU8(r) => r.borrow().len(), + Container::VecU16(r) => r.borrow().len(), + Container::VecU32(r) => r.borrow().len(), + Container::VecU64(r) => r.borrow().len(), + Container::VecU128(r) => r.borrow().len(), + Container::VecU256(r) => r.borrow().len(), + Container::VecBool(r) => r.borrow().len(), + Container::VecAddress(r) => r.borrow().len(), + Container::Vec(r) => r.borrow().len(), + Container::Locals(_) | Container::Struct(_) | Container::Variant { .. } => { + unreachable!() + } + }; + if size >= (capacity as usize) { + return Err(PartialVMError::new(StatusCode::VECTOR_OPERATION_ERROR) + .with_sub_status(VEC_SIZE_LIMIT_REACHED) + .with_message(format!("vector size limit is {capacity}",))); + } + + match c { + Container::VecU8(r) => r.borrow_mut().push(e.value_as()?), + Container::VecU16(r) => r.borrow_mut().push(e.value_as()?), + Container::VecU32(r) => r.borrow_mut().push(e.value_as()?), + Container::VecU64(r) => r.borrow_mut().push(e.value_as()?), + Container::VecU128(r) => r.borrow_mut().push(e.value_as()?), + Container::VecU256(r) => r.borrow_mut().push(e.value_as()?), + Container::VecBool(r) => r.borrow_mut().push(e.value_as()?), + Container::VecAddress(r) => r.borrow_mut().push(e.value_as()?), + Container::Vec(r) => r.borrow_mut().push(e.0), + Container::Locals(_) | Container::Struct(_) | Container::Variant { .. } => { + unreachable!() + } + } + + self.0.mark_dirty(); + Ok(()) + } + + pub fn borrow_elem(&self, idx: usize, type_param: &Type) -> PartialVMResult { + let c = self.0.container(); + check_elem_layout(type_param, c)?; + if idx >= c.len() { + return Err(PartialVMError::new(StatusCode::VECTOR_OPERATION_ERROR) + .with_sub_status(INDEX_OUT_OF_BOUNDS)); + } + Ok(Value(self.0.borrow_elem(idx)?)) + } + + /// Returns a RefCell reference to the underlying vector of a `&vector` value. + pub fn as_bytes_ref(&self) -> std::cell::Ref<'_, Vec> { + let c = self.0.container(); + match c { + Container::VecU8(r) => r.borrow(), + _ => panic!("can only be called on vector"), + } + } + + pub fn pop(&self, type_param: &Type) -> PartialVMResult { + let c = self.0.container(); + check_elem_layout(type_param, c)?; + + macro_rules! err_pop_empty_vec { + () => { + return Err(PartialVMError::new(StatusCode::VECTOR_OPERATION_ERROR) + .with_sub_status(POP_EMPTY_VEC)) + }; + } + + let res = match c { + Container::VecU8(r) => match r.borrow_mut().pop() { + Some(x) => Value::u8(x), + None => err_pop_empty_vec!(), + }, + Container::VecU16(r) => match r.borrow_mut().pop() { + Some(x) => Value::u16(x), + None => err_pop_empty_vec!(), + }, + Container::VecU32(r) => match r.borrow_mut().pop() { + Some(x) => Value::u32(x), + None => err_pop_empty_vec!(), + }, + Container::VecU64(r) => match r.borrow_mut().pop() { + Some(x) => Value::u64(x), + None => err_pop_empty_vec!(), + }, + Container::VecU128(r) => match r.borrow_mut().pop() { + Some(x) => Value::u128(x), + None => err_pop_empty_vec!(), + }, + Container::VecU256(r) => match r.borrow_mut().pop() { + Some(x) => Value::u256(x), + None => err_pop_empty_vec!(), + }, + Container::VecBool(r) => match r.borrow_mut().pop() { + Some(x) => Value::bool(x), + None => err_pop_empty_vec!(), + }, + Container::VecAddress(r) => match r.borrow_mut().pop() { + Some(x) => Value::address(x), + None => err_pop_empty_vec!(), + }, + Container::Vec(r) => match r.borrow_mut().pop() { + Some(x) => Value(x), + None => err_pop_empty_vec!(), + }, + Container::Locals(_) | Container::Struct(_) | Container::Variant { .. } => { + unreachable!() + } + }; + + self.0.mark_dirty(); + Ok(res) + } + + pub fn swap(&self, idx1: usize, idx2: usize, type_param: &Type) -> PartialVMResult<()> { + let c = self.0.container(); + check_elem_layout(type_param, c)?; + + macro_rules! swap { + ($v: expr) => {{ + let mut v = $v.borrow_mut(); + if idx1 >= v.len() || idx2 >= v.len() { + return Err(PartialVMError::new(StatusCode::VECTOR_OPERATION_ERROR) + .with_sub_status(INDEX_OUT_OF_BOUNDS)); + } + v.swap(idx1, idx2); + }}; + } + + match c { + Container::VecU8(r) => swap!(r), + Container::VecU16(r) => swap!(r), + Container::VecU32(r) => swap!(r), + Container::VecU64(r) => swap!(r), + Container::VecU128(r) => swap!(r), + Container::VecU256(r) => swap!(r), + Container::VecBool(r) => swap!(r), + Container::VecAddress(r) => swap!(r), + Container::Vec(r) => swap!(r), + Container::Locals(_) | Container::Struct(_) | Container::Variant { .. } => { + unreachable!() + } + } + + self.0.mark_dirty(); + Ok(()) + } +} + +pub enum VectorSpecialization { + U8, + U16, + U32, + U64, + U128, + U256, + Bool, + Address, + Container, +} + +impl TryFrom<&Type> for VectorSpecialization { + type Error = PartialVMError; + + fn try_from(ty: &Type) -> Result { + Ok(match ty { + Type::U8 => VectorSpecialization::U8, + Type::U16 => VectorSpecialization::U16, + Type::U32 => VectorSpecialization::U32, + Type::U64 => VectorSpecialization::U64, + Type::U128 => VectorSpecialization::U128, + Type::U256 => VectorSpecialization::U256, + Type::Bool => VectorSpecialization::Bool, + Type::Address => VectorSpecialization::Address, + Type::Vector(_) | Type::Signer | Type::Datatype(_) | Type::DatatypeInstantiation(_) => { + VectorSpecialization::Container + } + Type::Reference(_) | Type::MutableReference(_) | Type::TyParam(_) => { + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message(format!("invalid type param for vector: {:?}", ty)), + ); + } + }) + } +} + +impl Vector { + pub fn pack( + specialization: VectorSpecialization, + elements: impl IntoIterator, + ) -> PartialVMResult { + let container = match specialization { + VectorSpecialization::U8 => Value::vector_u8( + elements + .into_iter() + .map(|v| v.value_as()) + .collect::>>()?, + ), + VectorSpecialization::U16 => Value::vector_u16( + elements + .into_iter() + .map(|v| v.value_as()) + .collect::>>()?, + ), + VectorSpecialization::U32 => Value::vector_u32( + elements + .into_iter() + .map(|v| v.value_as()) + .collect::>>()?, + ), + VectorSpecialization::U64 => Value::vector_u64( + elements + .into_iter() + .map(|v| v.value_as()) + .collect::>>()?, + ), + VectorSpecialization::U128 => Value::vector_u128( + elements + .into_iter() + .map(|v| v.value_as()) + .collect::>>()?, + ), + VectorSpecialization::U256 => Value::vector_u256( + elements + .into_iter() + .map(|v| v.value_as()) + .collect::>>()?, + ), + VectorSpecialization::Bool => Value::vector_bool( + elements + .into_iter() + .map(|v| v.value_as()) + .collect::>>()?, + ), + VectorSpecialization::Address => Value::vector_address( + elements + .into_iter() + .map(|v| v.value_as()) + .collect::>>()?, + ), + + VectorSpecialization::Container => Value(ValueImpl::Container(Container::Vec( + Rc::new(RefCell::new(elements.into_iter().map(|v| v.0).collect())), + ))), + }; + + Ok(container) + } + + pub fn empty(specialization: VectorSpecialization) -> PartialVMResult { + Self::pack(specialization, vec![]) + } + + pub fn unpack(self, type_param: &Type, expected_num: u64) -> PartialVMResult> { + check_elem_layout(type_param, &self.0)?; + let elements: Vec<_> = match self.0 { + Container::VecU8(r) => take_unique_ownership(r)? + .into_iter() + .map(Value::u8) + .collect(), + Container::VecU16(r) => take_unique_ownership(r)? + .into_iter() + .map(Value::u16) + .collect(), + Container::VecU32(r) => take_unique_ownership(r)? + .into_iter() + .map(Value::u32) + .collect(), + Container::VecU64(r) => take_unique_ownership(r)? + .into_iter() + .map(Value::u64) + .collect(), + Container::VecU128(r) => take_unique_ownership(r)? + .into_iter() + .map(Value::u128) + .collect(), + Container::VecU256(r) => take_unique_ownership(r)? + .into_iter() + .map(Value::u256) + .collect(), + Container::VecBool(r) => take_unique_ownership(r)? + .into_iter() + .map(Value::bool) + .collect(), + Container::VecAddress(r) => take_unique_ownership(r)? + .into_iter() + .map(Value::address) + .collect(), + Container::Vec(r) => take_unique_ownership(r)?.into_iter().map(Value).collect(), + Container::Locals(_) | Container::Struct(_) | Container::Variant { .. } => { + unreachable!() + } + }; + if expected_num as usize == elements.len() { + Ok(elements) + } else { + Err(PartialVMError::new(StatusCode::VECTOR_OPERATION_ERROR) + .with_sub_status(VEC_UNPACK_PARITY_MISMATCH)) + } + } + + pub fn destroy_empty(self, type_param: &Type) -> PartialVMResult<()> { + self.unpack(type_param, 0)?; + Ok(()) + } + + pub fn to_vec_u8(self) -> PartialVMResult> { + check_elem_layout(&Type::U8, &self.0)?; + if let Container::VecU8(r) = self.0 { + Ok(take_unique_ownership(r)?.into_iter().collect()) + } else { + Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("expected vector".to_string()), + ) + } + } +} + +/*************************************************************************************** + * + * Abstract Memory Size + * + * TODO(Gas): This is the oldest implementation of abstract memory size. + * It is now kept only as a reference impl, which is used to ensure + * the new implementation is fully backward compatible. + * We should be able to get this removed after we use the new impl + * for a while and gain enough confidence in that. + * + **************************************************************************************/ + +/// The size in bytes for a non-string or address constant on the stack +pub(crate) const LEGACY_CONST_SIZE: AbstractMemorySize = AbstractMemorySize::new(16); + +/// The size in bytes for a reference on the stack +pub(crate) const LEGACY_REFERENCE_SIZE: AbstractMemorySize = AbstractMemorySize::new(8); + +/// The size of a struct in bytes +pub(crate) const LEGACY_STRUCT_SIZE: AbstractMemorySize = AbstractMemorySize::new(2); + +impl Container { + fn legacy_size(&self) -> AbstractMemorySize { + match self { + Self::Locals(r) | Self::Vec(r) | Self::Struct(r) => { + Struct::legacy_size_impl(&r.borrow()) + } + Self::Variant(r) => Variant::legacy_size_impl(&r.borrow().1), + Self::VecU8(r) => { + AbstractMemorySize::new((r.borrow().len() * std::mem::size_of::()) as u64) + } + Self::VecU16(r) => { + AbstractMemorySize::new((r.borrow().len() * std::mem::size_of::()) as u64) + } + Self::VecU32(r) => { + AbstractMemorySize::new((r.borrow().len() * std::mem::size_of::()) as u64) + } + Self::VecU64(r) => { + AbstractMemorySize::new((r.borrow().len() * std::mem::size_of::()) as u64) + } + Self::VecU128(r) => { + AbstractMemorySize::new((r.borrow().len() * std::mem::size_of::()) as u64) + } + Self::VecU256(r) => AbstractMemorySize::new( + (r.borrow().len() * std::mem::size_of::()) as u64, + ), + Self::VecBool(r) => { + AbstractMemorySize::new((r.borrow().len() * std::mem::size_of::()) as u64) + } + Self::VecAddress(r) => AbstractMemorySize::new( + (r.borrow().len() * std::mem::size_of::()) as u64, + ), + } + } +} + +impl ContainerRef { + fn legacy_size(&self) -> AbstractMemorySize { + LEGACY_REFERENCE_SIZE + } +} + +impl IndexedRef { + fn legacy_size(&self) -> AbstractMemorySize { + LEGACY_REFERENCE_SIZE + } +} + +impl ValueImpl { + fn legacy_size(&self) -> AbstractMemorySize { + use ValueImpl::*; + + match self { + Invalid | U8(_) | U16(_) | U32(_) | U64(_) | U128(_) | U256(_) | Bool(_) => { + LEGACY_CONST_SIZE + } + Address(_) => AbstractMemorySize::new(AccountAddress::LENGTH as u64), + ContainerRef(r) => r.legacy_size(), + IndexedRef(r) => r.legacy_size(), + // TODO: in case the borrow fails the VM will panic. + Container(c) => c.legacy_size(), + } + } +} + +impl Variant { + const TAG_SIZE: AbstractMemorySize = AbstractMemorySize::new(std::mem::size_of::() as u64); + + fn legacy_size_impl(fields: &[ValueImpl]) -> AbstractMemorySize { + fields + .iter() + .fold(LEGACY_STRUCT_SIZE.add(Self::TAG_SIZE), |acc, v| { + acc + v.legacy_size() + }) + } +} + +impl Struct { + fn legacy_size_impl(fields: &[ValueImpl]) -> AbstractMemorySize { + fields + .iter() + .fold(LEGACY_STRUCT_SIZE, |acc, v| acc + v.legacy_size()) + } + + #[cfg(test)] + pub(crate) fn legacy_size(&self) -> AbstractMemorySize { + Self::legacy_size_impl(&self.fields) + } +} + +impl Value { + pub fn legacy_size(&self) -> AbstractMemorySize { + self.0.legacy_size() + } +} + +#[cfg(test)] +impl ReferenceImpl { + fn legacy_size(&self) -> AbstractMemorySize { + match self { + Self::ContainerRef(r) => r.legacy_size(), + Self::IndexedRef(r) => r.legacy_size(), + } + } +} + +#[cfg(test)] +impl Reference { + pub(crate) fn legacy_size(&self) -> AbstractMemorySize { + self.0.legacy_size() + } +} + +/*************************************************************************************** + * + * Struct Operations + * + * Public APIs for Struct. + * + **************************************************************************************/ +impl Struct { + pub fn pack>(vals: I) -> Self { + Self { + fields: vals.into_iter().map(|v| v.0).collect(), + } + } + + pub fn unpack(self) -> PartialVMResult> { + Ok(self.fields.into_iter().map(Value)) + } +} + +/*************************************************************************************** + * + * Variant Operations + * + * Public APIs for Enums. + * + **************************************************************************************/ +impl Variant { + pub fn pack>(tag: VariantTag, vals: I) -> Self { + Self { + tag, + fields: vals.into_iter().map(|v| v.0).collect(), + } + } + + pub fn unpack(self) -> PartialVMResult> { + Ok(self.fields.into_iter().map(Value)) + } + + pub fn check_tag(&self, tag: VariantTag) -> PartialVMResult<()> { + if tag != self.tag { + Err(PartialVMError::new(StatusCode::VARIANT_TAG_MISMATCH) + .with_message(format!("tag mismatch: expected {}, got {}", tag, self.tag))) + } else { + Ok(()) + } + } +} + +/*************************************************************************************** + * + * Global Value Operations + * + * Public APIs for GlobalValue. They allow global values to be created from external + * source (a.k.a. storage), and references to be taken from them. At the end of the + * transaction execution the dirty ones can be identified and wrote back to storage. + * + **************************************************************************************/ +#[allow(clippy::unnecessary_wraps)] +impl GlobalValueImpl { + fn cached( + val: ValueImpl, + status: GlobalDataStatus, + ) -> Result { + match val { + ValueImpl::Container(Container::Struct(fields)) => { + let status = Rc::new(RefCell::new(status)); + Ok(Self::Cached { fields, status }) + } + val => Err(( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("failed to publish cached: not a resource".to_string()), + val, + )), + } + } + + fn fresh(val: ValueImpl) -> Result { + match val { + ValueImpl::Container(Container::Struct(fields)) => Ok(Self::Fresh { fields }), + val => Err(( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("failed to publish fresh: not a resource".to_string()), + val, + )), + } + } + + fn move_from(&mut self) -> PartialVMResult { + let fields = match self { + Self::None | Self::Deleted => { + return Err(PartialVMError::new(StatusCode::MISSING_DATA)); + } + Self::Fresh { .. } => match std::mem::replace(self, Self::None) { + Self::Fresh { fields } => fields, + _ => unreachable!(), + }, + Self::Cached { .. } => match std::mem::replace(self, Self::Deleted) { + Self::Cached { fields, .. } => fields, + _ => unreachable!(), + }, + }; + if Rc::strong_count(&fields) != 1 { + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("moving global resource with dangling reference".to_string()), + ); + } + Ok(ValueImpl::Container(Container::Struct(fields))) + } + + fn move_to(&mut self, val: ValueImpl) -> Result<(), (PartialVMError, ValueImpl)> { + match self { + Self::Fresh { .. } | Self::Cached { .. } => { + return Err(( + PartialVMError::new(StatusCode::RESOURCE_ALREADY_EXISTS), + val, + )); + } + Self::None => *self = Self::fresh(val)?, + Self::Deleted => *self = Self::cached(val, GlobalDataStatus::Dirty)?, + } + Ok(()) + } + + fn exists(&self) -> PartialVMResult { + match self { + Self::Fresh { .. } | Self::Cached { .. } => Ok(true), + Self::None | Self::Deleted => Ok(false), + } + } + + fn borrow_global(&self) -> PartialVMResult { + match self { + Self::None | Self::Deleted => Err(PartialVMError::new(StatusCode::MISSING_DATA)), + Self::Fresh { fields } => Ok(ValueImpl::ContainerRef(Box::new(ContainerRef::Local( + Container::Struct(Rc::clone(fields)), + )))), + Self::Cached { fields, status } => { + Ok(ValueImpl::ContainerRef(Box::new(ContainerRef::Global { + container: Container::Struct(Rc::clone(fields)), + status: Rc::clone(status), + }))) + } + } + } + + fn into_effect(self) -> Option> { + match self { + Self::None => None, + Self::Deleted => Some(Op::Delete), + Self::Fresh { fields } => { + Some(Op::New(ValueImpl::Container(Container::Struct(fields)))) + } + Self::Cached { fields, status } => match &*status.borrow() { + GlobalDataStatus::Dirty => { + Some(Op::Modify(ValueImpl::Container(Container::Struct(fields)))) + } + GlobalDataStatus::Clean => None, + }, + } + } + + fn is_mutated(&self) -> bool { + match self { + Self::None => false, + Self::Deleted => true, + Self::Fresh { fields: _ } => true, + Self::Cached { fields: _, status } => match &*status.borrow() { + GlobalDataStatus::Dirty => true, + GlobalDataStatus::Clean => false, + }, + } + } + + fn into_value(self) -> Option { + match self { + Self::None | Self::Deleted => None, + Self::Fresh { fields } | Self::Cached { fields, .. } => { + Some(ValueImpl::Container(Container::Struct(fields))) + } + } + } +} + +impl GlobalValue { + pub fn none() -> Self { + Self(GlobalValueImpl::None) + } + + pub fn cached(val: Value) -> PartialVMResult { + Ok(Self( + GlobalValueImpl::cached(val.0, GlobalDataStatus::Clean).map_err(|(err, _val)| err)?, + )) + } + + pub fn move_from(&mut self) -> PartialVMResult { + Ok(Value(self.0.move_from()?)) + } + + pub fn move_to(&mut self, val: Value) -> Result<(), (PartialVMError, Value)> { + self.0 + .move_to(val.0) + .map_err(|(err, val)| (err, Value(val))) + } + + pub fn borrow_global(&self) -> PartialVMResult { + Ok(Value(self.0.borrow_global()?)) + } + + pub fn exists(&self) -> PartialVMResult { + self.0.exists() + } + + pub fn into_effect(self) -> Option> { + self.0.into_effect().map(|op| op.map(Value)) + } + + pub fn is_mutated(&self) -> bool { + self.0.is_mutated() + } + + pub fn into_value(self) -> Option { + self.0.into_value().map(Value) + } +} + +/*************************************************************************************** +* +* Display +* +* Implementation of the Display trait for VM Values. These are supposed to be more +* friendly & readable than the generated Debug dump. +* +**************************************************************************************/ + +impl Display for ValueImpl { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Invalid => write!(f, "Invalid"), + + Self::U8(x) => write!(f, "U8({})", x), + Self::U16(x) => write!(f, "U16({})", x), + Self::U32(x) => write!(f, "U32({})", x), + Self::U64(x) => write!(f, "U64({})", x), + Self::U128(x) => write!(f, "U128({})", x), + Self::U256(x) => write!(f, "U256({})", x), + Self::Bool(x) => write!(f, "{}", x), + Self::Address(addr) => write!(f, "Address({})", addr.short_str_lossless()), + + Self::Container(r) => write!(f, "{}", r), + + Self::ContainerRef(r) => write!(f, "{}", r), + Self::IndexedRef(r) => write!(f, "{}", r), + } + } +} + +fn display_list_of_items(items: I, f: &mut fmt::Formatter) -> fmt::Result +where + T: Display, + I: IntoIterator, +{ + write!(f, "[")?; + let mut items = items.into_iter(); + if let Some(x) = items.next() { + write!(f, "{}", x)?; + for x in items { + write!(f, ", {}", x)?; + } + } + write!(f, "]") +} + +impl Container { + fn raw_address(&self) -> usize { + use Container::*; + + match self { + Locals(r) => r.as_ptr() as usize, + Vec(r) => r.as_ptr() as usize, + Struct(r) => r.as_ptr() as usize, + VecU8(r) => r.as_ptr() as usize, + VecU16(r) => r.as_ptr() as usize, + VecU32(r) => r.as_ptr() as usize, + VecU64(r) => r.as_ptr() as usize, + VecU128(r) => r.as_ptr() as usize, + VecU256(r) => r.as_ptr() as usize, + VecBool(r) => r.as_ptr() as usize, + VecAddress(r) => r.as_ptr() as usize, + Variant(r) => r.as_ptr() as usize, + } + } +} + +impl Locals { + pub fn raw_address(&self) -> usize { + self.0.as_ptr() as usize + } +} + +impl Display for ContainerRef { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Local(c) => write!(f, "(&container {:x})", c.raw_address()), + Self::Global { status, container } => write!( + f, + "(&container {:x} -- {:?})", + container.raw_address(), + &*status.borrow(), + ), + } + } +} + +impl Display for IndexedRef { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}[{}]", self.container_ref, self.idx) + } +} + +impl Display for Container { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "(container {:x}: ", self.raw_address())?; + + match self { + Self::Locals(r) | Self::Vec(r) | Self::Struct(r) => { + display_list_of_items(r.borrow().iter(), f) + } + Self::Variant(r) => { + let (tag, values) = &*r.borrow(); + write!(f, "|tag: {}|", tag)?; + display_list_of_items(values, f) + } + Self::VecU8(r) => display_list_of_items(r.borrow().iter(), f), + Self::VecU16(r) => display_list_of_items(r.borrow().iter(), f), + Self::VecU32(r) => display_list_of_items(r.borrow().iter(), f), + Self::VecU64(r) => display_list_of_items(r.borrow().iter(), f), + Self::VecU128(r) => display_list_of_items(r.borrow().iter(), f), + Self::VecU256(r) => display_list_of_items(r.borrow().iter(), f), + Self::VecBool(r) => display_list_of_items(r.borrow().iter(), f), + Self::VecAddress(r) => display_list_of_items(r.borrow().iter(), f), + }?; + + write!(f, ")") + } +} + +impl Display for Value { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + Display::fmt(&self.0, f) + } +} + +impl Display for Locals { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{}", + self.0 + .borrow() + .iter() + .enumerate() + .map(|(idx, val)| format!("[{}] {}", idx, val)) + .collect::>() + .join("\n") + ) + } +} + +#[allow(dead_code)] +pub mod debug { + use super::*; + use std::fmt::Write; + + fn print_invalid(buf: &mut B) -> PartialVMResult<()> { + debug_write!(buf, "-") + } + + fn print_u8(buf: &mut B, x: &u8) -> PartialVMResult<()> { + debug_write!(buf, "{}", x) + } + + fn print_u16(buf: &mut B, x: &u16) -> PartialVMResult<()> { + debug_write!(buf, "{}", x) + } + + fn print_u32(buf: &mut B, x: &u32) -> PartialVMResult<()> { + debug_write!(buf, "{}", x) + } + + fn print_u64(buf: &mut B, x: &u64) -> PartialVMResult<()> { + debug_write!(buf, "{}", x) + } + + fn print_u128(buf: &mut B, x: &u128) -> PartialVMResult<()> { + debug_write!(buf, "{}", x) + } + + fn print_u256(buf: &mut B, x: &u256::U256) -> PartialVMResult<()> { + debug_write!(buf, "{}", x) + } + + fn print_bool(buf: &mut B, x: &bool) -> PartialVMResult<()> { + debug_write!(buf, "{}", x) + } + + fn print_address(buf: &mut B, x: &AccountAddress) -> PartialVMResult<()> { + debug_write!(buf, "{}", x) + } + + fn print_value_impl(buf: &mut B, val: &ValueImpl) -> PartialVMResult<()> { + match val { + ValueImpl::Invalid => print_invalid(buf), + + ValueImpl::U8(x) => print_u8(buf, x), + ValueImpl::U16(x) => print_u16(buf, x), + ValueImpl::U32(x) => print_u32(buf, x), + ValueImpl::U64(x) => print_u64(buf, x), + ValueImpl::U128(x) => print_u128(buf, x), + ValueImpl::U256(x) => print_u256(buf, x), + ValueImpl::Bool(x) => print_bool(buf, x), + ValueImpl::Address(x) => print_address(buf, x), + + ValueImpl::Container(c) => print_container(buf, c), + + ValueImpl::ContainerRef(r) => print_container_ref(buf, r), + ValueImpl::IndexedRef(r) => print_indexed_ref(buf, r), + } + } + + fn print_list<'a, B, I, X, F>( + buf: &mut B, + begin: &str, + items: I, + print: F, + end: &str, + ) -> PartialVMResult<()> + where + B: Write, + X: 'a, + I: IntoIterator, + F: Fn(&mut B, &X) -> PartialVMResult<()>, + { + debug_write!(buf, "{}", begin)?; + let mut it = items.into_iter(); + if let Some(x) = it.next() { + print(buf, x)?; + for x in it { + debug_write!(buf, ", ")?; + print(buf, x)?; + } + } + debug_write!(buf, "{}", end)?; + Ok(()) + } + + fn print_container(buf: &mut B, c: &Container) -> PartialVMResult<()> { + match c { + Container::Vec(r) => print_list(buf, "[", r.borrow().iter(), print_value_impl, "]"), + + Container::Struct(r) => { + print_list(buf, "{ ", r.borrow().iter(), print_value_impl, " }") + } + + Container::Variant(r) => { + let (tag, values) = &*r.borrow(); + print_list( + buf, + &format!("|{}|{{ ", tag), + values, + print_value_impl, + " }", + ) + } + + Container::VecU8(r) => print_list(buf, "[", r.borrow().iter(), print_u8, "]"), + Container::VecU16(r) => print_list(buf, "[", r.borrow().iter(), print_u16, "]"), + Container::VecU32(r) => print_list(buf, "[", r.borrow().iter(), print_u32, "]"), + Container::VecU64(r) => print_list(buf, "[", r.borrow().iter(), print_u64, "]"), + Container::VecU128(r) => print_list(buf, "[", r.borrow().iter(), print_u128, "]"), + Container::VecU256(r) => print_list(buf, "[", r.borrow().iter(), print_u256, "]"), + Container::VecBool(r) => print_list(buf, "[", r.borrow().iter(), print_bool, "]"), + Container::VecAddress(r) => print_list(buf, "[", r.borrow().iter(), print_address, "]"), + + Container::Locals(_) => Err(PartialVMError::new( + StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR, + ) + .with_message("debug print - invalid container: Locals".to_string())), + } + } + + fn print_container_ref(buf: &mut B, r: &ContainerRef) -> PartialVMResult<()> { + debug_write!(buf, "(&) ")?; + print_container(buf, r.container()) + } + + fn print_slice_elem(buf: &mut B, v: &[X], idx: usize, print: F) -> PartialVMResult<()> + where + B: Write, + F: FnOnce(&mut B, &X) -> PartialVMResult<()>, + { + match v.get(idx) { + Some(x) => print(buf, x), + None => Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("ref index out of bounds".to_string()), + ), + } + } + + fn print_indexed_ref(buf: &mut B, r: &IndexedRef) -> PartialVMResult<()> { + let idx = r.idx; + match r.container_ref.container() { + Container::Locals(r) | Container::Vec(r) | Container::Struct(r) => { + print_slice_elem(buf, &r.borrow(), idx, print_value_impl) + } + Container::Variant(r) => { + let (tag, values) = &*r.borrow(); + debug_write!(buf, "|{}|", tag)?; + print_slice_elem(buf, values, idx, print_value_impl) + } + + Container::VecU8(r) => print_slice_elem(buf, &r.borrow(), idx, print_u8), + Container::VecU16(r) => print_slice_elem(buf, &r.borrow(), idx, print_u16), + Container::VecU32(r) => print_slice_elem(buf, &r.borrow(), idx, print_u32), + Container::VecU64(r) => print_slice_elem(buf, &r.borrow(), idx, print_u64), + Container::VecU128(r) => print_slice_elem(buf, &r.borrow(), idx, print_u128), + Container::VecU256(r) => print_slice_elem(buf, &r.borrow(), idx, print_u256), + Container::VecBool(r) => print_slice_elem(buf, &r.borrow(), idx, print_bool), + Container::VecAddress(r) => print_slice_elem(buf, &r.borrow(), idx, print_address), + } + } + + // TODO: This function was used in an old implementation of std::debug::print, and can probably be removed. + pub fn print_reference(buf: &mut B, r: &Reference) -> PartialVMResult<()> { + match &r.0 { + ReferenceImpl::ContainerRef(r) => print_container_ref(buf, r), + ReferenceImpl::IndexedRef(r) => print_indexed_ref(buf, r), + } + } + + pub fn print_locals(buf: &mut B, locals: &Locals) -> PartialVMResult<()> { + // REVIEW: The number of spaces in the indent is currently hard coded. + for (idx, val) in locals.0.borrow().iter().enumerate() { + debug_write!(buf, " [{}] ", idx)?; + print_value_impl(buf, val)?; + debug_writeln!(buf)?; + } + Ok(()) + } + + pub fn print_value(buf: &mut B, val: &Value) -> PartialVMResult<()> { + print_value_impl(buf, &val.0) + } +} + +/*************************************************************************************** + * + * Serialization & Deserialization + * + * BCS implementation for VM values. Note although values are represented as Rust + * enums that carry type info in the tags, we should NOT rely on them for + * serialization: + * 1) Depending on the specific internal representation, it may be impossible to + * reconstruct the layout from a value. For example, one cannot tell if a general + * container is a struct or a value. + * 2) Even if 1) is not a problem at a certain time, we may change to a different + * internal representation that breaks the 1-1 mapping. Extremely speaking, if + * we switch to untagged unions one day, none of the type info will be carried + * by the value. + * + * Therefore the appropriate & robust way to implement serialization & deserialization + * is to involve an explicit representation of the type layout. + * + **************************************************************************************/ +use serde::{ + Deserialize, + de::Error as DeError, + ser::{Error as SerError, SerializeSeq, SerializeTuple}, +}; + +impl Value { + pub fn simple_deserialize(blob: &[u8], layout: &MoveTypeLayout) -> Option { + bcs::from_bytes_seed(SeedWrapper { layout }, blob).ok() + } + + pub fn typed_serialize(&self, layout: &MoveTypeLayout) -> Option> { + bcs::to_bytes(&AnnotatedValue { + layout, + val: &self.0, + }) + .ok() + } +} + +struct AnnotatedValue<'a, 'b, T1, T2> { + layout: &'a T1, + val: &'b T2, +} + +fn invariant_violation(message: String) -> S::Error { + S::Error::custom( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR).with_message(message), + ) +} + +impl serde::Serialize for AnnotatedValue<'_, '_, MoveTypeLayout, ValueImpl> { + fn serialize(&self, serializer: S) -> Result { + match (self.layout, self.val) { + (MoveTypeLayout::U8, ValueImpl::U8(x)) => serializer.serialize_u8(*x), + (MoveTypeLayout::U16, ValueImpl::U16(x)) => serializer.serialize_u16(*x), + (MoveTypeLayout::U32, ValueImpl::U32(x)) => serializer.serialize_u32(*x), + (MoveTypeLayout::U64, ValueImpl::U64(x)) => serializer.serialize_u64(*x), + (MoveTypeLayout::U128, ValueImpl::U128(x)) => serializer.serialize_u128(**x), + (MoveTypeLayout::U256, ValueImpl::U256(x)) => x.serialize(serializer), + (MoveTypeLayout::Bool, ValueImpl::Bool(x)) => serializer.serialize_bool(*x), + (MoveTypeLayout::Address, ValueImpl::Address(x)) => x.serialize(serializer), + + (MoveTypeLayout::Struct(struct_layout), ValueImpl::Container(Container::Struct(r))) => { + (AnnotatedValue { + layout: &**struct_layout, + val: &*r.borrow(), + }) + .serialize(serializer) + } + + (MoveTypeLayout::Enum(enum_layout), ValueImpl::Container(Container::Variant(r))) => { + (AnnotatedValue { + layout: &**enum_layout, + val: &*r.borrow(), + }) + .serialize(serializer) + } + + (MoveTypeLayout::Vector(layout), ValueImpl::Container(c)) => { + let layout = &**layout; + match (layout, c) { + (MoveTypeLayout::U8, Container::VecU8(r)) => r.borrow().serialize(serializer), + (MoveTypeLayout::U16, Container::VecU16(r)) => r.borrow().serialize(serializer), + (MoveTypeLayout::U32, Container::VecU32(r)) => r.borrow().serialize(serializer), + (MoveTypeLayout::U64, Container::VecU64(r)) => r.borrow().serialize(serializer), + (MoveTypeLayout::U128, Container::VecU128(r)) => { + r.borrow().serialize(serializer) + } + (MoveTypeLayout::U256, Container::VecU256(r)) => { + r.borrow().serialize(serializer) + } + (MoveTypeLayout::Bool, Container::VecBool(r)) => { + r.borrow().serialize(serializer) + } + (MoveTypeLayout::Address, Container::VecAddress(r)) => { + r.borrow().serialize(serializer) + } + + (_, Container::Vec(r)) => { + let v = r.borrow(); + let mut t = serializer.serialize_seq(Some(v.len()))?; + for val in v.iter() { + t.serialize_element(&AnnotatedValue { layout, val })?; + } + t.end() + } + + (layout, container) => Err(invariant_violation::(format!( + "cannot serialize container {:?} as {:?}", + container, layout + ))), + } + } + + (MoveTypeLayout::Signer, ValueImpl::Container(Container::Struct(r))) => { + let v = r.borrow(); + if v.len() != 1 { + return Err(invariant_violation::(format!( + "cannot serialize container as a signer -- expected 1 field got {}", + v.len() + ))); + } + (AnnotatedValue { + layout: &MoveTypeLayout::Address, + val: &v[0], + }) + .serialize(serializer) + } + + (ty, val) => Err(invariant_violation::(format!( + "cannot serialize value {:?} as {:?}", + val, ty + ))), + } + } +} + +impl serde::Serialize for AnnotatedValue<'_, '_, MoveStructLayout, Vec> { + fn serialize(&self, serializer: S) -> Result { + let values = &self.val; + let fields = self.layout.fields(); + if fields.len() != values.len() { + return Err(invariant_violation::(format!( + "cannot serialize struct value {:?} as {:?} -- number of fields mismatch", + self.val, self.layout + ))); + } + let mut t = serializer.serialize_tuple(values.len())?; + for (field_layout, val) in fields.iter().zip(values.iter()) { + t.serialize_element(&AnnotatedValue { + layout: field_layout, + val, + })?; + } + t.end() + } +} + +impl serde::Serialize for AnnotatedValue<'_, '_, MoveEnumLayout, (VariantTag, Vec)> { + fn serialize(&self, serializer: S) -> Result { + let (tag, values) = &self.val; + let tag = if *tag as u64 > VARIANT_TAG_MAX_VALUE { + return Err(serde::ser::Error::custom(format!( + "Variant tag {} is greater than the maximum allowed value of {}", + tag, VARIANT_TAG_MAX_VALUE + ))); + } else { + *tag as u8 + }; + + let fields = &self.layout.0[tag as usize]; + if fields.len() != values.len() { + return Err(invariant_violation::(format!( + "cannot serialize variant value {:?} as {:?} -- number of fields mismatch", + self.val, self.layout + ))); + } + + let mut t = serializer.serialize_tuple(2)?; + t.serialize_element(&tag)?; + + t.serialize_element(&AnnotatedValue { + layout: &VariantFields(fields), + val: values, + })?; + + t.end() + } +} + +struct VariantFields<'a>(&'a [MoveTypeLayout]); + +impl<'a> serde::Serialize for AnnotatedValue<'a, '_, VariantFields<'a>, Vec> { + fn serialize(&self, serializer: S) -> Result { + let values = self.val; + let types = self.layout.0; + if types.len() != values.len() { + return Err(invariant_violation::(format!( + "cannot serialize variant value {:?} as {:?} -- number of fields mismatch", + self.val, self.layout.0 + ))); + } + let mut t = serializer.serialize_tuple(values.len())?; + for (field_layout, val) in types.iter().zip(values.iter()) { + t.serialize_element(&AnnotatedValue { + layout: field_layout, + val, + })?; + } + t.end() + } +} + +#[derive(Clone)] +struct SeedWrapper { + layout: L, +} + +impl<'d> serde::de::DeserializeSeed<'d> for SeedWrapper<&MoveTypeLayout> { + type Value = Value; + + fn deserialize>( + self, + deserializer: D, + ) -> Result { + use MoveTypeLayout as L; + + match self.layout { + L::Bool => bool::deserialize(deserializer).map(Value::bool), + L::U8 => u8::deserialize(deserializer).map(Value::u8), + L::U16 => u16::deserialize(deserializer).map(Value::u16), + L::U32 => u32::deserialize(deserializer).map(Value::u32), + L::U64 => u64::deserialize(deserializer).map(Value::u64), + L::U128 => u128::deserialize(deserializer).map(Value::u128), + L::U256 => u256::U256::deserialize(deserializer).map(Value::u256), + L::Address => AccountAddress::deserialize(deserializer).map(Value::address), + L::Signer => AccountAddress::deserialize(deserializer).map(Value::signer), + + L::Struct(struct_layout) => Ok(SeedWrapper { + layout: &**struct_layout, + } + .deserialize(deserializer)?), + + L::Enum(enum_layout) => Ok(SeedWrapper { + layout: &**enum_layout, + } + .deserialize(deserializer)?), + + L::Vector(layout) => { + let container = match &**layout { + L::U8 => { + Container::VecU8(Rc::new(RefCell::new(Vec::deserialize(deserializer)?))) + } + L::U16 => { + Container::VecU16(Rc::new(RefCell::new(Vec::deserialize(deserializer)?))) + } + L::U32 => { + Container::VecU32(Rc::new(RefCell::new(Vec::deserialize(deserializer)?))) + } + L::U64 => { + Container::VecU64(Rc::new(RefCell::new(Vec::deserialize(deserializer)?))) + } + L::U128 => { + Container::VecU128(Rc::new(RefCell::new(Vec::deserialize(deserializer)?))) + } + L::U256 => { + Container::VecU256(Rc::new(RefCell::new(Vec::deserialize(deserializer)?))) + } + L::Bool => { + Container::VecBool(Rc::new(RefCell::new(Vec::deserialize(deserializer)?))) + } + L::Address => Container::VecAddress(Rc::new(RefCell::new(Vec::deserialize( + deserializer, + )?))), + layout => { + let v = deserializer + .deserialize_seq(VectorElementVisitor(SeedWrapper { layout }))?; + Container::Vec(Rc::new(RefCell::new(v))) + } + }; + Ok(Value(ValueImpl::Container(container))) + } + } + } +} + +impl<'d> serde::de::DeserializeSeed<'d> for SeedWrapper<&MoveStructLayout> { + type Value = Value; + + fn deserialize>( + self, + deserializer: D, + ) -> Result { + let fields = deserializer + .deserialize_tuple(self.layout.0.len(), StructFieldVisitor(&self.layout.0))?; + Ok(Value::struct_(Struct::pack(fields))) + } +} + +impl<'d> serde::de::DeserializeSeed<'d> for SeedWrapper<&MoveEnumLayout> { + type Value = Value; + + fn deserialize>( + self, + deserializer: D, + ) -> Result { + let variant = deserializer.deserialize_tuple(2, EnumFieldVisitor(&self.layout.0))?; + Ok(Value::variant(variant)) + } +} + +struct VectorElementVisitor<'a>(SeedWrapper<&'a MoveTypeLayout>); + +impl<'d> serde::de::Visitor<'d> for VectorElementVisitor<'_> { + type Value = Vec; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("Vector") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'d>, + { + let mut vals = Vec::new(); + while let Some(elem) = seq.next_element_seed(self.0.clone())? { + vals.push(elem.0) + } + Ok(vals) + } +} + +struct StructFieldVisitor<'a>(&'a [MoveTypeLayout]); + +impl<'d> serde::de::Visitor<'d> for StructFieldVisitor<'_> { + type Value = Vec; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("Struct") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'d>, + { + let mut val = Vec::new(); + for (i, field_layout) in self.0.iter().enumerate() { + if let Some(elem) = seq.next_element_seed(SeedWrapper { + layout: field_layout, + })? { + val.push(elem) + } else { + return Err(A::Error::invalid_length(i, &self)); + } + } + Ok(val) + } +} + +struct EnumFieldVisitor<'a>(&'a Vec>); + +impl<'d> serde::de::Visitor<'d> for EnumFieldVisitor<'_> { + type Value = Variant; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("Enum") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'d>, + { + let tag = match seq.next_element_seed(&MoveTypeLayout::U8)? { + Some(MoveValue::U8(tag)) if tag as u64 <= VARIANT_TAG_MAX_VALUE => tag as u16, + Some(MoveValue::U8(tag)) => return Err(A::Error::invalid_length(tag as usize, &self)), + Some(val) => { + return Err(A::Error::invalid_type( + serde::de::Unexpected::Other(&format!("{val:?}")), + &self, + )); + } + None => return Err(A::Error::invalid_length(0, &self)), + }; + + let Some(variant_layout) = self.0.get(tag as usize) else { + return Err(A::Error::invalid_length(tag as usize, &self)); + }; + + let Some(fields) = seq.next_element_seed(&MoveRuntimeVariantFieldLayout(variant_layout))? + else { + return Err(A::Error::invalid_length(1, &self)); + }; + + Ok(Variant::pack(tag, fields)) + } +} + +struct MoveRuntimeVariantFieldLayout<'a>(&'a Vec); + +impl<'d> serde::de::DeserializeSeed<'d> for &MoveRuntimeVariantFieldLayout<'_> { + type Value = Vec; + + fn deserialize>( + self, + deserializer: D, + ) -> Result { + deserializer.deserialize_tuple(self.0.len(), StructFieldVisitor(self.0)) + } +} + +/*************************************************************************************** +* +* Constants +* +* Implementation of deserialization of constant data into a runtime value +* +**************************************************************************************/ + +impl Value { + fn constant_sig_token_to_layout(constant_signature: &SignatureToken) -> Option { + use MoveTypeLayout as L; + use SignatureToken as S; + + Some(match constant_signature { + S::Bool => L::Bool, + S::U8 => L::U8, + S::U16 => L::U16, + S::U32 => L::U32, + S::U64 => L::U64, + S::U128 => L::U128, + S::U256 => L::U256, + S::Address => L::Address, + S::Signer => return None, + S::Vector(inner) => L::Vector(Box::new(Self::constant_sig_token_to_layout(inner)?)), + // Not yet supported + S::Datatype(_) | S::DatatypeInstantiation(_) => return None, + // Not allowed/Not meaningful + S::TypeParameter(_) | S::Reference(_) | S::MutableReference(_) => return None, + }) + } + + pub fn deserialize_constant(constant: &Constant) -> Option { + let layout = Self::constant_sig_token_to_layout(&constant.type_)?; + Value::simple_deserialize(&constant.data, &layout) + } +} + +/*************************************************************************************** +* +* Destructors +* +**************************************************************************************/ +// Locals may contain reference values that points to the same cotnainer through Rc, hencing forming +// a cycle. Therefore values need to be manually taken out of the Locals in order to not leak memory. +impl Drop for Locals { + fn drop(&mut self) { + _ = self.drop_all_values(); + } +} + +/*************************************************************************************** +* +* Views +* +**************************************************************************************/ +impl Container { + fn visit_impl(&self, visitor: &mut impl ValueVisitor, depth: usize) { + use Container::*; + + match self { + Locals(_) => unreachable!("Should not ba able to visit a Locals container directly"), + Vec(r) => { + let r = r.borrow(); + if visitor.visit_vec(depth, r.len()) { + for val in r.iter() { + val.visit_impl(visitor, depth + 1); + } + } + } + Struct(r) => { + let r = r.borrow(); + if visitor.visit_struct(depth, r.len()) { + for val in r.iter() { + val.visit_impl(visitor, depth + 1); + } + } + } + Variant(r) => { + let r = &*r.borrow().1; + if visitor.visit_variant(depth, r.len()) { + for val in r.iter() { + val.visit_impl(visitor, depth + 1); + } + } + } + VecU8(r) => visitor.visit_vec_u8(depth, &r.borrow()), + VecU16(r) => visitor.visit_vec_u16(depth, &r.borrow()), + VecU32(r) => visitor.visit_vec_u32(depth, &r.borrow()), + VecU64(r) => visitor.visit_vec_u64(depth, &r.borrow()), + VecU128(r) => visitor.visit_vec_u128(depth, &r.borrow()), + VecU256(r) => visitor.visit_vec_u256(depth, &r.borrow()), + VecBool(r) => visitor.visit_vec_bool(depth, &r.borrow()), + VecAddress(r) => visitor.visit_vec_address(depth, &r.borrow()), + } + } + + fn visit_indexed(&self, visitor: &mut impl ValueVisitor, depth: usize, idx: usize) { + use Container::*; + + match self { + Locals(r) | Vec(r) | Struct(r) => r.borrow()[idx].visit_impl(visitor, depth + 1), + Variant(r) => r.borrow().1[idx].visit_impl(visitor, depth + 1), + VecU8(vals) => visitor.visit_u8(depth + 1, vals.borrow()[idx]), + VecU16(vals) => visitor.visit_u16(depth + 1, vals.borrow()[idx]), + VecU32(vals) => visitor.visit_u32(depth + 1, vals.borrow()[idx]), + VecU64(vals) => visitor.visit_u64(depth + 1, vals.borrow()[idx]), + VecU128(vals) => visitor.visit_u128(depth + 1, vals.borrow()[idx]), + VecU256(vals) => visitor.visit_u256(depth + 1, vals.borrow()[idx]), + VecBool(vals) => visitor.visit_bool(depth + 1, vals.borrow()[idx]), + VecAddress(vals) => visitor.visit_address(depth + 1, vals.borrow()[idx]), + } + } +} + +impl ContainerRef { + fn visit_impl(&self, visitor: &mut impl ValueVisitor, depth: usize) { + use ContainerRef::*; + + let (container, is_global) = match self { + Local(container) => (container, false), + Global { container, .. } => (container, false), + }; + + if visitor.visit_ref(depth, is_global) { + container.visit_impl(visitor, depth + 1); + } + } +} + +impl IndexedRef { + fn visit_impl(&self, visitor: &mut impl ValueVisitor, depth: usize) { + use ContainerRef::*; + + let (container, is_global) = match &self.container_ref { + Local(container) => (container, false), + Global { container, .. } => (container, false), + }; + + if visitor.visit_ref(depth, is_global) { + container.visit_indexed(visitor, depth, self.idx) + } + } +} + +impl ValueImpl { + fn visit_impl(&self, visitor: &mut impl ValueVisitor, depth: usize) { + use ValueImpl::*; + + match self { + Invalid => unreachable!("Should not be able to visit an invalid value"), + + U8(val) => visitor.visit_u8(depth, *val), + U16(val) => visitor.visit_u16(depth, *val), + U32(val) => visitor.visit_u32(depth, *val), + U64(val) => visitor.visit_u64(depth, *val), + U128(val) => visitor.visit_u128(depth, **val), + U256(val) => visitor.visit_u256(depth, **val), + Bool(val) => visitor.visit_bool(depth, *val), + Address(val) => visitor.visit_address(depth, **val), + + Container(c) => c.visit_impl(visitor, depth), + + ContainerRef(r) => r.visit_impl(visitor, depth), + IndexedRef(r) => r.visit_impl(visitor, depth), + } + } +} + +impl ValueView for ValueImpl { + fn visit(&self, visitor: &mut impl ValueVisitor) { + self.visit_impl(visitor, 0) + } +} + +impl ValueView for Value { + fn visit(&self, visitor: &mut impl ValueVisitor) { + self.0.visit(visitor) + } +} + +impl ValueView for Struct { + fn visit(&self, visitor: &mut impl ValueVisitor) { + if visitor.visit_struct(0, self.fields.len()) { + for val in self.fields.iter() { + val.visit_impl(visitor, 1); + } + } + } +} + +impl ValueView for Vector { + fn visit(&self, visitor: &mut impl ValueVisitor) { + self.0.visit_impl(visitor, 0) + } +} + +impl ValueView for IntegerValue { + fn visit(&self, visitor: &mut impl ValueVisitor) { + use IntegerValue::*; + + match self { + U8(val) => visitor.visit_u8(0, *val), + U16(val) => visitor.visit_u16(0, *val), + U32(val) => visitor.visit_u32(0, *val), + U64(val) => visitor.visit_u64(0, *val), + U128(val) => visitor.visit_u128(0, *val), + U256(val) => visitor.visit_u256(0, *val), + } + } +} + +impl ValueView for Reference { + fn visit(&self, visitor: &mut impl ValueVisitor) { + use ReferenceImpl::*; + + match &self.0 { + ContainerRef(r) => r.visit_impl(visitor, 0), + IndexedRef(r) => r.visit_impl(visitor, 0), + } + } +} + +impl ValueView for VectorRef { + fn visit(&self, visitor: &mut impl ValueVisitor) { + self.0.visit_impl(visitor, 0) + } +} + +impl ValueView for StructRef { + fn visit(&self, visitor: &mut impl ValueVisitor) { + self.0.visit_impl(visitor, 0) + } +} + +impl ValueView for SignerRef { + fn visit(&self, visitor: &mut impl ValueVisitor) { + self.0.visit_impl(visitor, 0) + } +} + +impl ValueView for VariantRef { + fn visit(&self, visitor: &mut impl ValueVisitor) { + self.0.visit_impl(visitor, 0) + } +} + +// Note: We may want to add more helpers to retrieve value views behind references here. + +impl Struct { + #[allow(clippy::needless_lifetimes)] + pub fn field_views<'a>(&'a self) -> impl ExactSizeIterator { + self.fields.iter() + } +} + +impl Variant { + #[allow(clippy::needless_lifetimes)] + pub fn field_views<'a>(&'a self) -> impl ExactSizeIterator { + self.fields.iter() + } +} + +impl Vector { + pub fn elem_len(&self) -> usize { + self.0.len() + } + + #[allow(clippy::needless_lifetimes)] + pub fn elem_views<'a>(&'a self) -> impl ExactSizeIterator { + struct ElemView<'b> { + container: &'b Container, + idx: usize, + } + + impl<'b> ValueView for ElemView<'b> { + fn visit(&self, visitor: &mut impl ValueVisitor) { + self.container.visit_indexed(visitor, 0, self.idx) + } + } + + let len = self.0.len(); + + (0..len).map(|idx| ElemView { + container: &self.0, + idx, + }) + } +} + +impl Reference { + #[allow(clippy::needless_lifetimes)] + pub fn value_view<'a>(&'a self) -> impl ValueView + 'a { + struct ValueBehindRef<'b>(&'b ReferenceImpl); + + impl<'b> ValueView for ValueBehindRef<'b> { + fn visit(&self, visitor: &mut impl ValueVisitor) { + use ReferenceImpl::*; + + match self.0 { + ContainerRef(r) => r.container().visit_impl(visitor, 0), + IndexedRef(r) => r.container_ref.container().visit_indexed(visitor, 0, r.idx), + } + } + } + + ValueBehindRef(&self.0) + } +} + +impl GlobalValue { + #[allow(clippy::needless_lifetimes)] + pub fn view<'a>(&'a self) -> Option { + use GlobalValueImpl as G; + + struct Wrapper<'b>(&'b Rc>>); + + impl<'b> ValueView for Wrapper<'b> { + fn visit(&self, visitor: &mut impl ValueVisitor) { + let r = self.0.borrow(); + if visitor.visit_struct(0, r.len()) { + for val in r.iter() { + val.visit_impl(visitor, 1); + } + } + } + } + + match &self.0 { + G::None | G::Deleted => None, + G::Cached { fields, .. } | G::Fresh { fields } => Some(Wrapper(fields)), + } + } +} + +/*************************************************************************************** + * + * Prop Testing + * + * Random generation of values that fit into a given layout. + * + **************************************************************************************/ +#[cfg(any(test, feature = "fuzzing"))] +pub mod prop { + use super::*; + use proptest::{collection::vec, prelude::*}; + + pub fn value_strategy_with_layout(layout: MoveTypeLayout) -> impl Strategy { + use MoveTypeLayout as L; + + match layout { + L::U8 => any::().prop_map(Value::u8).boxed(), + L::U16 => any::().prop_map(Value::u16).boxed(), + L::U32 => any::().prop_map(Value::u32).boxed(), + L::U64 => any::().prop_map(Value::u64).boxed(), + L::U128 => any::().prop_map(Value::u128).boxed(), + L::U256 => any::().prop_map(Value::u256).boxed(), + L::Bool => any::().prop_map(Value::bool).boxed(), + L::Address => any::().prop_map(Value::address).boxed(), + L::Signer => any::().prop_map(Value::signer).boxed(), + + L::Vector(layout) => match *layout { + L::U8 => vec(any::(), 0..10) + .prop_map(|vals| { + Value(ValueImpl::Container(Container::VecU8(Rc::new( + RefCell::new(vals), + )))) + }) + .boxed(), + L::U16 => vec(any::(), 0..10) + .prop_map(|vals| { + Value(ValueImpl::Container(Container::VecU16(Rc::new( + RefCell::new(vals), + )))) + }) + .boxed(), + L::U32 => vec(any::(), 0..10) + .prop_map(|vals| { + Value(ValueImpl::Container(Container::VecU32(Rc::new( + RefCell::new(vals), + )))) + }) + .boxed(), + L::U64 => vec(any::(), 0..10) + .prop_map(|vals| { + Value(ValueImpl::Container(Container::VecU64(Rc::new( + RefCell::new(vals), + )))) + }) + .boxed(), + L::U128 => vec(any::(), 0..10) + .prop_map(|vals| { + Value(ValueImpl::Container(Container::VecU128(Rc::new( + RefCell::new(vals), + )))) + }) + .boxed(), + L::U256 => vec(any::(), 0..10) + .prop_map(|vals| { + Value(ValueImpl::Container(Container::VecU256(Rc::new( + RefCell::new(vals), + )))) + }) + .boxed(), + L::Bool => vec(any::(), 0..10) + .prop_map(|vals| { + Value(ValueImpl::Container(Container::VecBool(Rc::new( + RefCell::new(vals), + )))) + }) + .boxed(), + L::Address => vec(any::(), 0..10) + .prop_map(|vals| { + Value(ValueImpl::Container(Container::VecAddress(Rc::new( + RefCell::new(vals), + )))) + }) + .boxed(), + layout => vec(value_strategy_with_layout(layout), 0..10) + .prop_map(|vals| { + Value(ValueImpl::Container(Container::Vec(Rc::new(RefCell::new( + vals.into_iter().map(|val| val.0).collect(), + ))))) + }) + .boxed(), + }, + + L::Struct(struct_layout) => struct_layout + .into_fields() + .into_iter() + .map(value_strategy_with_layout) + .collect::>() + .prop_map(move |vals| Value::struct_(Struct::pack(vals))) + .boxed(), + + L::Enum(enum_layout) => { + let enum_layouts = enum_layout + .clone() + .0 + .into_iter() + .enumerate() + .collect::>(); + proptest::sample::select(enum_layouts) + .prop_flat_map(move |(tag, layout)| { + layout + .into_iter() + .map(value_strategy_with_layout) + .collect::>() + .prop_map(move |v| Value::variant(Variant::pack(tag as u16, v))) + }) + .boxed() + } + } + } + + pub fn layout_strategy() -> impl Strategy { + use MoveTypeLayout as L; + + let leaf = prop_oneof![ + 1 => Just(L::U8), + 1 => Just(L::U16), + 1 => Just(L::U32), + 1 => Just(L::U64), + 1 => Just(L::U128), + 1 => Just(L::U256), + 1 => Just(L::Bool), + 1 => Just(L::Address), + 1 => Just(L::Signer), + ]; + + leaf.prop_recursive(8, 32, 2, |inner| { + prop_oneof![ + 1 => inner.clone().prop_map(|layout| L::Vector(Box::new(layout))), + 1 => vec(inner, 0..1).prop_map(|f_layouts| { + L::Struct(Box::new(MoveStructLayout::new(f_layouts)))}), + ] + }) + } + + pub fn layout_and_value_strategy() -> impl Strategy { + layout_strategy().no_shrink().prop_flat_map(|layout| { + let value_strategy = value_strategy_with_layout(layout.clone()); + (Just(layout), value_strategy) + }) + } +} + +use move_core_types::runtime_value::{MoveStruct, MoveValue, MoveVariant}; + +impl ValueImpl { + pub fn as_move_value(&self, layout: &MoveTypeLayout) -> MoveValue { + use MoveTypeLayout as L; + + match (layout, &self) { + (L::U8, ValueImpl::U8(x)) => MoveValue::U8(*x), + (L::U16, ValueImpl::U16(x)) => MoveValue::U16(*x), + (L::U32, ValueImpl::U32(x)) => MoveValue::U32(*x), + (L::U64, ValueImpl::U64(x)) => MoveValue::U64(*x), + (L::U128, ValueImpl::U128(x)) => MoveValue::U128(**x), + (L::U256, ValueImpl::U256(x)) => MoveValue::U256(**x), + (L::Bool, ValueImpl::Bool(x)) => MoveValue::Bool(*x), + (L::Address, ValueImpl::Address(x)) => MoveValue::Address(**x), + + (L::Enum(enum_layout), ValueImpl::Container(Container::Variant(r))) => { + let MoveEnumLayout(variants) = &**enum_layout; + let (tag, values) = &*r.borrow(); + let tag = *tag; + let field_layouts = &variants.as_slice()[tag as usize]; + let mut fields = vec![]; + for (v, field_layout) in values.iter().zip(field_layouts) { + fields.push(v.as_move_value(field_layout)); + } + MoveValue::Variant(MoveVariant { tag, fields }) + } + + (L::Struct(struct_layout), ValueImpl::Container(Container::Struct(r))) => { + let mut fields = vec![]; + for (v, field_layout) in r.borrow().iter().zip(struct_layout.fields().iter()) { + fields.push(v.as_move_value(field_layout)); + } + MoveValue::Struct(MoveStruct::new(fields)) + } + + (L::Vector(inner_layout), ValueImpl::Container(c)) => MoveValue::Vector(match c { + Container::VecU8(r) => r.borrow().iter().map(|u| MoveValue::U8(*u)).collect(), + Container::VecU16(r) => r.borrow().iter().map(|u| MoveValue::U16(*u)).collect(), + Container::VecU32(r) => r.borrow().iter().map(|u| MoveValue::U32(*u)).collect(), + Container::VecU64(r) => r.borrow().iter().map(|u| MoveValue::U64(*u)).collect(), + Container::VecU128(r) => r.borrow().iter().map(|u| MoveValue::U128(*u)).collect(), + Container::VecU256(r) => r.borrow().iter().map(|u| MoveValue::U256(*u)).collect(), + Container::VecBool(r) => r.borrow().iter().map(|u| MoveValue::Bool(*u)).collect(), + Container::VecAddress(r) => { + r.borrow().iter().map(|u| MoveValue::Address(*u)).collect() + } + Container::Vec(r) => r + .borrow() + .iter() + .map(|v| v.as_move_value(inner_layout.as_ref())) + .collect(), + Container::Struct(_) => { + panic!("got struct container when converting vec") + } + Container::Variant { .. } => { + panic!("got variant container when converting vec") + } + Container::Locals(_) => panic!("got locals container when converting vec"), + }), + + (L::Signer, ValueImpl::Container(Container::Struct(r))) => { + let v = r.borrow(); + if v.len() != 1 { + panic!("Unexpected signer layout: {:?}", v); + } + match &v[0] { + ValueImpl::Address(a) => MoveValue::Signer(**a), + v => panic!("Unexpected non-address while converting signer: {:?}", v), + } + } + + (layout, val) => panic!("Cannot convert value {:?} as {:?}", val, layout), + } + } +} + +impl Value { + pub fn as_move_value(&self, layout: &MoveTypeLayout) -> MoveValue { + self.0.as_move_value(layout) + } + + pub fn as_annotated_move_value_for_tracing_only( + &self, + layout: &A::MoveTypeLayout, + ) -> Option { + self.0.as_annotated_move_value(layout) + } +} + +impl ValueImpl { + /// Converts the value to an annotated move value. This is only needed for tracing and care + /// should be taken when using this function as it can possibly inflate the size of the value. + fn as_annotated_move_value(&self, layout: &A::MoveTypeLayout) -> Option { + use move_core_types::annotated_value::MoveTypeLayout as L; + use move_core_types::annotated_value::MoveValue; + Some(match (layout, self) { + (L::U8, ValueImpl::U8(x)) => MoveValue::U8(*x), + (L::U16, ValueImpl::U16(x)) => MoveValue::U16(*x), + (L::U32, ValueImpl::U32(x)) => MoveValue::U32(*x), + (L::U64, ValueImpl::U64(x)) => MoveValue::U64(*x), + (L::U128, ValueImpl::U128(x)) => MoveValue::U128(**x), + (L::U256, ValueImpl::U256(x)) => MoveValue::U256(**x), + (L::Bool, ValueImpl::Bool(x)) => MoveValue::Bool(*x), + (L::Address, ValueImpl::Address(x)) => MoveValue::Address(**x), + (l, ValueImpl::Container(c)) => return c.as_annotated_move_value(l), + (_, ValueImpl::ContainerRef(container_ref)) => { + let (ContainerRef::Local(c) | ContainerRef::Global { container: c, .. }) = + &**container_ref; + return c.as_annotated_move_value(layout); + } + (_, ValueImpl::IndexedRef(index_ref)) => { + let IndexedRef { + container_ref: + ContainerRef::Local(c) | ContainerRef::Global { container: c, .. }, + idx, + } = &**index_ref; + use Container::*; + let idx = *idx; + let res = match c { + Locals(r) | Vec(r) | Struct(r) => r.borrow()[idx].copy_value().unwrap(), + Variant(r) => r.borrow().1[idx].copy_value().unwrap(), + VecU8(r) => ValueImpl::U8(r.borrow()[idx]), + VecU16(r) => ValueImpl::U16(r.borrow()[idx]), + VecU32(r) => ValueImpl::U32(r.borrow()[idx]), + VecU64(r) => ValueImpl::U64(r.borrow()[idx]), + VecU128(r) => ValueImpl::U128(Box::new(r.borrow()[idx])), + VecU256(r) => ValueImpl::U256(Box::new(r.borrow()[idx])), + VecBool(r) => ValueImpl::Bool(r.borrow()[idx]), + VecAddress(r) => ValueImpl::Address(Box::new(r.borrow()[idx])), + }; + return res.as_annotated_move_value(layout); + } + (_layout, _val) => return None, + }) + } +} + +impl Container { + fn as_annotated_move_value(&self, layout: &A::MoveTypeLayout) -> Option { + use move_core_types::annotated_value::MoveTypeLayout as L; + Some(match (layout, self) { + (L::Enum(e_layout), Container::Variant(r)) => { + let A::MoveEnumLayout { type_, variants } = e_layout.as_ref(); + let (tag, values) = &*r.borrow(); + let tag = *tag; + let ((name, _), field_layouts) = + variants.iter().find(|((_, t), _)| *t == tag).unwrap(); + let mut fields = vec![]; + for (v, field_layout) in values.iter().zip(field_layouts) { + fields.push(( + field_layout.name.clone(), + v.as_annotated_move_value(&field_layout.layout)?, + )); + } + A::MoveValue::Variant(A::MoveVariant { + tag, + fields, + type_: type_.clone(), + variant_name: name.clone(), + }) + } + (L::Struct(struct_layout), Container::Struct(r)) => { + let mut fields = vec![]; + for (v, field_layout) in r.borrow().iter().zip(struct_layout.fields.iter()) { + fields.push(( + field_layout.name.clone(), + v.as_annotated_move_value(&field_layout.layout)?, + )); + } + A::MoveValue::Struct(A::MoveStruct::new(struct_layout.type_.clone(), fields)) + } + + (L::Vector(inner_layout), c) => A::MoveValue::Vector(match c { + Container::VecU8(r) => r.borrow().iter().map(|u| A::MoveValue::U8(*u)).collect(), + Container::VecU16(r) => r.borrow().iter().map(|u| A::MoveValue::U16(*u)).collect(), + Container::VecU32(r) => r.borrow().iter().map(|u| A::MoveValue::U32(*u)).collect(), + Container::VecU64(r) => r.borrow().iter().map(|u| A::MoveValue::U64(*u)).collect(), + Container::VecU128(r) => { + r.borrow().iter().map(|u| A::MoveValue::U128(*u)).collect() + } + Container::VecU256(r) => { + r.borrow().iter().map(|u| A::MoveValue::U256(*u)).collect() + } + Container::VecBool(r) => { + r.borrow().iter().map(|u| A::MoveValue::Bool(*u)).collect() + } + Container::VecAddress(r) => r + .borrow() + .iter() + .map(|u| A::MoveValue::Address(*u)) + .collect(), + Container::Vec(r) => r + .borrow() + .iter() + .map(|v| v.as_annotated_move_value(inner_layout)) + .collect::>()?, + Container::Struct(_) | Container::Variant { .. } | Container::Locals(_) => { + return None; + } + }), + + (L::Signer, Container::Struct(r)) => { + let v = r.borrow(); + if v.len() != 1 { + return None; + } + match &v[0] { + ValueImpl::Address(a) => A::MoveValue::Signer(**a), + _ => return None, + } + } + (_layout, _val) => return None, + }) + } +} + +#[test] +fn assert_sizes() { + assert_eq!(size_of::(), 16); +} diff --git a/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/views.rs b/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/views.rs new file mode 100644 index 0000000000000..8bb24ba95c1b9 --- /dev/null +++ b/external-crates/move/move-execution/replay_cut/crates/move-vm-types/src/views.rs @@ -0,0 +1,250 @@ +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +use move_core_types::{ + account_address::AccountAddress, gas_algebra::AbstractMemorySize, language_storage::TypeTag, +}; + +pub struct SizeConfig { + /// If true, the reference values will be traversed recursively. + pub traverse_references: bool, + /// If true, the size of the vector will be included in the abstract memory size. + pub include_vector_size: bool, +} + +/// Trait that provides an abstract view into a Move type. +/// +/// This is used to expose certain info to clients (e.g. the gas meter), +/// usually in a lazily evaluated fashion. +pub trait TypeView { + /// Returns the `TypeTag` (fully qualified name) of the type. + fn to_type_tag(&self) -> TypeTag; +} + +/// Trait that provides an abstract view into a Move Value. +/// +/// This is used to expose certain info to clients (e.g. the gas meter), +/// usually in a lazily evaluated fashion. +pub trait ValueView { + fn visit(&self, visitor: &mut impl ValueVisitor); + + /// Returns the abstract memory size of the value. + fn abstract_memory_size(&self, config: &SizeConfig) -> AbstractMemorySize { + use crate::values::{LEGACY_CONST_SIZE, LEGACY_REFERENCE_SIZE, LEGACY_STRUCT_SIZE}; + + struct Acc<'b> { + accumulated_size: AbstractMemorySize, + config: &'b SizeConfig, + } + + impl ValueVisitor for Acc<'_> { + fn visit_u8(&mut self, _depth: usize, _val: u8) { + self.accumulated_size += LEGACY_CONST_SIZE; + } + + fn visit_u16(&mut self, _depth: usize, _val: u16) { + self.accumulated_size += LEGACY_CONST_SIZE; + } + + fn visit_u32(&mut self, _depth: usize, _val: u32) { + self.accumulated_size += LEGACY_CONST_SIZE; + } + + fn visit_u64(&mut self, _depth: usize, _val: u64) { + self.accumulated_size += LEGACY_CONST_SIZE; + } + + fn visit_u128(&mut self, _depth: usize, _val: u128) { + self.accumulated_size += LEGACY_CONST_SIZE; + } + + fn visit_u256(&mut self, _depth: usize, _val: move_core_types::u256::U256) { + self.accumulated_size += LEGACY_CONST_SIZE; + } + + fn visit_bool(&mut self, _depth: usize, _val: bool) { + self.accumulated_size += LEGACY_CONST_SIZE; + } + + fn visit_address(&mut self, _depth: usize, _val: AccountAddress) { + self.accumulated_size += AbstractMemorySize::new(AccountAddress::LENGTH as u64); + } + + fn visit_struct(&mut self, _depth: usize, _len: usize) -> bool { + self.accumulated_size += LEGACY_STRUCT_SIZE; + true + } + + fn visit_variant(&mut self, _depth: usize, _len: usize) -> bool { + self.accumulated_size += LEGACY_STRUCT_SIZE; + true + } + + fn visit_vec(&mut self, _depth: usize, _len: usize) -> bool { + self.accumulated_size += LEGACY_STRUCT_SIZE; + true + } + + fn visit_vec_u8(&mut self, _depth: usize, vals: &[u8]) { + if self.config.include_vector_size { + self.accumulated_size += LEGACY_STRUCT_SIZE; + } + self.accumulated_size += (std::mem::size_of_val(vals) as u64).into(); + } + + fn visit_vec_u16(&mut self, _depth: usize, vals: &[u16]) { + if self.config.include_vector_size { + self.accumulated_size += LEGACY_STRUCT_SIZE; + } + self.accumulated_size += (std::mem::size_of_val(vals) as u64).into(); + } + + fn visit_vec_u32(&mut self, _depth: usize, vals: &[u32]) { + if self.config.include_vector_size { + self.accumulated_size += LEGACY_STRUCT_SIZE; + } + self.accumulated_size += (std::mem::size_of_val(vals) as u64).into(); + } + + fn visit_vec_u64(&mut self, _depth: usize, vals: &[u64]) { + if self.config.include_vector_size { + self.accumulated_size += LEGACY_STRUCT_SIZE; + } + self.accumulated_size += (std::mem::size_of_val(vals) as u64).into(); + } + + fn visit_vec_u128(&mut self, _depth: usize, vals: &[u128]) { + if self.config.include_vector_size { + self.accumulated_size += LEGACY_STRUCT_SIZE; + } + self.accumulated_size += (std::mem::size_of_val(vals) as u64).into(); + } + + fn visit_vec_u256(&mut self, _depth: usize, vals: &[move_core_types::u256::U256]) { + if self.config.include_vector_size { + self.accumulated_size += LEGACY_STRUCT_SIZE; + } + self.accumulated_size += (std::mem::size_of_val(vals) as u64).into(); + } + + fn visit_vec_bool(&mut self, _depth: usize, vals: &[bool]) { + if self.config.include_vector_size { + self.accumulated_size += LEGACY_STRUCT_SIZE; + } + self.accumulated_size += (std::mem::size_of_val(vals) as u64).into(); + } + + fn visit_vec_address(&mut self, _depth: usize, vals: &[AccountAddress]) { + if self.config.include_vector_size { + self.accumulated_size += LEGACY_STRUCT_SIZE; + } + self.accumulated_size += (std::mem::size_of_val(vals) as u64).into(); + } + + fn visit_ref(&mut self, _depth: usize, _is_global: bool) -> bool { + self.accumulated_size += LEGACY_REFERENCE_SIZE; + self.config.traverse_references + } + } + + let mut acc = Acc { + accumulated_size: 0.into(), + config, + }; + self.visit(&mut acc); + + acc.accumulated_size + } +} + +/// Trait that defines a visitor that could be used to traverse a value recursively. +pub trait ValueVisitor { + fn visit_u8(&mut self, depth: usize, val: u8); + fn visit_u16(&mut self, depth: usize, val: u16); + fn visit_u32(&mut self, depth: usize, val: u32); + fn visit_u64(&mut self, depth: usize, val: u64); + fn visit_u128(&mut self, depth: usize, val: u128); + fn visit_u256(&mut self, depth: usize, val: move_core_types::u256::U256); + fn visit_bool(&mut self, depth: usize, val: bool); + fn visit_address(&mut self, depth: usize, val: AccountAddress); + + fn visit_struct(&mut self, depth: usize, len: usize) -> bool; + fn visit_variant(&mut self, depth: usize, len: usize) -> bool; + fn visit_vec(&mut self, depth: usize, len: usize) -> bool; + + fn visit_ref(&mut self, depth: usize, is_global: bool) -> bool; + + fn visit_vec_u8(&mut self, depth: usize, vals: &[u8]) { + self.visit_vec(depth, vals.len()); + for val in vals { + self.visit_u8(depth + 1, *val); + } + } + + fn visit_vec_u16(&mut self, depth: usize, vals: &[u16]) { + self.visit_vec(depth, vals.len()); + for val in vals { + self.visit_u16(depth + 1, *val); + } + } + + fn visit_vec_u32(&mut self, depth: usize, vals: &[u32]) { + self.visit_vec(depth, vals.len()); + for val in vals { + self.visit_u32(depth + 1, *val); + } + } + + fn visit_vec_u64(&mut self, depth: usize, vals: &[u64]) { + self.visit_vec(depth, vals.len()); + for val in vals { + self.visit_u64(depth + 1, *val); + } + } + + fn visit_vec_u128(&mut self, depth: usize, vals: &[u128]) { + self.visit_vec(depth, vals.len()); + for val in vals { + self.visit_u128(depth + 1, *val); + } + } + + fn visit_vec_u256(&mut self, depth: usize, vals: &[move_core_types::u256::U256]) { + self.visit_vec(depth, vals.len()); + for val in vals { + self.visit_u256(depth + 1, *val); + } + } + + fn visit_vec_bool(&mut self, depth: usize, vals: &[bool]) { + self.visit_vec(depth, vals.len()); + for val in vals { + self.visit_bool(depth + 1, *val); + } + } + + fn visit_vec_address(&mut self, depth: usize, vals: &[AccountAddress]) { + self.visit_vec(depth, vals.len()); + for val in vals { + self.visit_address(depth + 1, *val); + } + } +} + +impl ValueView for &T +where + T: ValueView, +{ + fn visit(&self, visitor: &mut impl ValueVisitor) { + ::visit(*self, visitor) + } +} + +impl TypeView for &T +where + T: TypeView, +{ + fn to_type_tag(&self) -> TypeTag { + ::to_type_tag(*self) + } +} diff --git a/sui-execution/Cargo.toml b/sui-execution/Cargo.toml index 1433b16734d83..8922407a61815 100644 --- a/sui-execution/Cargo.toml +++ b/sui-execution/Cargo.toml @@ -15,39 +15,49 @@ move-binary-format.workspace = true move-bytecode-verifier-meter.workspace = true move-trace-format.workspace = true move-vm-config.workspace = true +tracing.workspace = true +similar.workspace = true sui-adapter-latest = { path = "latest/sui-adapter" } sui-adapter-v0 = { path = "v0/sui-adapter" } sui-adapter-v1 = { path = "v1/sui-adapter" } sui-adapter-v2 = { path = "v2/sui-adapter" } +sui-adapter-replay_cut = { path = "replay_cut/sui-adapter" } # sui-adapter-$CUT = { path = "$CUT/sui-adapter" } sui-move-natives-latest = { path = "latest/sui-move-natives" } sui-move-natives-v0 = { path = "v0/sui-move-natives" } sui-move-natives-v1 = { path = "v1/sui-move-natives" } sui-move-natives-v2 = { path = "v2/sui-move-natives" } +sui-move-natives-replay_cut = { path = "replay_cut/sui-move-natives" } # sui-move-natives-$CUT = { path = "$CUT/sui-move-natives" } sui-verifier-latest = { path = "latest/sui-verifier" } sui-verifier-v0 = { path = "v0/sui-verifier" } sui-verifier-v1 = { path = "v1/sui-verifier" } sui-verifier-v2 = { path = "v2/sui-verifier" } +sui-verifier-replay_cut = { path = "replay_cut/sui-verifier" } # sui-verifier-$CUT = { path = "$CUT/sui-verifier" } move-abstract-interpreter-latest = { path = "../external-crates/move/crates/move-abstract-interpreter", package = "move-abstract-interpreter" } move-abstract-interpreter-v2 = { path = "../external-crates/move/move-execution/v2/crates/move-abstract-interpreter" } +move-abstract-interpreter-replay_cut = { path = "../external-crates/move/move-execution/replay_cut/crates/move-abstract-interpreter" } # move-abstract-interpreter-$CUT = { path = "../external-crates/move/move-execution/$CUT/crates/move-abstract-interpreter" } move-bytecode-verifier-latest = { path = "../external-crates/move/crates/move-bytecode-verifier", package = "move-bytecode-verifier" } move-bytecode-verifier-v0 = { path = "../external-crates/move/move-execution/v0/crates/move-bytecode-verifier" } move-bytecode-verifier-v1 = { path = "../external-crates/move/move-execution/v1/crates/move-bytecode-verifier" } move-bytecode-verifier-v2 = { path = "../external-crates/move/move-execution/v2/crates/move-bytecode-verifier" } +move-bytecode-verifier-replay_cut = { path = "../external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier" } # move-bytecode-verifier-$CUT = { path = "../external-crates/move/move-execution/$CUT/crates/move-bytecode-verifier" } move-vm-runtime-latest = { path = "../external-crates/move/crates/move-vm-runtime", package = "move-vm-runtime" } move-vm-runtime-v0 = { path = "../external-crates/move/move-execution/v0/crates/move-vm-runtime" } move-vm-runtime-v1 = { path = "../external-crates/move/move-execution/v1/crates/move-vm-runtime" } move-vm-runtime-v2 = { path = "../external-crates/move/move-execution/v2/crates/move-vm-runtime" } +move-vm-runtime-replay_cut = { path = "../external-crates/move/move-execution/replay_cut/crates/move-vm-runtime" } # move-vm-runtime-$CUT = { path = "../external-crates/move/move-execution/$CUT/crates/move-vm-runtime" } move-vm-types-v0 = { path = "../external-crates/move/move-execution/v0/crates/move-vm-types" } move-vm-types-v1 = { path = "../external-crates/move/move-execution/v1/crates/move-vm-types" } move-vm-types-v2 = { path = "../external-crates/move/move-execution/v2/crates/move-vm-types" } +move-vm-types-replay_cut = { path = "../external-crates/move/move-execution/replay_cut/crates/move-vm-types" } +# move-vm-types-$CUT = { path = "../external-crates/move/move-execution/$CUT/crates/move-vm-types" } [dev-dependencies] cargo_metadata = "0.15.4" @@ -63,11 +73,13 @@ tracing = [ "sui-adapter-v0/tracing", "sui-adapter-v1/tracing", "sui-adapter-v2/tracing", + "sui-adapter-replay_cut/tracing", # "sui-adapter-$CUT/tracing", "move-vm-runtime-v0/tracing", "move-vm-runtime-v1/tracing", "move-vm-runtime-latest/tracing", "move-vm-runtime-v2/tracing", + "move-vm-runtime-replay_cut/tracing", # "move-vm-runtime-$CUT/tracing", "move-vm-config/tracing", ] diff --git a/sui-execution/replay_cut/sui-adapter/Cargo.toml b/sui-execution/replay_cut/sui-adapter/Cargo.toml new file mode 100644 index 0000000000000..6c01d497ba7c0 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "sui-adapter-replay_cut" +version = "0.1.0" +authors = ["Mysten Labs "] +description = "Adapter and accompanying CLI for local sui development" +license = "Apache-2.0" +publish = false +edition = "2024" + +[lints] +workspace = true + +[dependencies] +anyhow = { workspace = true, features = ["backtrace"] } +bcs.workspace = true +leb128.workspace = true +tracing.workspace = true +serde.workspace = true +indexmap.workspace = true +serde_json.workspace = true + +move-binary-format.workspace = true +move-bytecode-utils.workspace = true +move-bytecode-verifier-meter.workspace = true +move-core-types.workspace = true +move-trace-format.workspace = true +move-vm-config.workspace = true +mysten-common.workspace = true +mysten-metrics.workspace = true +nonempty.workspace = true + +move-bytecode-verifier = { path = "../../../external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier", package = "move-bytecode-verifier-replay_cut" } +move-vm-runtime = { path = "../../../external-crates/move/move-execution/replay_cut/crates/move-vm-runtime", package = "move-vm-runtime-replay_cut" } +move-vm-profiler = { path = "../../../external-crates/move/crates/move-vm-profiler" } +sui-move-natives = { path = "../sui-move-natives", package = "sui-move-natives-replay_cut" } +sui-verifier = { path = "../sui-verifier", package = "sui-verifier-replay_cut" } +move-vm-types = { path = "../../../external-crates/move/move-execution/replay_cut/crates/move-vm-types", package = "move-vm-types-replay_cut" } +move-regex-borrow-graph = { path = "../../../external-crates/move/crates/move-regex-borrow-graph" } + +sui-macros.workspace = true +sui-protocol-config.workspace = true +sui-types.workspace = true +parking_lot.workspace = true + +[features] +tracing = [ + "sui-types/tracing", + "move-vm-runtime/tracing", + "move-vm-profiler/tracing", + "move-vm-config/tracing", +] diff --git a/sui-execution/replay_cut/sui-adapter/src/adapter.rs b/sui-execution/replay_cut/sui-adapter/src/adapter.rs new file mode 100644 index 0000000000000..5d6501a8b5a70 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/adapter.rs @@ -0,0 +1,238 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +pub use checked::*; +#[sui_macros::with_checked_arithmetic] +mod checked { + use std::cell::RefCell; + use std::rc::Rc; + use std::{collections::BTreeMap, sync::Arc}; + + use anyhow::Result; + use move_binary_format::file_format::CompiledModule; + use move_bytecode_verifier::verify_module_with_config_metered; + use move_bytecode_verifier_meter::{Meter, Scope}; + use move_core_types::account_address::AccountAddress; + use move_vm_config::{ + runtime::{VMConfig, VMRuntimeLimitsConfig}, + verifier::VerifierConfig, + }; + use move_vm_runtime::{ + move_vm::MoveVM, native_extensions::NativeContextExtensions, + native_functions::NativeFunctionTable, + }; + use mysten_common::debug_fatal; + use sui_move_natives::{object_runtime, transaction_context::TransactionContext}; + use sui_types::error::SuiErrorKind; + use sui_types::metrics::BytecodeVerifierMetrics; + use sui_verifier::check_for_verifier_timeout; + use tracing::instrument; + + use sui_move_natives::{NativesCostTable, object_runtime::ObjectRuntime}; + use sui_protocol_config::ProtocolConfig; + use sui_types::{ + base_types::*, + error::ExecutionError, + error::{ExecutionErrorKind, SuiError}, + metrics::LimitsMetrics, + storage::ChildObjectResolver, + }; + use sui_verifier::verifier::sui_verify_module_metered_check_timeout_only; + + pub fn new_move_vm( + natives: NativeFunctionTable, + protocol_config: &ProtocolConfig, + ) -> Result { + MoveVM::new_with_config( + natives, + VMConfig { + verifier: protocol_config.verifier_config(/* signing_limits */ None), + max_binary_format_version: protocol_config.move_binary_format_version(), + runtime_limits_config: VMRuntimeLimitsConfig { + vector_len_max: protocol_config.max_move_vector_len(), + max_value_nest_depth: protocol_config.max_move_value_depth_as_option(), + hardened_otw_check: protocol_config.hardened_otw_check(), + }, + enable_invariant_violation_check_in_swap_loc: !protocol_config + .disable_invariant_violation_check_in_swap_loc(), + check_no_extraneous_bytes_during_deserialization: protocol_config + .no_extraneous_module_bytes(), + // Don't augment errors with execution state on-chain + error_execution_state: false, + binary_config: protocol_config.binary_config(None), + rethrow_serialization_type_layout_errors: protocol_config + .rethrow_serialization_type_layout_errors(), + max_type_to_layout_nodes: protocol_config.max_type_to_layout_nodes_as_option(), + variant_nodes: protocol_config.variant_nodes(), + deprecate_global_storage_ops_during_deserialization: protocol_config + .deprecate_global_storage_ops_during_deserialization(), + optimize_bytecode: false, + }, + ) + .map_err(|_| SuiErrorKind::ExecutionInvariantViolation.into()) + } + + pub fn new_native_extensions<'r>( + child_resolver: &'r dyn ChildObjectResolver, + input_objects: BTreeMap, + is_metered: bool, + protocol_config: &'r ProtocolConfig, + metrics: Arc, + tx_context: Rc>, + ) -> NativeContextExtensions<'r> { + let current_epoch_id: EpochId = tx_context.borrow().epoch(); + let mut extensions = NativeContextExtensions::default(); + extensions.add(ObjectRuntime::new( + child_resolver, + input_objects, + is_metered, + protocol_config, + metrics, + current_epoch_id, + )); + extensions.add(NativesCostTable::from_protocol_config(protocol_config)); + extensions.add(TransactionContext::new(tx_context)); + extensions + } + + /// Given a list of `modules` and an `object_id`, mutate each module's self ID (which must be + /// 0x0) to be `object_id`. + pub fn substitute_package_id( + modules: &mut [CompiledModule], + object_id: ObjectID, + ) -> Result<(), ExecutionError> { + let new_address = AccountAddress::from(object_id); + + for module in modules.iter_mut() { + let self_handle = module.self_handle().clone(); + let self_address_idx = self_handle.address; + + let addrs = &mut module.address_identifiers; + let Some(address_mut) = addrs.get_mut(self_address_idx.0 as usize) else { + let name = module.identifier_at(self_handle.name); + return Err(ExecutionError::new_with_source( + ExecutionErrorKind::PublishErrorNonZeroAddress, + format!("Publishing module {name} with invalid address index"), + )); + }; + + if *address_mut != AccountAddress::ZERO { + let name = module.identifier_at(self_handle.name); + return Err(ExecutionError::new_with_source( + ExecutionErrorKind::PublishErrorNonZeroAddress, + format!("Publishing module {name} with non-zero address is not allowed"), + )); + }; + + *address_mut = new_address; + } + + Ok(()) + } + + pub fn missing_unwrapped_msg(id: &ObjectID) -> String { + format!( + "Unable to unwrap object {}. Was unable to retrieve last known version in the parent sync", + id + ) + } + + /// Run the bytecode verifier with a meter limit + /// + /// This function only fails if the verification does not complete within the limit. If the + /// modules fail to verify but verification completes within the meter limit, the function + /// succeeds. + #[instrument(level = "trace", skip_all)] + pub fn run_metered_move_bytecode_verifier( + modules: &[CompiledModule], + verifier_config: &VerifierConfig, + meter: &mut (impl Meter + ?Sized), + metrics: &Arc, + ) -> Result<(), SuiError> { + // run the Move verifier + for module in modules.iter() { + let per_module_meter_verifier_timer = metrics + .verifier_runtime_per_module_success_latency + .start_timer(); + + if let Err(e) = verify_module_timeout_only(module, verifier_config, meter) { + // We only checked that the failure was due to timeout + // Discard success timer, but record timeout/failure timer + metrics + .verifier_runtime_per_module_timeout_latency + .observe(per_module_meter_verifier_timer.stop_and_discard()); + metrics + .verifier_timeout_metrics + .with_label_values(&[ + BytecodeVerifierMetrics::OVERALL_TAG, + BytecodeVerifierMetrics::TIMEOUT_TAG, + ]) + .inc(); + + return Err(e); + }; + + // Save the success timer + per_module_meter_verifier_timer.stop_and_record(); + metrics + .verifier_timeout_metrics + .with_label_values(&[ + BytecodeVerifierMetrics::OVERALL_TAG, + BytecodeVerifierMetrics::SUCCESS_TAG, + ]) + .inc(); + } + + Ok(()) + } + + /// Run both the Move verifier and the Sui verifier, checking just for timeouts. Returns Ok(()) + /// if the verifier completes within the module meter limit and the ticks are successfully + /// transfered to the package limit (regardless of whether verification succeeds or not). + fn verify_module_timeout_only( + module: &CompiledModule, + verifier_config: &VerifierConfig, + meter: &mut (impl Meter + ?Sized), + ) -> Result<(), SuiError> { + meter.enter_scope(module.self_id().name().as_str(), Scope::Module); + + if let Err(e) = verify_module_with_config_metered(verifier_config, module, meter) { + // Check that the status indicates metering timeout. + if check_for_verifier_timeout(&e.major_status()) { + if e.major_status() + == move_core_types::vm_status::StatusCode::REFERENCE_SAFETY_INCONSISTENT + { + let mut bytes = vec![]; + let _ = module.serialize_with_version( + move_binary_format::file_format_common::VERSION_MAX, + &mut bytes, + ); + debug_fatal!( + "Reference safety inconsistency detected in module: {:?}", + bytes + ); + } + return Err(SuiErrorKind::ModuleVerificationFailure { + error: format!("Verification timed out: {}", e), + } + .into()); + } + } else if let Err(err) = sui_verify_module_metered_check_timeout_only( + module, + &BTreeMap::new(), + meter, + verifier_config, + ) { + return Err(err.into()); + } + + if meter.transfer(Scope::Module, Scope::Package, 1.0).is_err() { + return Err(SuiErrorKind::ModuleVerificationFailure { + error: "Verification timed out".to_string(), + } + .into()); + } + + Ok(()) + } +} diff --git a/sui-execution/replay_cut/sui-adapter/src/data_store/cached_package_store.rs b/sui-execution/replay_cut/sui-adapter/src/data_store/cached_package_store.rs new file mode 100644 index 0000000000000..89a1600d03897 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/data_store/cached_package_store.rs @@ -0,0 +1,252 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::data_store::PackageStore; +use indexmap::IndexMap; +use move_core_types::identifier::IdentStr; +use std::{ + cell::RefCell, + collections::{BTreeMap, BTreeSet}, + rc::Rc, +}; +use sui_types::{ + base_types::ObjectID, + error::{ExecutionError, SuiResult}, + move_package::MovePackage, + storage::BackingPackageStore, +}; + +/// A package store that caches packages in memory and indexes type origins. This is useful for +/// speeding up package loading and type resolution. +/// +/// There is a `max_package_cache_size` that determines how many packages we will cache. If the cache is +/// full, we will drop the cache. +/// +/// The `max_type_cache_size` determines how many types we will cache. If the cache is full, we +/// will drop the cache. +/// +/// FUTURE: Most/all of this will be replaced by the VM runtime cache in the new VM. For now +/// though, we need to use this. +pub struct CachedPackageStore<'state> { + /// Underlying store the fetch packages from + pub package_store: Box, + /// A cache of packages that we've loaded so far. This is used to speed up package loading. + /// Elements in this are safe to be evicted based on cache decisions. + package_cache: RefCell>>>, + /// A cache of type origins that we've loaded so far. This is used to speed up type resolution. + /// Elements in this are safe to be evicted based on cache decisions. + type_origin_cache: RefCell, + /// Elements in this are packages that are being published or have been published in the current + /// transaction. + /// Elements in this are _not_ safe to evict, unless evicted through `pop_package`. + new_packages: RefCell>>, + /// Maximum size (in number of packages) that the `package_cache` can grow to before it is + /// cleared. + max_package_cache_size: usize, + /// Maximum size (in number of keys) that the `type_origin_cache` can grow to before it is + /// cleared., + max_type_cache_size: usize, +} + +type TypeOriginMap = BTreeMap>; + +#[derive(Debug)] +struct CachedTypeOriginMap { + /// Tracker of all packages that we've loaded so far. This is used to determine if the + /// `TypeOriginMap` needs to be updated when loading a package, or if that package has already + /// contributed to the `TypeOriginMap`. + pub cached_type_origins: BTreeSet, + /// A mapping of the (original package ID):::: to the defining ID for + /// that type. + pub type_origin_map: TypeOriginMap, +} + +impl CachedTypeOriginMap { + pub fn new() -> Self { + Self { + cached_type_origins: BTreeSet::new(), + type_origin_map: TypeOriginMap::new(), + } + } +} + +impl<'state> CachedPackageStore<'state> { + pub const DEFAULT_MAX_PACKAGE_CACHE_SIZE: usize = 200; + pub const DEFAULT_MAX_TYPE_ORIGIN_CACHE_SIZE: usize = 1000; + + pub fn new(package_store: Box) -> Self { + Self { + package_store, + package_cache: RefCell::new(BTreeMap::new()), + type_origin_cache: RefCell::new(CachedTypeOriginMap::new()), + new_packages: RefCell::new(IndexMap::new()), + max_package_cache_size: Self::DEFAULT_MAX_PACKAGE_CACHE_SIZE, + max_type_cache_size: Self::DEFAULT_MAX_TYPE_ORIGIN_CACHE_SIZE, + } + } + + /// Push a new package into the new packages. This is used to track packages that are being + /// published or have been published. + pub fn push_package( + &self, + id: ObjectID, + package: Rc, + ) -> Result<(), ExecutionError> { + // Check that the package ID is not already present anywhere. + debug_assert!(self.fetch_package(&id).unwrap().is_none()); + + // Insert the package into the new packages + // If the package already exists, we will overwrite it and signal an error. + if self.new_packages.borrow_mut().insert(id, package).is_some() { + invariant_violation!( + "Package with ID {} already exists in the new packages. This should never happen.", + id + ); + } + + Ok(()) + } + + /// Rollback a package that was pushed into the new packages. We keep the invariant that: + /// * You can only pop the most recent package that was pushed. + /// * The element being popped _must_ exist in the new packages. + /// + /// Otherwise this returns an invariant violation. + pub fn pop_package(&self, id: ObjectID) -> Result, ExecutionError> { + if self + .new_packages + .borrow() + .last() + .is_none_or(|(pkg_id, _)| *pkg_id != id) + { + make_invariant_violation!( + "Tried to pop package {} from new packages, but new packages was empty or \ + it is not the most recent package inserted. This should never happen.", + id + ); + } + + let Some((pkg_id, pkg)) = self.new_packages.borrow_mut().pop() else { + unreachable!( + "We just checked that new packages is not empty, so this should never happen." + ); + }; + assert_eq!( + pkg_id, id, + "Popped package ID {} does not match requested ID {}. This should never happen as was checked above.", + pkg_id, id + ); + + Ok(pkg) + } + + pub fn to_new_packages(&self) -> Vec { + self.new_packages + .borrow() + .iter() + .map(|(_, pkg)| pkg.as_ref().clone()) + .collect() + } + + /// Get a package by its package ID (i.e., not original ID). This will first look in the new + /// packages, then in the cache, and then finally try and fetch the package from the underlying + /// package store. + /// + /// Once the package is fetched it will be added to the type origin cache if it is not already + /// present in the type origin cache. + pub fn get_package(&self, object_id: &ObjectID) -> SuiResult>> { + let Some(pkg) = self.fetch_package(object_id)? else { + return Ok(None); + }; + + let package_id = pkg.id(); + + // If the number of type origins that we have cached exceeds the max size, drop the cache. + if self.type_origin_cache.borrow().cached_type_origins.len() >= self.max_type_cache_size { + *self.type_origin_cache.borrow_mut() = CachedTypeOriginMap::new(); + } + + if !self + .type_origin_cache + .borrow() + .cached_type_origins + .contains(&package_id) + { + let cached_type_origin_map = &mut self.type_origin_cache.borrow_mut(); + cached_type_origin_map + .cached_type_origins + .insert(package_id); + let original_package_id = pkg.original_package_id(); + let package_types = cached_type_origin_map + .type_origin_map + .entry(original_package_id) + .or_default(); + for ((module_name, type_name), defining_id) in pkg.type_origin_map().into_iter() { + if let Some(other) = package_types.insert( + (module_name.to_string(), type_name.to_string()), + defining_id, + ) { + assert_eq!( + other, defining_id, + "type origin map should never have conflicting entries" + ); + } + } + } + Ok(Some(pkg)) + } + + /// Get a package by its package ID (i.e., not original ID). This will first look in the new + /// packages, then in the cache, and then finally try and fetch the package from the underlying + /// package store. NB: this does not do any indexing of the package. + fn fetch_package(&self, id: &ObjectID) -> SuiResult>> { + // Look for package in new packages + if let Some(pkg) = self.new_packages.borrow().get(id).cloned() { + return Ok(Some(pkg)); + } + + // Look for package in cache + if let Some(pkg) = self.package_cache.borrow().get(id).cloned() { + return Ok(pkg); + } + + if self.package_cache.borrow().len() >= self.max_package_cache_size { + self.package_cache.borrow_mut().clear(); + } + + let pkg = self + .package_store + .get_package_object(id)? + .map(|pkg_obj| Rc::new(pkg_obj.move_package().clone())); + self.package_cache.borrow_mut().insert(*id, pkg.clone()); + Ok(pkg) + } +} + +impl PackageStore for CachedPackageStore<'_> { + fn get_package(&self, id: &ObjectID) -> SuiResult>> { + self.get_package(id) + } + + fn resolve_type_to_defining_id( + &self, + module_address: ObjectID, + module_name: &IdentStr, + type_name: &IdentStr, + ) -> SuiResult> { + let Some(pkg) = self.get_package(&module_address)? else { + return Ok(None); + }; + + Ok(self + .type_origin_cache + .borrow() + .type_origin_map + .get(&pkg.original_package_id()) + .and_then(|module_map| { + module_map + .get(&(module_name.to_string(), type_name.to_string())) + .copied() + })) + } +} diff --git a/sui-execution/replay_cut/sui-adapter/src/data_store/legacy/linkage_view.rs b/sui-execution/replay_cut/sui-adapter/src/data_store/legacy/linkage_view.rs new file mode 100644 index 0000000000000..64c2ef7dbaa71 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/data_store/legacy/linkage_view.rs @@ -0,0 +1,365 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::data_store::PackageStore; +use move_core_types::{ + account_address::AccountAddress, + identifier::{IdentStr, Identifier}, + language_storage::ModuleId, + resolver::ModuleResolver, +}; +use move_vm_types::data_store::LinkageResolver; +use std::{ + cell::RefCell, + collections::{BTreeMap, HashMap, HashSet, hash_map::Entry}, + rc::Rc, + str::FromStr, +}; +use sui_types::{ + base_types::ObjectID, + error::{ExecutionError, SuiError, SuiResult}, + move_package::{MovePackage, TypeOrigin, UpgradeInfo}, +}; + +/// Exposes module and linkage resolution to the Move runtime. The first by delegating to +/// `resolver` and the second via linkage information that is loaded from a move package. +pub struct LinkageView<'state> { + /// Interface to resolve packages, modules and resources directly from the store, and possibly + /// from other sources (e.g., packages just published). + resolver: Box, + /// Information used to change module and type identities during linkage. + linkage_info: RefCell>, + /// Cache containing the type origin information from every package that has been set as the + /// link context, and every other type that has been requested by the loader in this session. + /// It's okay to retain entries in this cache between different link contexts because a type's + /// Runtime ID and Defining ID are invariant between across link contexts. + /// + /// Cache is keyed first by the Runtime ID of the type's module, and then the type's identifier. + /// The value is the ObjectID/Address of the package that introduced the type. + type_origin_cache: RefCell>>, + /// Cache of past package addresses that have been the link context -- if a package is in this + /// set, then we will not try to load its type origin table when setting it as a context (again). + past_contexts: RefCell>, +} + +#[derive(Debug)] +pub struct LinkageInfo { + storage_id: AccountAddress, + runtime_id: AccountAddress, + link_table: BTreeMap, +} + +pub struct SavedLinkage(LinkageInfo); + +impl<'state> LinkageView<'state> { + pub fn new(resolver: Box) -> Self { + Self { + resolver, + linkage_info: RefCell::new(None), + type_origin_cache: RefCell::new(HashMap::new()), + past_contexts: RefCell::new(HashSet::new()), + } + } + + pub fn reset_linkage(&self) -> Result<(), ExecutionError> { + let Ok(mut linkage_info) = self.linkage_info.try_borrow_mut() else { + invariant_violation!("Unable to reset linkage") + }; + *linkage_info = None; + Ok(()) + } + + /// Indicates whether this `LinkageView` has had its context set to match the linkage in + /// `context`. + pub fn has_linkage(&self, context: ObjectID) -> Result { + let Ok(linkage_info) = self.linkage_info.try_borrow() else { + invariant_violation!("Unable to borrow linkage info") + }; + Ok(linkage_info + .as_ref() + .is_some_and(|l| l.storage_id == *context)) + } + + /// Reset the linkage, but save the context that existed before, if there was one. + pub fn steal_linkage(&self) -> Option { + Some(SavedLinkage(self.linkage_info.take()?)) + } + + /// Restore a previously saved linkage context. Fails if there is already a context set. + pub fn restore_linkage(&self, saved: Option) -> Result<(), ExecutionError> { + let Some(SavedLinkage(saved)) = saved else { + return Ok(()); + }; + + let Ok(mut linkage_info) = self.linkage_info.try_borrow_mut() else { + invariant_violation!("Unable to borrow linkage while restoring") + }; + if let Some(existing) = &*linkage_info { + invariant_violation!( + "Attempt to overwrite linkage by restoring: {saved:#?} \ + Existing linkage: {existing:#?}", + ) + } + + // No need to populate type origin cache, because a saved context must have been set as a + // linkage before, and the cache would have been populated at that time. + *linkage_info = Some(saved); + Ok(()) + } + + /// Set the linkage context to the information based on the linkage and type origin tables from + /// the `context` package. Returns the original package ID (aka the runtime ID) of the context + /// package on success. + pub fn set_linkage(&self, context: &MovePackage) -> Result { + let Ok(mut linkage_info) = self.linkage_info.try_borrow_mut() else { + invariant_violation!("Unable to borrow linkage to set") + }; + if let Some(existing) = &*linkage_info { + invariant_violation!( + "Attempt to overwrite linkage info with context from {}. \ + Existing linkage: {existing:#?}", + context.id(), + ) + } + + let linkage = LinkageInfo::from(context); + let storage_id = context.id(); + let runtime_id = linkage.runtime_id; + *linkage_info = Some(linkage); + + if !self.past_contexts.borrow_mut().insert(storage_id) { + return Ok(runtime_id); + } + + // Pre-populate the type origin cache with entries from the current package -- this is + // necessary to serve "defining module" requests for unpublished packages, but will also + // speed up other requests. + for TypeOrigin { + module_name, + datatype_name: struct_name, + package: defining_id, + } in context.type_origin_table() + { + let Ok(module_name) = Identifier::from_str(module_name) else { + invariant_violation!("Module name isn't an identifier: {module_name}"); + }; + + let Ok(struct_name) = Identifier::from_str(struct_name) else { + invariant_violation!("Struct name isn't an identifier: {struct_name}"); + }; + + let runtime_id = ModuleId::new(runtime_id, module_name); + self.add_type_origin(runtime_id, struct_name, *defining_id)?; + } + + Ok(runtime_id) + } + + pub fn original_package_id(&self) -> Result, ExecutionError> { + let Ok(linkage_info) = self.linkage_info.try_borrow() else { + invariant_violation!("Unable to borrow linkage info") + }; + Ok(linkage_info.as_ref().map(|info| info.runtime_id)) + } + + fn get_cached_type_origin( + &self, + runtime_id: &ModuleId, + struct_: &IdentStr, + ) -> Option { + self.type_origin_cache + .borrow() + .get(runtime_id)? + .get(struct_) + .cloned() + } + + fn add_type_origin( + &self, + runtime_id: ModuleId, + struct_: Identifier, + defining_id: ObjectID, + ) -> Result<(), ExecutionError> { + let mut cache = self.type_origin_cache.borrow_mut(); + let module_cache = cache.entry(runtime_id.clone()).or_default(); + + match module_cache.entry(struct_) { + Entry::Vacant(entry) => { + entry.insert(*defining_id); + } + + Entry::Occupied(entry) => { + if entry.get() != &*defining_id { + invariant_violation!( + "Conflicting defining ID for {}::{}: {} and {}", + runtime_id, + entry.key(), + defining_id, + entry.get(), + ); + } + } + } + + Ok(()) + } + + pub(crate) fn link_context(&self) -> Result { + let Ok(linkage_info) = self.linkage_info.try_borrow() else { + invariant_violation!("Unable to borrow linkage info") + }; + Ok(linkage_info + .as_ref() + .map_or(AccountAddress::ZERO, |l| l.storage_id)) + } + + pub(crate) fn relocate(&self, module_id: &ModuleId) -> Result { + let Ok(linkage_info) = self.linkage_info.try_borrow() else { + invariant_violation!("Unable to borrow linkage info") + }; + let Some(linkage) = &*linkage_info else { + invariant_violation!("No linkage context set while relocating {module_id}.") + }; + + // The request is to relocate a module in the package that the link context is from. This + // entry will not be stored in the linkage table, so must be handled specially. + if module_id.address() == &linkage.runtime_id { + return Ok(ModuleId::new( + linkage.storage_id, + module_id.name().to_owned(), + )); + } + + let runtime_id = ObjectID::from_address(*module_id.address()); + let Some(upgrade) = linkage.link_table.get(&runtime_id) else { + invariant_violation!( + "Missing linkage for {runtime_id} in context {}, runtime_id is {}", + linkage.storage_id, + linkage.runtime_id + ); + }; + + Ok(ModuleId::new( + upgrade.upgraded_id.into(), + module_id.name().to_owned(), + )) + } + + pub(crate) fn defining_module( + &self, + runtime_id: &ModuleId, + struct_: &IdentStr, + ) -> Result { + let Ok(linkage_info) = self.linkage_info.try_borrow() else { + invariant_violation!("Unable to borrow linkage info") + }; + if linkage_info.is_none() { + invariant_violation!( + "No linkage context set for defining module query on {runtime_id}::{struct_}." + ) + } + + if let Some(cached) = self.get_cached_type_origin(runtime_id, struct_) { + return Ok(ModuleId::new(cached, runtime_id.name().to_owned())); + } + + let storage_id = ObjectID::from(*self.relocate(runtime_id)?.address()); + let Some(package) = self.resolver.get_package(&storage_id)? else { + invariant_violation!("Missing dependent package in store: {storage_id}",) + }; + + for TypeOrigin { + module_name, + datatype_name: struct_name, + package, + } in package.type_origin_table() + { + if module_name == runtime_id.name().as_str() && struct_name == struct_.as_str() { + self.add_type_origin(runtime_id.clone(), struct_.to_owned(), *package)?; + return Ok(ModuleId::new(**package, runtime_id.name().to_owned())); + } + } + + invariant_violation!( + "{runtime_id}::{struct_} not found in type origin table in {storage_id} (v{})", + package.version(), + ) + } +} + +impl From<&MovePackage> for LinkageInfo { + fn from(package: &MovePackage) -> Self { + Self { + storage_id: package.id().into(), + runtime_id: package.original_package_id().into(), + link_table: package.linkage_table().clone(), + } + } +} + +impl LinkageResolver for LinkageView<'_> { + type Error = SuiError; + + fn link_context(&self) -> AccountAddress { + // TODO should we propagate the error + LinkageView::link_context(self).unwrap() + } + + fn relocate(&self, module_id: &ModuleId) -> Result { + LinkageView::relocate(self, module_id) + } + + fn defining_module( + &self, + runtime_id: &ModuleId, + struct_: &IdentStr, + ) -> Result { + LinkageView::defining_module(self, runtime_id, struct_) + } +} + +impl ModuleResolver for LinkageView<'_> { + type Error = SuiError; + + fn get_module(&self, id: &ModuleId) -> Result>, Self::Error> { + Ok(self + .get_package(&ObjectID::from(*id.address()))? + .and_then(|package| { + package + .serialized_module_map() + .get(id.name().as_str()) + .cloned() + })) + } + + fn get_packages_static( + &self, + _ids: [AccountAddress; N], + ) -> Result<[Option; N], Self::Error> { + unreachable!("replay_cut get_packages_static should not be called on LinkageView") + } + + fn get_packages( + &self, + _ids: &[AccountAddress], + ) -> Result>, Self::Error> { + unreachable!("replay_cut get_packages should not be called on LinkageView") + } +} + +impl PackageStore for LinkageView<'_> { + fn get_package(&self, package_id: &ObjectID) -> SuiResult>> { + self.resolver.get_package(package_id) + } + + fn resolve_type_to_defining_id( + &self, + _module_address: ObjectID, + _module_name: &IdentStr, + _type_name: &IdentStr, + ) -> SuiResult> { + unimplemented!( + "resolve_type_to_defining_id is not implemented for LinkageView and should never be called" + ) + } +} diff --git a/sui-execution/replay_cut/sui-adapter/src/data_store/legacy/mod.rs b/sui-execution/replay_cut/sui-adapter/src/data_store/legacy/mod.rs new file mode 100644 index 0000000000000..3c8630c9b906c --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/data_store/legacy/mod.rs @@ -0,0 +1,5 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +pub mod linkage_view; +pub mod sui_data_store; diff --git a/sui-execution/replay_cut/sui-adapter/src/data_store/legacy/sui_data_store.rs b/sui-execution/replay_cut/sui-adapter/src/data_store/legacy/sui_data_store.rs new file mode 100644 index 0000000000000..7f27fcd124eb5 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/data_store/legacy/sui_data_store.rs @@ -0,0 +1,125 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::data_store::{PackageStore, legacy::linkage_view::LinkageView}; +use move_binary_format::errors::{Location, PartialVMError, PartialVMResult, VMResult}; +use move_core_types::{ + account_address::AccountAddress, identifier::IdentStr, language_storage::ModuleId, + resolver::ModuleResolver, vm_status::StatusCode, +}; +use move_vm_types::data_store::DataStore; +use std::rc::Rc; +use sui_types::{base_types::ObjectID, error::SuiResult, move_package::MovePackage}; + +// Implementation of the `DataStore` trait for the Move VM. +// When used during execution it may have a list of new packages that have +// just been published in the current context. Those are used for module/type +// resolution when executing module init. +// It may be created with an empty slice of packages either when no publish/upgrade +// are performed or when a type is requested not during execution. +pub(crate) struct SuiDataStore<'state, 'a> { + linkage_view: &'a LinkageView<'state>, + new_packages: &'a [MovePackage], +} + +impl<'state, 'a> SuiDataStore<'state, 'a> { + pub(crate) fn new( + linkage_view: &'a LinkageView<'state>, + new_packages: &'a [MovePackage], + ) -> Self { + Self { + linkage_view, + new_packages, + } + } + + fn get_module(&self, module_id: &ModuleId) -> Option<&Vec> { + for package in self.new_packages { + let module = package.get_module(module_id); + if module.is_some() { + return module; + } + } + None + } +} + +// TODO: `DataStore` will be reworked and this is likely to disappear. +// Leaving this comment around until then as testament to better days to come... +impl DataStore for SuiDataStore<'_, '_> { + fn link_context(&self) -> PartialVMResult { + self.linkage_view.link_context().map_err(|e| { + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message(e.to_string()) + }) + } + + fn relocate(&self, module_id: &ModuleId) -> PartialVMResult { + self.linkage_view.relocate(module_id).map_err(|err| { + PartialVMError::new(StatusCode::LINKER_ERROR) + .with_message(format!("Error relocating {module_id}: {err:?}")) + }) + } + + fn defining_module( + &self, + runtime_id: &ModuleId, + struct_: &IdentStr, + ) -> PartialVMResult { + self.linkage_view + .defining_module(runtime_id, struct_) + .map_err(|err| { + PartialVMError::new(StatusCode::LINKER_ERROR).with_message(format!( + "Error finding defining module for {runtime_id}::{struct_}: {err:?}" + )) + }) + } + + fn load_module(&self, module_id: &ModuleId) -> VMResult> { + if let Some(bytes) = self.get_module(module_id) { + return Ok(bytes.clone()); + } + match self.linkage_view.get_module(module_id) { + Ok(Some(bytes)) => Ok(bytes), + Ok(None) => Err(PartialVMError::new(StatusCode::LINKER_ERROR) + .with_message(format!("Cannot find {:?} in data cache", module_id)) + .finish(Location::Undefined)), + Err(err) => { + let msg = format!("Unexpected storage error: {:?}", err); + Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message(msg) + .finish(Location::Undefined), + ) + } + } + } + + fn publish_module(&mut self, _module_id: &ModuleId, _blob: Vec) -> VMResult<()> { + // we cannot panic here because during execution and publishing this is + // currently called from the publish flow in the Move runtime + Ok(()) + } +} + +impl PackageStore for SuiDataStore<'_, '_> { + fn get_package(&self, id: &ObjectID) -> SuiResult>> { + for package in self.new_packages { + if package.id() == *id { + return Ok(Some(Rc::new(package.clone()))); + } + } + self.linkage_view.get_package(id) + } + + fn resolve_type_to_defining_id( + &self, + _module_address: ObjectID, + _module_name: &IdentStr, + _type_name: &IdentStr, + ) -> SuiResult> { + unimplemented!( + "resolve_type_to_defining_id is not implemented for legacy::SuiDataStore and should never be called" + ) + } +} diff --git a/sui-execution/replay_cut/sui-adapter/src/data_store/linked_data_store.rs b/sui-execution/replay_cut/sui-adapter/src/data_store/linked_data_store.rs new file mode 100644 index 0000000000000..617e96425bbeb --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/data_store/linked_data_store.rs @@ -0,0 +1,182 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + data_store::PackageStore, + static_programmable_transactions::linkage::resolved_linkage::RootedLinkage, +}; +use move_binary_format::errors::{Location, PartialVMError, PartialVMResult, VMResult}; +use move_core_types::{ + account_address::AccountAddress, identifier::IdentStr, language_storage::ModuleId, + resolver::ModuleResolver, vm_status::StatusCode, +}; +use move_vm_types::data_store::{DataStore, LinkageResolver}; +use sui_types::{ + base_types::ObjectID, + error::{ExecutionErrorKind, SuiError}, +}; + +/// A `LinkedDataStore` is a wrapper around a `PackageStore` (i.e., a package store where +/// we can also resolve types to defining IDs) along with a specific `linkage`. These two together +/// allow us to resolve modules and types in a way that is consistent with the `linkage` provided +/// and allow us to then pass this into the VM. Until we have a linkage set it is not possible to +/// construct a valid `DataStore` for execution in the VM as it needs to be able to resolve modules +/// under a specific linkage. +pub struct LinkedDataStore<'a> { + pub linkage: &'a RootedLinkage, + pub store: &'a dyn PackageStore, +} + +impl<'a> LinkedDataStore<'a> { + pub fn new(linkage: &'a RootedLinkage, store: &'a dyn PackageStore) -> Self { + Self { linkage, store } + } +} + +impl DataStore for LinkedDataStore<'_> { + fn link_context(&self) -> PartialVMResult { + Ok(self.linkage.link_context) + } + + fn relocate(&self, module_id: &ModuleId) -> PartialVMResult { + self.linkage + .resolved_linkage + .linkage + .get(&ObjectID::from(*module_id.address())) + .map(|obj_id| ModuleId::new(**obj_id, module_id.name().to_owned())) + .ok_or_else(|| { + PartialVMError::new(StatusCode::LINKER_ERROR).with_message(format!( + "Error relocating {module_id} -- could not find linkage" + )) + }) + } + + fn defining_module( + &self, + module_id: &ModuleId, + struct_: &IdentStr, + ) -> PartialVMResult { + self.store + .resolve_type_to_defining_id( + ObjectID::from(*module_id.address()), + module_id.name(), + struct_, + ) + .ok() + .flatten() + .map(|obj_id| ModuleId::new(*obj_id, module_id.name().to_owned())) + .ok_or_else(|| { + PartialVMError::new(StatusCode::LINKER_ERROR).with_message(format!( + "Error finding defining module for {module_id}::{struct_} -- could nod find linkage" + )) + }) + } + + // NB: module_id is original ID based + fn load_module(&self, module_id: &ModuleId) -> VMResult> { + let package_storage_id = ObjectID::from(*module_id.address()); + match self + .store + .get_package(&package_storage_id) + .map(|pkg| pkg.and_then(|pkg| pkg.get_module(module_id).cloned())) + { + Ok(Some(bytes)) => Ok(bytes), + Ok(None) => Err(PartialVMError::new(StatusCode::LINKER_ERROR) + .with_message(format!("Cannot find {:?} in data cache", module_id)) + .finish(Location::Undefined)), + Err(err) => { + let msg = format!("Unexpected storage error: {:?}", err); + Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message(msg) + .finish(Location::Undefined), + ) + } + } + } + + fn publish_module(&mut self, _module_id: &ModuleId, _blob: Vec) -> VMResult<()> { + Ok(()) + } +} + +impl DataStore for &LinkedDataStore<'_> { + fn link_context(&self) -> PartialVMResult { + DataStore::link_context(*self) + } + + fn relocate(&self, module_id: &ModuleId) -> PartialVMResult { + DataStore::relocate(*self, module_id) + } + + fn defining_module( + &self, + module_id: &ModuleId, + struct_: &IdentStr, + ) -> PartialVMResult { + DataStore::defining_module(*self, module_id, struct_) + } + + fn load_module(&self, module_id: &ModuleId) -> VMResult> { + DataStore::load_module(*self, module_id) + } + + fn publish_module(&mut self, _module_id: &ModuleId, _blob: Vec) -> VMResult<()> { + Ok(()) + } +} + +impl ModuleResolver for LinkedDataStore<'_> { + type Error = SuiError; + + fn get_module(&self, id: &ModuleId) -> Result>, Self::Error> { + self.load_module(id) + .map(Some) + .map_err(|_| SuiError::from(ExecutionErrorKind::VMVerificationOrDeserializationError)) + } + + fn get_packages_static( + &self, + _ids: [AccountAddress; N], + ) -> Result<[Option; N], Self::Error> { + unreachable!("replay_cut get_packages_static should not be called on LinkageView") + } + + fn get_packages( + &self, + _ids: &[AccountAddress], + ) -> Result>, Self::Error> { + unreachable!("replay_cut get_packages should not be called on LinkageView") + } +} + +impl LinkageResolver for LinkedDataStore<'_> { + type Error = SuiError; + + fn link_context(&self) -> AccountAddress { + // TODO should we propagate the error + DataStore::link_context(self).unwrap() + } + + fn relocate(&self, module_id: &ModuleId) -> Result { + DataStore::relocate(self, module_id).map_err(|err| { + make_invariant_violation!("Error relocating {}: {:?}", module_id, err).into() + }) + } + + fn defining_module( + &self, + runtime_id: &ModuleId, + struct_: &IdentStr, + ) -> Result { + DataStore::defining_module(self, runtime_id, struct_).map_err(|err| { + make_invariant_violation!( + "Error finding defining module for {}::{}: {:?}", + runtime_id, + struct_, + err + ) + .into() + }) + } +} diff --git a/sui-execution/replay_cut/sui-adapter/src/data_store/mod.rs b/sui-execution/replay_cut/sui-adapter/src/data_store/mod.rs new file mode 100644 index 0000000000000..4ebf565ebf81b --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/data_store/mod.rs @@ -0,0 +1,25 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +pub mod cached_package_store; +pub mod legacy; +pub mod linked_data_store; + +use move_core_types::identifier::IdentStr; +use std::rc::Rc; +use sui_types::{base_types::ObjectID, error::SuiResult, move_package::MovePackage}; + +// A unifying trait that allows us to resolve a type to its defining ID as well as load packages. +// Some move packages that can be "loaded" via this may not be objects just yet (e.g., if +// they were published in the current transaction). Note that this needs to load `MovePackage`s and +// not `MovePackageObject`s because of this. +pub trait PackageStore { + fn get_package(&self, id: &ObjectID) -> SuiResult>>; + + fn resolve_type_to_defining_id( + &self, + module_address: ObjectID, + module_name: &IdentStr, + type_name: &IdentStr, + ) -> SuiResult>; +} diff --git a/sui-execution/replay_cut/sui-adapter/src/error.rs b/sui-execution/replay_cut/sui-adapter/src/error.rs new file mode 100644 index 0000000000000..148ccf416aee3 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/error.rs @@ -0,0 +1,80 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use move_binary_format::{ + errors::{Location, VMError}, + file_format::FunctionDefinitionIndex, +}; +use move_core_types::{ + language_storage::ModuleId, + vm_status::{StatusCode, StatusType}, +}; +use sui_types::error::ExecutionError; +use sui_types::execution_status::{ExecutionFailureStatus, MoveLocation, MoveLocationOpt}; + +pub(crate) fn convert_vm_error_impl( + error: VMError, + abort_module_id_relocation_fn: &impl Fn(&ModuleId) -> ModuleId, + function_name_resolution_fn: &impl Fn(&ModuleId, FunctionDefinitionIndex) -> Option, +) -> ExecutionError { + let kind = match (error.major_status(), error.sub_status(), error.location()) { + (StatusCode::EXECUTED, _, _) => { + // If we have an error the status probably shouldn't ever be Executed + debug_assert!(false, "VmError shouldn't ever report successful execution"); + ExecutionFailureStatus::VMInvariantViolation + } + (StatusCode::ABORTED, None, _) => { + debug_assert!(false, "No abort code"); + // this is a Move VM invariant violation, the code should always be there + ExecutionFailureStatus::VMInvariantViolation + } + (StatusCode::ABORTED, Some(code), Location::Module(id)) => { + let abort_location_id = abort_module_id_relocation_fn(id); + let offset = error.offsets().first().copied().map(|(f, i)| (f.0, i)); + debug_assert!(offset.is_some(), "Move should set the location on aborts"); + let (function, instruction) = offset.unwrap_or((0, 0)); + let function_name = function_name_resolution_fn(id, FunctionDefinitionIndex(function)); + ExecutionFailureStatus::MoveAbort( + MoveLocation { + module: abort_location_id, + function, + instruction, + function_name, + }, + code, + ) + } + (StatusCode::OUT_OF_GAS, _, _) => ExecutionFailureStatus::InsufficientGas, + (_, _, location) => match error.major_status().status_type() { + StatusType::Execution => { + debug_assert!(error.major_status() != StatusCode::ABORTED); + let location = match location { + Location::Module(id) => { + let offset = error.offsets().first().copied().map(|(f, i)| (f.0, i)); + debug_assert!( + offset.is_some(), + "Move should set the location on all execution errors. Error {error}" + ); + let (function, instruction) = offset.unwrap_or((0, 0)); + let function_name = + function_name_resolution_fn(id, FunctionDefinitionIndex(function)); + Some(MoveLocation { + module: id.clone(), + function, + instruction, + function_name, + }) + } + _ => None, + }; + ExecutionFailureStatus::MovePrimitiveRuntimeError(MoveLocationOpt(location)) + } + StatusType::Validation + | StatusType::Verification + | StatusType::Deserialization + | StatusType::Unknown => ExecutionFailureStatus::VMVerificationOrDeserializationError, + StatusType::InvariantViolation => ExecutionFailureStatus::VMInvariantViolation, + }, + }; + ExecutionError::new_with_source(kind, error) +} diff --git a/sui-execution/replay_cut/sui-adapter/src/execution_engine.rs b/sui-execution/replay_cut/sui-adapter/src/execution_engine.rs new file mode 100644 index 0000000000000..910f7583ab5b7 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/execution_engine.rs @@ -0,0 +1,1523 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +pub use checked::*; + +#[sui_macros::with_checked_arithmetic] +mod checked { + + use crate::execution_mode::{self, ExecutionMode}; + use crate::execution_value::SuiResolver; + use move_binary_format::CompiledModule; + use move_trace_format::format::MoveTraceBuilder; + use move_vm_runtime::move_vm::MoveVM; + use mysten_common::debug_fatal; + 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::{ + BALANCE_CREATE_REWARDS_FUNCTION_NAME, BALANCE_DESTROY_REBATES_FUNCTION_NAME, + BALANCE_MODULE_NAME, + }; + use sui_types::execution_params::ExecutionOrEarlyError; + use sui_types::gas_coin::GAS; + use sui_types::messages_checkpoint::CheckpointTimestamp; + use sui_types::metrics::LimitsMetrics; + use sui_types::object::OBJECT_START_VERSION; + use sui_types::programmable_transaction_builder::ProgrammableTransactionBuilder; + use sui_types::randomness_state::{ + RANDOMNESS_MODULE_NAME, RANDOMNESS_STATE_CREATE_FUNCTION_NAME, + RANDOMNESS_STATE_UPDATE_FUNCTION_NAME, + }; + use sui_types::{BRIDGE_ADDRESS, SUI_BRIDGE_OBJECT_ID, SUI_RANDOMNESS_STATE_OBJECT_ID}; + use tracing::{info, instrument, trace, warn}; + + use crate::adapter::new_move_vm; + use crate::programmable_transactions; + use crate::sui_types::gas::SuiGasStatusAPI; + use crate::type_layout_resolver::TypeLayoutResolver; + use crate::{gas_charger::GasCharger, temporary_store::TemporaryStore}; + use move_core_types::ident_str; + use sui_move_natives::all_natives; + use sui_protocol_config::{ + LimitThresholdCrossed, PerObjectCongestionControlMode, ProtocolConfig, check_limit_by_meter, + }; + use sui_types::authenticator_state::{ + AUTHENTICATOR_STATE_CREATE_FUNCTION_NAME, AUTHENTICATOR_STATE_EXPIRE_JWKS_FUNCTION_NAME, + AUTHENTICATOR_STATE_MODULE_NAME, AUTHENTICATOR_STATE_UPDATE_FUNCTION_NAME, + }; + use sui_types::base_types::SequenceNumber; + use sui_types::bridge::BRIDGE_COMMITTEE_MINIMAL_VOTING_POWER; + use sui_types::bridge::{ + BRIDGE_CREATE_FUNCTION_NAME, BRIDGE_INIT_COMMITTEE_FUNCTION_NAME, BRIDGE_MODULE_NAME, + BridgeChainId, + }; + use sui_types::clock::{CLOCK_MODULE_NAME, CONSENSUS_COMMIT_PROLOGUE_FUNCTION_NAME}; + use sui_types::committee::EpochId; + use sui_types::deny_list_v1::{DENY_LIST_CREATE_FUNC, DENY_LIST_MODULE}; + use sui_types::digests::{ + ChainIdentifier, get_mainnet_chain_identifier, get_testnet_chain_identifier, + }; + use sui_types::effects::TransactionEffects; + 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::gas::SuiGasStatus; + use sui_types::id::UID; + use sui_types::inner_temporary_store::InnerTemporaryStore; + use sui_types::storage::BackingStore; + #[cfg(msim)] + use sui_types::sui_system_state::advance_epoch_result_injection::maybe_modify_result; + 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, + ProgrammableTransaction, StoredExecutionTimeObservations, TransactionKind, + is_gas_paid_from_address_balance, + }; + use sui_types::transaction::{CheckedInputObjects, RandomnessStateUpdate}; + use sui_types::{ + SUI_AUTHENTICATOR_STATE_OBJECT_ID, SUI_FRAMEWORK_ADDRESS, SUI_FRAMEWORK_PACKAGE_ID, + SUI_SYSTEM_PACKAGE_ID, + base_types::{SuiAddress, TransactionDigest, TxContext}, + object::{Object, ObjectInner}, + sui_system_state::{ADVANCE_EPOCH_FUNCTION_NAME, SUI_SYSTEM_MODULE_NAME}, + }; + + #[instrument(name = "tx_execute_to_effects", level = "debug", skip_all)] + pub fn execute_transaction_to_effects( + store: &dyn BackingStore, + input_objects: CheckedInputObjects, + 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, + ) { + let input_objects = input_objects.into_inner(); + let mutable_inputs = if enable_expensive_checks { + input_objects.all_mutable_inputs().keys().copied().collect() + } else { + HashSet::new() + }; + let shared_object_refs = input_objects.filter_shared_objects(); + let receiving_objects = transaction_kind.receiving_objects(); + let mut transaction_dependencies = input_objects.transaction_dependencies(); + + let mut temporary_store = TemporaryStore::new( + store, + input_objects, + receiving_objects, + transaction_digest, + protocol_config, + *epoch_id, + ); + + let sponsor = { + let gas_owner = gas_data.owner; + if gas_owner == transaction_signer { + None + } else { + Some(gas_owner) + } + }; + let gas_price = gas_status.gas_price(); + let rgp = gas_status.reference_gas_price(); + let address_balance_gas_payer = + if is_gas_paid_from_address_balance(&gas_data, &transaction_kind) { + Some(gas_data.owner) + } else { + None + }; + let mut gas_charger = GasCharger::new( + transaction_digest, + gas_data.payment, + gas_status, + protocol_config, + address_balance_gas_payer, + ); + + let tx_ctx = TxContext::new_from_components( + &transaction_signer, + &transaction_digest, + epoch_id, + epoch_timestamp_ms, + rgp, + gas_price, + gas_data.budget, + sponsor, + protocol_config, + ); + let tx_ctx = Rc::new(RefCell::new(tx_ctx)); + + let is_epoch_change = transaction_kind.is_end_of_epoch_tx(); + + let (gas_cost_summary, execution_result, timings) = execute_transaction::( + store, + &mut temporary_store, + transaction_kind, + &mut gas_charger, + tx_ctx, + move_vm, + protocol_config, + metrics, + enable_expensive_checks, + execution_params, + trace_builder_opt, + ); + + 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; + match error.kind() { + K::InvariantViolation | K::VMInvariantViolation => { + if protocol_config.debug_fatal_on_move_invariant_violation() { + debug_fatal!( + "INVARIANT VIOLATION! Txn Digest: {}, Source: {:?}", + transaction_digest, + error.source(), + ); + } else { + #[skip_checked_arithmetic] + tracing::error!( + kind = ?error.kind(), + tx_digest = ?transaction_digest, + "INVARIANT VIOLATION! Source: {:?}", + error.source(), + ); + } + } + + K::SuiMoveVerificationError | K::VMVerificationOrDeserializationError => { + #[skip_checked_arithmetic] + tracing::debug!( + kind = ?error.kind(), + tx_digest = ?transaction_digest, + "Verification Error. Source: {:?}", + error.source(), + ); + } + + K::PublishUpgradeMissingDependency | K::PublishUpgradeDependencyDowngrade => { + #[skip_checked_arithmetic] + tracing::debug!( + kind = ?error.kind(), + tx_digest = ?transaction_digest, + "Publish/Upgrade Error. Source: {:?}", + error.source(), + ) + } + + _ => (), + }; + + let (status, command) = error.to_execution_status(); + ExecutionStatus::new_failure(status, command) + } else { + ExecutionStatus::Success + }; + + #[skip_checked_arithmetic] + trace!( + tx_digest = ?transaction_digest, + computation_gas_cost = gas_cost_summary.computation_cost, + storage_gas_cost = gas_cost_summary.storage_cost, + storage_gas_rebate = gas_cost_summary.storage_rebate, + "Finished execution of transaction with status {:?}", + status + ); + + // Genesis writes a special digest to indicate that an object was created during + // genesis and not written by any normal transaction - remove that from the + // dependencies + transaction_dependencies.remove(&TransactionDigest::genesis_marker()); + + if enable_expensive_checks && !Mode::allow_arbitrary_function_calls() { + temporary_store + .check_ownership_invariants( + &transaction_signer, + &sponsor, + &mut gas_charger, + &mutable_inputs, + is_epoch_change, + ) + .unwrap() + } // else, in dev inspect mode and anything goes--don't check + + let (inner, effects) = temporary_store.into_effects( + shared_object_refs, + &transaction_digest, + transaction_dependencies, + gas_cost_summary, + status, + &mut gas_charger, + *epoch_id, + ); + + ( + inner, + gas_charger.into_gas_status(), + effects, + timings, + execution_result, + ) + } + + pub fn execute_genesis_state_update( + store: &dyn BackingStore, + protocol_config: &ProtocolConfig, + metrics: Arc, + move_vm: &Arc, + tx_context: Rc>, + input_objects: CheckedInputObjects, + pt: ProgrammableTransaction, + ) -> Result { + let input_objects = input_objects.into_inner(); + let mut temporary_store = TemporaryStore::new( + store, + input_objects, + vec![], + tx_context.borrow().digest(), + protocol_config, + 0, + ); + let mut gas_charger = GasCharger::new_unmetered(tx_context.borrow().digest()); + programmable_transactions::execution::execute::( + protocol_config, + metrics, + move_vm, + &mut temporary_store, + store.as_backing_package_store(), + tx_context, + &mut gas_charger, + pt, + &mut None, + ) + .map_err(|(e, _)| e)?; + temporary_store.update_object_version_and_prev_tx(); + Ok(temporary_store.into_inner()) + } + + #[instrument(name = "tx_execute", level = "debug", skip_all)] + fn execute_transaction( + store: &dyn BackingStore, + temporary_store: &mut TemporaryStore<'_>, + transaction_kind: TransactionKind, + gas_charger: &mut GasCharger, + tx_ctx: Rc>, + move_vm: &Arc, + protocol_config: &ProtocolConfig, + metrics: Arc, + enable_expensive_checks: bool, + execution_params: ExecutionOrEarlyError, + trace_builder_opt: &mut Option, + ) -> ( + GasCostSummary, + Result, + Vec, + ) { + gas_charger.smash_gas(temporary_store); + + // At this point no charges have been applied yet + debug_assert!( + gas_charger.no_charges(), + "No gas charges must be applied yet" + ); + + let is_genesis_tx = matches!(transaction_kind, TransactionKind::Genesis(_)); + let advance_epoch_gas_summary = transaction_kind.get_advance_epoch_tx_gas_summary(); + let digest = tx_ctx.borrow().digest(); + + // We must charge object read here during transaction execution, because if this fails + // we must still ensure an effect is committed and all objects versions incremented + let result = gas_charger.charge_input_objects(temporary_store); + + let result: ResultWithTimings = + result.map_err(|e| (e, vec![])).and_then( + |()| -> ResultWithTimings { + let mut execution_result: ResultWithTimings< + Mode::ExecutionResults, + ExecutionError, + > = match execution_params { + ExecutionOrEarlyError::Err(early_execution_error) => { + Err((ExecutionError::new(early_execution_error, None), vec![])) + } + ExecutionOrEarlyError::Ok(()) => execution_loop::( + store, + temporary_store, + transaction_kind, + tx_ctx, + move_vm, + gas_charger, + protocol_config, + metrics.clone(), + trace_builder_opt, + ), + }; + + let meter_check = check_meter_limit( + temporary_store, + gas_charger, + protocol_config, + metrics.clone(), + ); + if let Err(e) = meter_check { + execution_result = Err((e, vec![])); + } + + if execution_result.is_ok() { + let gas_check = check_written_objects_limit::( + temporary_store, + gas_charger, + protocol_config, + metrics, + ); + if let Err(e) = gas_check { + execution_result = Err((e, vec![])); + } + } + + execution_result + }, + ); + + let (mut result, timings) = match result { + Ok((r, t)) => (Ok(r), t), + Err((e, t)) => (Err(e), t), + }; + + let cost_summary = gas_charger.charge_gas(temporary_store, &mut result); + // For advance epoch transaction, we need to provide epoch rewards and rebates as extra + // information provided to check_sui_conserved, because we mint rewards, and burn + // the rebates. We also need to pass in the unmetered_storage_rebate because storage + // rebate is not reflected in the storage_rebate of gas summary. This is a bit confusing. + // We could probably clean up the code a bit. + // Put all the storage rebate accumulated in the system transaction + // to the 0x5 object so that it's not lost. + temporary_store.conserve_unmetered_storage_rebate(gas_charger.unmetered_storage_rebate()); + + if let Err(e) = run_conservation_checks::( + temporary_store, + gas_charger, + digest, + move_vm, + protocol_config.simple_conservation_checks(), + enable_expensive_checks, + &cost_summary, + is_genesis_tx, + advance_epoch_gas_summary, + ) { + // FIXME: we cannot fail the transaction if this is an epoch change transaction. + result = Err(e); + } + + (cost_summary, result, timings) + } + + #[instrument(name = "run_conservation_checks", level = "debug", skip_all)] + fn run_conservation_checks( + temporary_store: &mut TemporaryStore<'_>, + gas_charger: &mut GasCharger, + tx_digest: TransactionDigest, + move_vm: &Arc, + simple_conservation_checks: bool, + enable_expensive_checks: bool, + cost_summary: &GasCostSummary, + is_genesis_tx: bool, + advance_epoch_gas_summary: Option<(u64, u64)>, + ) -> Result<(), ExecutionError> { + let mut result: std::result::Result<(), sui_types::error::ExecutionError> = Ok(()); + if !is_genesis_tx && !Mode::skip_conservation_checks() { + // ensure that this transaction did not create or destroy SUI, try to recover if the check fails + let conservation_result = { + temporary_store + .check_sui_conserved(simple_conservation_checks, cost_summary) + .and_then(|()| { + if enable_expensive_checks { + // ensure that this transaction did not create or destroy SUI, try to recover if the check fails + let mut layout_resolver = + TypeLayoutResolver::new(move_vm, Box::new(&*temporary_store)); + temporary_store.check_sui_conserved_expensive( + cost_summary, + advance_epoch_gas_summary, + &mut layout_resolver, + ) + } else { + Ok(()) + } + }) + }; + if let Err(conservation_err) = conservation_result { + // conservation violated. try to avoid panic by dumping all writes, charging for gas, re-checking + // conservation, and surfacing an aborted transaction with an invariant violation if all of that works + result = Err(conservation_err); + gas_charger.reset(temporary_store); + gas_charger.charge_gas(temporary_store, &mut result); + // check conservation once more + if let Err(recovery_err) = { + temporary_store + .check_sui_conserved(simple_conservation_checks, cost_summary) + .and_then(|()| { + if enable_expensive_checks { + // ensure that this transaction did not create or destroy SUI, try to recover if the check fails + let mut layout_resolver = + TypeLayoutResolver::new(move_vm, Box::new(&*temporary_store)); + temporary_store.check_sui_conserved_expensive( + cost_summary, + advance_epoch_gas_summary, + &mut layout_resolver, + ) + } else { + Ok(()) + } + }) + } { + // if we still fail, it's a problem with gas + // charging that happens even in the "aborted" case--no other option but panic. + // we will create or destroy SUI otherwise + panic!( + "SUI conservation fail in tx block {}: {}\nGas status is {}\nTx was ", + tx_digest, + recovery_err, + gas_charger.summary() + ) + } + } + } // else, we're in the genesis transaction which mints the SUI supply, and hence does not satisfy SUI conservation, or + // we're in the non-production dev inspect mode which allows us to violate conservation + result + } + + #[instrument(name = "check_meter_limit", level = "debug", skip_all)] + fn check_meter_limit( + temporary_store: &mut TemporaryStore<'_>, + gas_charger: &mut GasCharger, + protocol_config: &ProtocolConfig, + metrics: Arc, + ) -> Result<(), ExecutionError> { + let effects_estimated_size = temporary_store.estimate_effects_size_upperbound(); + + // Check if a limit threshold was crossed. + // For metered transactions, there is not soft limit. + // For system transactions, we allow a soft limit with alerting, and a hard limit where we terminate + match check_limit_by_meter!( + !gas_charger.is_unmetered(), + effects_estimated_size, + protocol_config.max_serialized_tx_effects_size_bytes(), + protocol_config.max_serialized_tx_effects_size_bytes_system_tx(), + metrics.excessive_estimated_effects_size + ) { + LimitThresholdCrossed::None => Ok(()), + LimitThresholdCrossed::Soft(_, limit) => { + warn!( + effects_estimated_size = effects_estimated_size, + soft_limit = limit, + "Estimated transaction effects size crossed soft limit", + ); + Ok(()) + } + LimitThresholdCrossed::Hard(_, lim) => Err(ExecutionError::new_with_source( + ExecutionErrorKind::EffectsTooLarge { + current_size: effects_estimated_size as u64, + max_size: lim as u64, + }, + "Transaction effects are too large", + )), + } + } + + #[instrument(name = "check_written_objects_limit", level = "debug", skip_all)] + fn check_written_objects_limit( + temporary_store: &mut TemporaryStore<'_>, + gas_charger: &mut GasCharger, + protocol_config: &ProtocolConfig, + metrics: Arc, + ) -> Result<(), ExecutionError> { + if let (Some(normal_lim), Some(system_lim)) = ( + protocol_config.max_size_written_objects_as_option(), + protocol_config.max_size_written_objects_system_tx_as_option(), + ) { + let written_objects_size = temporary_store.written_objects_size(); + + match check_limit_by_meter!( + !gas_charger.is_unmetered(), + written_objects_size, + normal_lim, + system_lim, + metrics.excessive_written_objects_size + ) { + LimitThresholdCrossed::None => (), + LimitThresholdCrossed::Soft(_, limit) => { + warn!( + written_objects_size = written_objects_size, + soft_limit = limit, + "Written objects size crossed soft limit", + ) + } + LimitThresholdCrossed::Hard(_, lim) => { + return Err(ExecutionError::new_with_source( + ExecutionErrorKind::WrittenObjectsTooLarge { + current_size: written_objects_size as u64, + max_size: lim as u64, + }, + "Written objects size crossed hard limit", + )); + } + }; + } + + Ok(()) + } + + #[instrument(level = "debug", skip_all)] + fn execution_loop( + store: &dyn BackingStore, + temporary_store: &mut TemporaryStore<'_>, + transaction_kind: TransactionKind, + tx_ctx: Rc>, + move_vm: &Arc, + gas_charger: &mut GasCharger, + protocol_config: &ProtocolConfig, + metrics: Arc, + trace_builder_opt: &mut Option, + ) -> ResultWithTimings { + let result = match transaction_kind { + TransactionKind::ChangeEpoch(change_epoch) => { + let builder = ProgrammableTransactionBuilder::new(); + advance_epoch( + builder, + change_epoch, + temporary_store, + store, + tx_ctx, + move_vm, + gas_charger, + protocol_config, + metrics, + trace_builder_opt, + ) + .map_err(|e| (e, vec![]))?; + Ok((Mode::empty_results(), vec![])) + } + TransactionKind::Genesis(GenesisTransaction { objects }) => { + if tx_ctx.borrow().epoch() != 0 { + panic!("BUG: Genesis Transactions can only be executed in epoch 0"); + } + + for genesis_object in objects { + match genesis_object { + sui_types::transaction::GenesisObject::RawObject { data, owner } => { + let object = ObjectInner { + data, + owner, + previous_transaction: tx_ctx.borrow().digest(), + storage_rebate: 0, + }; + temporary_store.create_object(object.into()); + } + } + } + Ok((Mode::empty_results(), vec![])) + } + TransactionKind::ConsensusCommitPrologue(prologue) => { + setup_consensus_commit( + prologue.commit_timestamp_ms, + temporary_store, + store, + tx_ctx, + move_vm, + gas_charger, + protocol_config, + metrics, + trace_builder_opt, + ) + .expect("ConsensusCommitPrologue cannot fail"); + Ok((Mode::empty_results(), vec![])) + } + TransactionKind::ConsensusCommitPrologueV2(prologue) => { + setup_consensus_commit( + prologue.commit_timestamp_ms, + temporary_store, + store, + tx_ctx, + move_vm, + gas_charger, + protocol_config, + metrics, + trace_builder_opt, + ) + .expect("ConsensusCommitPrologueV2 cannot fail"); + Ok((Mode::empty_results(), vec![])) + } + TransactionKind::ConsensusCommitPrologueV3(prologue) => { + setup_consensus_commit( + prologue.commit_timestamp_ms, + temporary_store, + store, + tx_ctx, + move_vm, + gas_charger, + protocol_config, + metrics, + trace_builder_opt, + ) + .expect("ConsensusCommitPrologueV3 cannot fail"); + Ok((Mode::empty_results(), vec![])) + } + TransactionKind::ConsensusCommitPrologueV4(prologue) => { + setup_consensus_commit( + prologue.commit_timestamp_ms, + temporary_store, + store, + tx_ctx, + move_vm, + gas_charger, + protocol_config, + metrics, + trace_builder_opt, + ) + .expect("ConsensusCommitPrologue cannot fail"); + Ok((Mode::empty_results(), vec![])) + } + TransactionKind::ProgrammableTransaction(pt) => { + programmable_transactions::execution::execute::( + protocol_config, + metrics, + move_vm, + temporary_store, + store.as_backing_package_store(), + tx_ctx, + gas_charger, + pt, + trace_builder_opt, + ) + } + TransactionKind::ProgrammableSystemTransaction(pt) => { + programmable_transactions::execution::execute::( + protocol_config, + metrics, + move_vm, + temporary_store, + store.as_backing_package_store(), + tx_ctx, + gas_charger, + pt, + trace_builder_opt, + )?; + Ok((Mode::empty_results(), vec![])) + } + TransactionKind::EndOfEpochTransaction(txns) => { + let mut builder = ProgrammableTransactionBuilder::new(); + let len = txns.len(); + for (i, tx) in txns.into_iter().enumerate() { + match tx { + EndOfEpochTransactionKind::ChangeEpoch(change_epoch) => { + assert_eq!(i, len - 1); + advance_epoch( + builder, + change_epoch, + temporary_store, + store, + tx_ctx, + move_vm, + gas_charger, + protocol_config, + metrics, + trace_builder_opt, + ) + .map_err(|e| (e, vec![]))?; + return Ok((Mode::empty_results(), vec![])); + } + EndOfEpochTransactionKind::AuthenticatorStateCreate => { + assert!(protocol_config.enable_jwk_consensus_updates()); + builder = setup_authenticator_state_create(builder); + } + EndOfEpochTransactionKind::AuthenticatorStateExpire(expire) => { + assert!(protocol_config.enable_jwk_consensus_updates()); + + // TODO: it would be nice if a failure of this function didn't cause + // safe mode. + builder = setup_authenticator_state_expire(builder, expire); + } + EndOfEpochTransactionKind::RandomnessStateCreate => { + assert!(protocol_config.random_beacon()); + builder = setup_randomness_state_create(builder); + } + EndOfEpochTransactionKind::DenyListStateCreate => { + assert!(protocol_config.enable_coin_deny_list_v1()); + builder = setup_coin_deny_list_state_create(builder); + } + EndOfEpochTransactionKind::BridgeStateCreate(chain_id) => { + assert!(protocol_config.enable_bridge()); + builder = setup_bridge_create(builder, chain_id) + } + EndOfEpochTransactionKind::BridgeCommitteeInit(bridge_shared_version) => { + assert!(protocol_config.enable_bridge()); + assert!(protocol_config.should_try_to_finalize_bridge_committee()); + builder = setup_bridge_committee_update(builder, bridge_shared_version) + } + EndOfEpochTransactionKind::StoreExecutionTimeObservations(estimates) => { + if let PerObjectCongestionControlMode::ExecutionTimeEstimate(params) = + protocol_config.per_object_congestion_control_mode() + { + if let Some(chunk_size) = params.observations_chunk_size { + builder = setup_store_execution_time_estimates_v2( + builder, + estimates, + chunk_size as usize, + ); + } else { + builder = + setup_store_execution_time_estimates(builder, estimates); + } + } + } + EndOfEpochTransactionKind::AccumulatorRootCreate => { + assert!(protocol_config.create_root_accumulator_object()); + builder = setup_accumulator_root_create(builder); + } + EndOfEpochTransactionKind::CoinRegistryCreate => { + assert!(protocol_config.enable_coin_registry()); + builder = setup_coin_registry_create(builder); + } + EndOfEpochTransactionKind::DisplayRegistryCreate => { + assert!(protocol_config.enable_display_registry()); + builder = setup_display_registry_create(builder); + } + } + } + unreachable!( + "EndOfEpochTransactionKind::ChangeEpoch should be the last transaction in the list" + ) + } + TransactionKind::AuthenticatorStateUpdate(auth_state_update) => { + setup_authenticator_state_update( + auth_state_update, + temporary_store, + store, + tx_ctx, + move_vm, + gas_charger, + protocol_config, + metrics, + trace_builder_opt, + ) + .map_err(|e| (e, vec![]))?; + Ok((Mode::empty_results(), vec![])) + } + TransactionKind::RandomnessStateUpdate(randomness_state_update) => { + setup_randomness_state_update( + randomness_state_update, + temporary_store, + store, + tx_ctx, + move_vm, + gas_charger, + protocol_config, + metrics, + trace_builder_opt, + ) + .map_err(|e| (e, vec![]))?; + Ok((Mode::empty_results(), vec![])) + } + }?; + temporary_store + .check_execution_results_consistency() + .map_err(|e| (e, vec![]))?; + Ok(result) + } + + fn mint_epoch_rewards_in_pt( + builder: &mut ProgrammableTransactionBuilder, + params: &AdvanceEpochParams, + ) -> (Argument, Argument) { + // Create storage rewards. + let storage_charge_arg = builder + .input(CallArg::Pure( + bcs::to_bytes(¶ms.storage_charge).unwrap(), + )) + .unwrap(); + let storage_rewards = builder.programmable_move_call( + SUI_FRAMEWORK_PACKAGE_ID, + BALANCE_MODULE_NAME.to_owned(), + BALANCE_CREATE_REWARDS_FUNCTION_NAME.to_owned(), + vec![GAS::type_tag()], + vec![storage_charge_arg], + ); + + // Create computation rewards. + let computation_charge_arg = builder + .input(CallArg::Pure( + bcs::to_bytes(¶ms.computation_charge).unwrap(), + )) + .unwrap(); + let computation_rewards = builder.programmable_move_call( + SUI_FRAMEWORK_PACKAGE_ID, + BALANCE_MODULE_NAME.to_owned(), + BALANCE_CREATE_REWARDS_FUNCTION_NAME.to_owned(), + vec![GAS::type_tag()], + vec![computation_charge_arg], + ); + (storage_rewards, computation_rewards) + } + + pub fn construct_advance_epoch_pt( + mut builder: ProgrammableTransactionBuilder, + params: &AdvanceEpochParams, + ) -> Result { + // Step 1: Create storage and computation rewards. + let (storage_rewards, computation_rewards) = mint_epoch_rewards_in_pt(&mut builder, params); + + // Step 2: Advance the epoch. + let mut arguments = vec![storage_rewards, computation_rewards]; + let call_arg_arguments = vec![ + CallArg::SUI_SYSTEM_MUT, + CallArg::Pure(bcs::to_bytes(¶ms.epoch).unwrap()), + CallArg::Pure(bcs::to_bytes(¶ms.next_protocol_version.as_u64()).unwrap()), + CallArg::Pure(bcs::to_bytes(¶ms.storage_rebate).unwrap()), + CallArg::Pure(bcs::to_bytes(¶ms.non_refundable_storage_fee).unwrap()), + CallArg::Pure(bcs::to_bytes(¶ms.storage_fund_reinvest_rate).unwrap()), + CallArg::Pure(bcs::to_bytes(¶ms.reward_slashing_rate).unwrap()), + CallArg::Pure(bcs::to_bytes(¶ms.epoch_start_timestamp_ms).unwrap()), + ] + .into_iter() + .map(|a| builder.input(a)) + .collect::>(); + + assert_invariant!( + call_arg_arguments.is_ok(), + "Unable to generate args for advance_epoch transaction!" + ); + + arguments.append(&mut call_arg_arguments.unwrap()); + + info!("Call arguments to advance_epoch transaction: {:?}", params); + + let storage_rebates = builder.programmable_move_call( + SUI_SYSTEM_PACKAGE_ID, + SUI_SYSTEM_MODULE_NAME.to_owned(), + ADVANCE_EPOCH_FUNCTION_NAME.to_owned(), + vec![], + arguments, + ); + + // Step 3: Destroy the storage rebates. + builder.programmable_move_call( + SUI_FRAMEWORK_PACKAGE_ID, + BALANCE_MODULE_NAME.to_owned(), + BALANCE_DESTROY_REBATES_FUNCTION_NAME.to_owned(), + vec![GAS::type_tag()], + vec![storage_rebates], + ); + Ok(builder.finish()) + } + + pub fn construct_advance_epoch_safe_mode_pt( + params: &AdvanceEpochParams, + protocol_config: &ProtocolConfig, + ) -> Result { + let mut builder = ProgrammableTransactionBuilder::new(); + // Step 1: Create storage and computation rewards. + let (storage_rewards, computation_rewards) = mint_epoch_rewards_in_pt(&mut builder, params); + + // Step 2: Advance the epoch. + let mut arguments = vec![storage_rewards, computation_rewards]; + + let mut args = vec![ + CallArg::SUI_SYSTEM_MUT, + CallArg::Pure(bcs::to_bytes(¶ms.epoch).unwrap()), + CallArg::Pure(bcs::to_bytes(¶ms.next_protocol_version.as_u64()).unwrap()), + CallArg::Pure(bcs::to_bytes(¶ms.storage_rebate).unwrap()), + CallArg::Pure(bcs::to_bytes(¶ms.non_refundable_storage_fee).unwrap()), + ]; + + if protocol_config.get_advance_epoch_start_time_in_safe_mode() { + args.push(CallArg::Pure( + bcs::to_bytes(¶ms.epoch_start_timestamp_ms).unwrap(), + )); + } + + let call_arg_arguments = args + .into_iter() + .map(|a| builder.input(a)) + .collect::>(); + + assert_invariant!( + call_arg_arguments.is_ok(), + "Unable to generate args for advance_epoch transaction!" + ); + + arguments.append(&mut call_arg_arguments.unwrap()); + + info!("Call arguments to advance_epoch transaction: {:?}", params); + + builder.programmable_move_call( + SUI_SYSTEM_PACKAGE_ID, + SUI_SYSTEM_MODULE_NAME.to_owned(), + ADVANCE_EPOCH_SAFE_MODE_FUNCTION_NAME.to_owned(), + vec![], + arguments, + ); + + Ok(builder.finish()) + } + + fn advance_epoch( + builder: ProgrammableTransactionBuilder, + change_epoch: ChangeEpoch, + temporary_store: &mut TemporaryStore<'_>, + store: &dyn BackingStore, + tx_ctx: Rc>, + move_vm: &Arc, + gas_charger: &mut GasCharger, + protocol_config: &ProtocolConfig, + metrics: Arc, + trace_builder_opt: &mut Option, + ) -> Result<(), ExecutionError> { + let params = AdvanceEpochParams { + epoch: change_epoch.epoch, + next_protocol_version: change_epoch.protocol_version, + storage_charge: change_epoch.storage_charge, + computation_charge: change_epoch.computation_charge, + storage_rebate: change_epoch.storage_rebate, + non_refundable_storage_fee: change_epoch.non_refundable_storage_fee, + storage_fund_reinvest_rate: protocol_config.storage_fund_reinvest_rate(), + reward_slashing_rate: protocol_config.reward_slashing_rate(), + epoch_start_timestamp_ms: change_epoch.epoch_start_timestamp_ms, + }; + let advance_epoch_pt = construct_advance_epoch_pt(builder, ¶ms)?; + let result = programmable_transactions::execution::execute::( + protocol_config, + metrics.clone(), + move_vm, + temporary_store, + store.as_backing_package_store(), + tx_ctx.clone(), + gas_charger, + advance_epoch_pt, + trace_builder_opt, + ); + + #[cfg(msim)] + let result = maybe_modify_result(result, change_epoch.epoch); + + if let Err(err) = &result { + tracing::error!( + "Failed to execute advance epoch transaction. Switching to safe mode. Error: {:?}. Input objects: {:?}. Tx data: {:?}", + err.0, + temporary_store.objects(), + change_epoch, + ); + temporary_store.drop_writes(); + // Must reset the storage rebate since we are re-executing. + gas_charger.reset_storage_cost_and_rebate(); + + if protocol_config.get_advance_epoch_start_time_in_safe_mode() { + temporary_store.advance_epoch_safe_mode(¶ms, protocol_config); + } else { + let advance_epoch_safe_mode_pt = + construct_advance_epoch_safe_mode_pt(¶ms, protocol_config)?; + programmable_transactions::execution::execute::( + protocol_config, + metrics.clone(), + move_vm, + temporary_store, + store.as_backing_package_store(), + tx_ctx.clone(), + gas_charger, + advance_epoch_safe_mode_pt, + trace_builder_opt, + ) + .map_err(|(e, _)| e) + .expect("Advance epoch with safe mode must succeed"); + } + } + + if protocol_config.fresh_vm_on_framework_upgrade() { + let new_vm = new_move_vm( + all_natives(/* silent */ true, protocol_config), + protocol_config, + ) + .expect("Failed to create new MoveVM"); + process_system_packages( + change_epoch, + temporary_store, + store, + tx_ctx, + &new_vm, + gas_charger, + protocol_config, + metrics, + trace_builder_opt, + ); + } else { + process_system_packages( + change_epoch, + temporary_store, + store, + tx_ctx, + move_vm, + gas_charger, + protocol_config, + metrics, + trace_builder_opt, + ); + } + Ok(()) + } + + fn process_system_packages( + change_epoch: ChangeEpoch, + temporary_store: &mut TemporaryStore<'_>, + store: &dyn BackingStore, + tx_ctx: Rc>, + move_vm: &MoveVM, + gas_charger: &mut GasCharger, + protocol_config: &ProtocolConfig, + metrics: Arc, + trace_builder_opt: &mut Option, + ) { + let digest = tx_ctx.borrow().digest(); + let binary_config = protocol_config.binary_config(None); + for (version, modules, dependencies) in change_epoch.system_packages.into_iter() { + let deserialized_modules: Vec<_> = modules + .iter() + .map(|m| CompiledModule::deserialize_with_config(m, &binary_config).unwrap()) + .collect(); + + if version == OBJECT_START_VERSION { + let package_id = deserialized_modules.first().unwrap().address(); + info!("adding new system package {package_id}"); + + let publish_pt = { + let mut b = ProgrammableTransactionBuilder::new(); + b.command(Command::Publish(modules, dependencies)); + b.finish() + }; + + programmable_transactions::execution::execute::( + protocol_config, + metrics.clone(), + move_vm, + temporary_store, + store.as_backing_package_store(), + tx_ctx.clone(), + gas_charger, + publish_pt, + trace_builder_opt, + ) + .map_err(|(e, _)| e) + .expect("System Package Publish must succeed"); + } else { + let mut new_package = Object::new_system_package( + &deserialized_modules, + version, + dependencies, + digest, + ); + + info!( + "upgraded system package {:?}", + new_package.compute_object_reference() + ); + + // Decrement the version before writing the package so that the store can record the + // version growing by one in the effects. + new_package + .data + .try_as_package_mut() + .unwrap() + .decrement_version(); + + // upgrade of a previously existing framework module + temporary_store.upgrade_system_package(new_package); + } + } + } + + /// Perform metadata updates in preparation for the transactions in the upcoming checkpoint: + /// + /// - Set the timestamp for the `Clock` shared object from the timestamp in the header from + /// consensus. + fn setup_consensus_commit( + consensus_commit_timestamp_ms: CheckpointTimestamp, + temporary_store: &mut TemporaryStore<'_>, + store: &dyn BackingStore, + tx_ctx: Rc>, + move_vm: &Arc, + gas_charger: &mut GasCharger, + protocol_config: &ProtocolConfig, + metrics: Arc, + trace_builder_opt: &mut Option, + ) -> Result<(), ExecutionError> { + let pt = { + let mut builder = ProgrammableTransactionBuilder::new(); + let res = builder.move_call( + SUI_FRAMEWORK_ADDRESS.into(), + CLOCK_MODULE_NAME.to_owned(), + CONSENSUS_COMMIT_PROLOGUE_FUNCTION_NAME.to_owned(), + vec![], + vec![ + CallArg::CLOCK_MUT, + CallArg::Pure(bcs::to_bytes(&consensus_commit_timestamp_ms).unwrap()), + ], + ); + assert_invariant!( + res.is_ok(), + "Unable to generate consensus_commit_prologue transaction!" + ); + builder.finish() + }; + programmable_transactions::execution::execute::( + protocol_config, + metrics, + move_vm, + temporary_store, + store.as_backing_package_store(), + tx_ctx, + gas_charger, + pt, + trace_builder_opt, + ) + .map_err(|(e, _)| e)?; + Ok(()) + } + + fn setup_authenticator_state_create( + mut builder: ProgrammableTransactionBuilder, + ) -> ProgrammableTransactionBuilder { + builder + .move_call( + SUI_FRAMEWORK_ADDRESS.into(), + AUTHENTICATOR_STATE_MODULE_NAME.to_owned(), + AUTHENTICATOR_STATE_CREATE_FUNCTION_NAME.to_owned(), + vec![], + vec![], + ) + .expect("Unable to generate authenticator_state_create transaction!"); + builder + } + + fn setup_randomness_state_create( + mut builder: ProgrammableTransactionBuilder, + ) -> ProgrammableTransactionBuilder { + builder + .move_call( + SUI_FRAMEWORK_ADDRESS.into(), + RANDOMNESS_MODULE_NAME.to_owned(), + RANDOMNESS_STATE_CREATE_FUNCTION_NAME.to_owned(), + vec![], + vec![], + ) + .expect("Unable to generate randomness_state_create transaction!"); + builder + } + + fn setup_bridge_create( + mut builder: ProgrammableTransactionBuilder, + chain_id: ChainIdentifier, + ) -> ProgrammableTransactionBuilder { + let bridge_uid = builder + .input(CallArg::Pure(UID::new(SUI_BRIDGE_OBJECT_ID).to_bcs_bytes())) + .expect("Unable to create Bridge object UID!"); + + let bridge_chain_id = if chain_id == get_mainnet_chain_identifier() { + BridgeChainId::SuiMainnet as u8 + } else if chain_id == get_testnet_chain_identifier() { + BridgeChainId::SuiTestnet as u8 + } else { + // How do we distinguish devnet from other test envs? + BridgeChainId::SuiCustom as u8 + }; + + let bridge_chain_id = builder.pure(bridge_chain_id).unwrap(); + builder.programmable_move_call( + BRIDGE_ADDRESS.into(), + BRIDGE_MODULE_NAME.to_owned(), + BRIDGE_CREATE_FUNCTION_NAME.to_owned(), + vec![], + vec![bridge_uid, bridge_chain_id], + ); + builder + } + + fn setup_bridge_committee_update( + mut builder: ProgrammableTransactionBuilder, + bridge_shared_version: SequenceNumber, + ) -> ProgrammableTransactionBuilder { + let bridge = builder + .obj(ObjectArg::SharedObject { + id: SUI_BRIDGE_OBJECT_ID, + initial_shared_version: bridge_shared_version, + mutability: sui_types::transaction::SharedObjectMutability::Mutable, + }) + .expect("Unable to create Bridge object arg!"); + let system_state = builder + .obj(ObjectArg::SUI_SYSTEM_MUT) + .expect("Unable to create System State object arg!"); + + let voting_power = builder.programmable_move_call( + SUI_SYSTEM_PACKAGE_ID, + SUI_SYSTEM_MODULE_NAME.to_owned(), + ident_str!("validator_voting_powers").to_owned(), + vec![], + vec![system_state], + ); + + // Hardcoding min stake participation to 75.00% + // TODO: We need to set a correct value or make this configurable. + let min_stake_participation_percentage = builder + .input(CallArg::Pure( + bcs::to_bytes(&BRIDGE_COMMITTEE_MINIMAL_VOTING_POWER).unwrap(), + )) + .unwrap(); + + builder.programmable_move_call( + BRIDGE_ADDRESS.into(), + BRIDGE_MODULE_NAME.to_owned(), + BRIDGE_INIT_COMMITTEE_FUNCTION_NAME.to_owned(), + vec![], + vec![bridge, voting_power, min_stake_participation_percentage], + ); + builder + } + + fn setup_authenticator_state_update( + update: AuthenticatorStateUpdate, + temporary_store: &mut TemporaryStore<'_>, + store: &dyn BackingStore, + tx_ctx: Rc>, + move_vm: &Arc, + gas_charger: &mut GasCharger, + protocol_config: &ProtocolConfig, + metrics: Arc, + trace_builder_opt: &mut Option, + ) -> Result<(), ExecutionError> { + let pt = { + let mut builder = ProgrammableTransactionBuilder::new(); + let res = builder.move_call( + SUI_FRAMEWORK_ADDRESS.into(), + AUTHENTICATOR_STATE_MODULE_NAME.to_owned(), + AUTHENTICATOR_STATE_UPDATE_FUNCTION_NAME.to_owned(), + vec![], + vec![ + CallArg::Object(ObjectArg::SharedObject { + id: SUI_AUTHENTICATOR_STATE_OBJECT_ID, + initial_shared_version: update.authenticator_obj_initial_shared_version, + mutability: sui_types::transaction::SharedObjectMutability::Mutable, + }), + CallArg::Pure(bcs::to_bytes(&update.new_active_jwks).unwrap()), + ], + ); + assert_invariant!( + res.is_ok(), + "Unable to generate authenticator_state_update transaction!" + ); + builder.finish() + }; + programmable_transactions::execution::execute::( + protocol_config, + metrics, + move_vm, + temporary_store, + store.as_backing_package_store(), + tx_ctx, + gas_charger, + pt, + trace_builder_opt, + ) + .map_err(|(e, _)| e)?; + Ok(()) + } + + fn setup_authenticator_state_expire( + mut builder: ProgrammableTransactionBuilder, + expire: AuthenticatorStateExpire, + ) -> ProgrammableTransactionBuilder { + builder + .move_call( + SUI_FRAMEWORK_ADDRESS.into(), + AUTHENTICATOR_STATE_MODULE_NAME.to_owned(), + AUTHENTICATOR_STATE_EXPIRE_JWKS_FUNCTION_NAME.to_owned(), + vec![], + vec![ + CallArg::Object(ObjectArg::SharedObject { + id: SUI_AUTHENTICATOR_STATE_OBJECT_ID, + initial_shared_version: expire.authenticator_obj_initial_shared_version, + mutability: sui_types::transaction::SharedObjectMutability::Mutable, + }), + CallArg::Pure(bcs::to_bytes(&expire.min_epoch).unwrap()), + ], + ) + .expect("Unable to generate authenticator_state_expire transaction!"); + builder + } + + fn setup_randomness_state_update( + update: RandomnessStateUpdate, + temporary_store: &mut TemporaryStore<'_>, + store: &dyn BackingStore, + tx_ctx: Rc>, + move_vm: &Arc, + gas_charger: &mut GasCharger, + protocol_config: &ProtocolConfig, + metrics: Arc, + trace_builder_opt: &mut Option, + ) -> Result<(), ExecutionError> { + let pt = { + let mut builder = ProgrammableTransactionBuilder::new(); + let res = builder.move_call( + SUI_FRAMEWORK_ADDRESS.into(), + RANDOMNESS_MODULE_NAME.to_owned(), + RANDOMNESS_STATE_UPDATE_FUNCTION_NAME.to_owned(), + vec![], + vec![ + CallArg::Object(ObjectArg::SharedObject { + id: SUI_RANDOMNESS_STATE_OBJECT_ID, + initial_shared_version: update.randomness_obj_initial_shared_version, + mutability: sui_types::transaction::SharedObjectMutability::Mutable, + }), + CallArg::Pure(bcs::to_bytes(&update.randomness_round).unwrap()), + CallArg::Pure(bcs::to_bytes(&update.random_bytes).unwrap()), + ], + ); + assert_invariant!( + res.is_ok(), + "Unable to generate randomness_state_update transaction!" + ); + builder.finish() + }; + programmable_transactions::execution::execute::( + protocol_config, + metrics, + move_vm, + temporary_store, + store.as_backing_package_store(), + tx_ctx, + gas_charger, + pt, + trace_builder_opt, + ) + .map_err(|(e, _)| e)?; + Ok(()) + } + + fn setup_coin_deny_list_state_create( + mut builder: ProgrammableTransactionBuilder, + ) -> ProgrammableTransactionBuilder { + builder + .move_call( + SUI_FRAMEWORK_ADDRESS.into(), + DENY_LIST_MODULE.to_owned(), + DENY_LIST_CREATE_FUNC.to_owned(), + vec![], + vec![], + ) + .expect("Unable to generate coin_deny_list_create transaction!"); + builder + } + + fn setup_store_execution_time_estimates( + mut builder: ProgrammableTransactionBuilder, + estimates: StoredExecutionTimeObservations, + ) -> ProgrammableTransactionBuilder { + let system_state = builder.obj(ObjectArg::SUI_SYSTEM_MUT).unwrap(); + // This is stored as a vector in Move, so we first convert to bytes before again + // serializing inside the call to `pure`. + let estimates_bytes = bcs::to_bytes(&estimates).unwrap(); + let estimates_arg = builder.pure(estimates_bytes).unwrap(); + builder.programmable_move_call( + SUI_SYSTEM_PACKAGE_ID, + SUI_SYSTEM_MODULE_NAME.to_owned(), + ident_str!("store_execution_time_estimates").to_owned(), + vec![], + vec![system_state, estimates_arg], + ); + builder + } + + fn setup_store_execution_time_estimates_v2( + mut builder: ProgrammableTransactionBuilder, + estimates: StoredExecutionTimeObservations, + chunk_size: usize, + ) -> ProgrammableTransactionBuilder { + let system_state = builder.obj(ObjectArg::SUI_SYSTEM_MUT).unwrap(); + + let estimate_chunks = estimates.chunk_observations(chunk_size); + + let chunk_bytes: Vec> = estimate_chunks + .into_iter() + .map(|chunk| bcs::to_bytes(&chunk).unwrap()) + .collect(); + + let chunks_arg = builder.pure(chunk_bytes).unwrap(); + + builder.programmable_move_call( + SUI_SYSTEM_PACKAGE_ID, + SUI_SYSTEM_MODULE_NAME.to_owned(), + ident_str!("store_execution_time_estimates_v2").to_owned(), + vec![], + vec![system_state, chunks_arg], + ); + builder + } + + fn setup_accumulator_root_create( + mut builder: ProgrammableTransactionBuilder, + ) -> ProgrammableTransactionBuilder { + builder + .move_call( + SUI_FRAMEWORK_ADDRESS.into(), + ACCUMULATOR_ROOT_MODULE.to_owned(), + ACCUMULATOR_ROOT_CREATE_FUNC.to_owned(), + vec![], + vec![], + ) + .expect("Unable to generate accumulator_root_create transaction!"); + builder + } + + fn setup_coin_registry_create( + mut builder: ProgrammableTransactionBuilder, + ) -> ProgrammableTransactionBuilder { + builder + .move_call( + SUI_FRAMEWORK_ADDRESS.into(), + ident_str!("coin_registry").to_owned(), + ident_str!("create").to_owned(), + vec![], + vec![], + ) + .expect("Unable to generate coin_registry_create transaction!"); + builder + } + + fn setup_display_registry_create( + mut builder: ProgrammableTransactionBuilder, + ) -> ProgrammableTransactionBuilder { + builder + .move_call( + SUI_FRAMEWORK_ADDRESS.into(), + ident_str!("display_registry").to_owned(), + ident_str!("create").to_owned(), + vec![], + vec![], + ) + .expect("Unable to generate display_registry_create transaction!"); + builder + } +} diff --git a/sui-execution/replay_cut/sui-adapter/src/execution_mode.rs b/sui-execution/replay_cut/sui-adapter/src/execution_mode.rs new file mode 100644 index 0000000000000..2007a15600a58 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/execution_mode.rs @@ -0,0 +1,375 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::execution_value::{RawValueType, Value}; +use crate::type_resolver::TypeTagResolver; +use move_core_types::language_storage::TypeTag; +use sui_types::{ + error::ExecutionError, execution::ExecutionResult, transaction::Argument, transfer::Receiving, +}; + +pub type TransactionIndex = usize; + +pub trait ExecutionMode { + /// All updates to a Arguments used in that Command + type ArgumentUpdates; + /// the gathered results from batched executions + type ExecutionResults; + + /// Controls the calling of arbitrary Move functions + fn allow_arbitrary_function_calls() -> bool; + + /// Controls the ability to instantiate any Move function parameter with a Pure call arg. + /// In other words, you can instantiate any struct or object or other value with its BCS byte + fn allow_arbitrary_values() -> bool; + + /// Do not perform conservation checks after execution. + fn skip_conservation_checks() -> bool; + + /// If not set, the package ID should be calculated like an object and an + /// UpgradeCap is produced + fn packages_are_predefined() -> bool; + + fn empty_arguments() -> Self::ArgumentUpdates; + + fn empty_results() -> Self::ExecutionResults; + + fn add_argument_update( + resolver: &impl TypeTagResolver, + acc: &mut Self::ArgumentUpdates, + arg: Argument, + _new_value: &Value, + ) -> Result<(), ExecutionError>; + + fn finish_command( + resolver: &impl TypeTagResolver, + acc: &mut Self::ExecutionResults, + argument_updates: Self::ArgumentUpdates, + command_result: &[Value], + ) -> Result<(), ExecutionError>; + + // == Arg/Result V2 == + + const TRACK_EXECUTION: bool; + + fn add_argument_update_v2( + acc: &mut Self::ArgumentUpdates, + arg: Argument, + bytes: Vec, + type_: TypeTag, + ) -> Result<(), ExecutionError>; + + fn finish_command_v2( + acc: &mut Self::ExecutionResults, + argument_updates: Vec<(Argument, Vec, TypeTag)>, + command_result: Vec<(Vec, TypeTag)>, + ) -> Result<(), ExecutionError>; +} + +#[derive(Copy, Clone)] +pub struct Normal; + +impl ExecutionMode for Normal { + type ArgumentUpdates = (); + type ExecutionResults = (); + + fn allow_arbitrary_function_calls() -> bool { + false + } + + fn allow_arbitrary_values() -> bool { + false + } + + fn skip_conservation_checks() -> bool { + false + } + + fn packages_are_predefined() -> bool { + false + } + + fn empty_arguments() -> Self::ArgumentUpdates {} + + fn empty_results() -> Self::ExecutionResults {} + + fn add_argument_update( + _resolver: &impl TypeTagResolver, + _acc: &mut Self::ArgumentUpdates, + _arg: Argument, + _new_value: &Value, + ) -> Result<(), ExecutionError> { + Ok(()) + } + + fn finish_command( + _resolver: &impl TypeTagResolver, + _acc: &mut Self::ExecutionResults, + _argument_updates: Self::ArgumentUpdates, + _command_result: &[Value], + ) -> Result<(), ExecutionError> { + Ok(()) + } + + const TRACK_EXECUTION: bool = false; + + fn add_argument_update_v2( + _acc: &mut Self::ArgumentUpdates, + _arg: Argument, + _bytes: Vec, + _type_: TypeTag, + ) -> Result<(), ExecutionError> { + invariant_violation!("should not be called"); + } + + fn finish_command_v2( + _acc: &mut Self::ExecutionResults, + _argument_updates: Vec<(Argument, Vec, TypeTag)>, + _command_result: Vec<(Vec, TypeTag)>, + ) -> Result<(), ExecutionError> { + invariant_violation!("should not be called"); + } +} + +#[derive(Copy, Clone)] +pub struct Genesis; + +impl ExecutionMode for Genesis { + type ArgumentUpdates = (); + type ExecutionResults = (); + + fn allow_arbitrary_function_calls() -> bool { + true + } + + fn allow_arbitrary_values() -> bool { + true + } + + fn packages_are_predefined() -> bool { + true + } + + fn skip_conservation_checks() -> bool { + false + } + + fn empty_arguments() -> Self::ArgumentUpdates {} + + fn empty_results() -> Self::ExecutionResults {} + + fn add_argument_update( + _resolver: &impl TypeTagResolver, + _acc: &mut Self::ArgumentUpdates, + _arg: Argument, + _new_value: &Value, + ) -> Result<(), ExecutionError> { + Ok(()) + } + + fn finish_command( + _resolver: &impl TypeTagResolver, + _acc: &mut Self::ExecutionResults, + _argument_updates: Self::ArgumentUpdates, + _command_result: &[Value], + ) -> Result<(), ExecutionError> { + Ok(()) + } + + const TRACK_EXECUTION: bool = false; + + fn add_argument_update_v2( + _acc: &mut Self::ArgumentUpdates, + _arg: Argument, + _bytes: Vec, + _type_: TypeTag, + ) -> Result<(), ExecutionError> { + invariant_violation!("should not be called"); + } + + fn finish_command_v2( + _acc: &mut Self::ExecutionResults, + _argument_updates: Vec<(Argument, Vec, TypeTag)>, + _command_result: Vec<(Vec, TypeTag)>, + ) -> Result<(), ExecutionError> { + invariant_violation!("should not be called"); + } +} + +#[derive(Copy, Clone)] +pub struct System; + +/// Execution mode for executing a system transaction, including the epoch change +/// transaction and the consensus commit prologue. In this mode, we allow calls to +/// any function bypassing visibility. +impl ExecutionMode for System { + type ArgumentUpdates = (); + type ExecutionResults = (); + + fn allow_arbitrary_function_calls() -> bool { + // allows bypassing visibility for system calls + true + } + + fn allow_arbitrary_values() -> bool { + // For AuthenticatorStateUpdate, we need to be able to pass in a vector of + // JWKs, so we need to allow arbitrary values. + true + } + + fn skip_conservation_checks() -> bool { + false + } + + fn packages_are_predefined() -> bool { + true + } + + fn empty_arguments() -> Self::ArgumentUpdates {} + + fn empty_results() -> Self::ExecutionResults {} + + fn add_argument_update( + _resolver: &impl TypeTagResolver, + _acc: &mut Self::ArgumentUpdates, + _arg: Argument, + _new_value: &Value, + ) -> Result<(), ExecutionError> { + Ok(()) + } + + fn finish_command( + _resolver: &impl TypeTagResolver, + _acc: &mut Self::ExecutionResults, + _argument_updates: Self::ArgumentUpdates, + _command_result: &[Value], + ) -> Result<(), ExecutionError> { + Ok(()) + } + + const TRACK_EXECUTION: bool = false; + + fn add_argument_update_v2( + _acc: &mut Self::ArgumentUpdates, + _arg: Argument, + _bytes: Vec, + _type_: TypeTag, + ) -> Result<(), ExecutionError> { + invariant_violation!("should not be called"); + } + + fn finish_command_v2( + _acc: &mut Self::ExecutionResults, + _argument_updates: Vec<(Argument, Vec, TypeTag)>, + _command_result: Vec<(Vec, TypeTag)>, + ) -> Result<(), ExecutionError> { + invariant_violation!("should not be called"); + } +} + +/// WARNING! Using this mode will bypass all normal checks around Move entry functions! This +/// includes the various rules for function arguments, meaning any object can be created just from +/// BCS bytes! +pub struct DevInspect; + +impl ExecutionMode for DevInspect { + type ArgumentUpdates = Vec<(Argument, Vec, TypeTag)>; + type ExecutionResults = Vec; + + fn allow_arbitrary_function_calls() -> bool { + SKIP_ALL_CHECKS + } + + fn allow_arbitrary_values() -> bool { + SKIP_ALL_CHECKS + } + + fn skip_conservation_checks() -> bool { + SKIP_ALL_CHECKS + } + + fn packages_are_predefined() -> bool { + false + } + + fn empty_arguments() -> Self::ArgumentUpdates { + vec![] + } + + fn empty_results() -> Self::ExecutionResults { + vec![] + } + + fn add_argument_update( + resolver: &impl TypeTagResolver, + acc: &mut Self::ArgumentUpdates, + arg: Argument, + new_value: &Value, + ) -> Result<(), ExecutionError> { + let (bytes, type_tag) = value_to_bytes_and_tag(resolver, new_value)?; + acc.push((arg, bytes, type_tag)); + Ok(()) + } + + fn finish_command( + resolver: &impl TypeTagResolver, + acc: &mut Self::ExecutionResults, + argument_updates: Self::ArgumentUpdates, + command_result: &[Value], + ) -> Result<(), ExecutionError> { + let command_bytes = command_result + .iter() + .map(|value| value_to_bytes_and_tag(resolver, value)) + .collect::>()?; + acc.push((argument_updates, command_bytes)); + Ok(()) + } + + const TRACK_EXECUTION: bool = true; + + fn add_argument_update_v2( + acc: &mut Self::ArgumentUpdates, + arg: Argument, + bytes: Vec, + type_: TypeTag, + ) -> Result<(), ExecutionError> { + acc.push((arg, bytes, type_)); + Ok(()) + } + + fn finish_command_v2( + acc: &mut Self::ExecutionResults, + argument_updates: Vec<(Argument, Vec, TypeTag)>, + command_result: Vec<(Vec, TypeTag)>, + ) -> Result<(), ExecutionError> { + acc.push((argument_updates, command_result)); + Ok(()) + } +} + +fn value_to_bytes_and_tag( + resolver: &impl TypeTagResolver, + value: &Value, +) -> Result<(Vec, TypeTag), ExecutionError> { + let (type_tag, bytes) = match value { + Value::Object(obj) => { + let tag = resolver.get_type_tag(&obj.type_)?; + let mut bytes = vec![]; + obj.write_bcs_bytes(&mut bytes, None)?; + (tag, bytes) + } + Value::Raw(RawValueType::Any, bytes) => { + // this case shouldn't happen + (TypeTag::Vector(Box::new(TypeTag::U8)), bytes.clone()) + } + Value::Raw(RawValueType::Loaded { ty, .. }, bytes) => { + let tag = resolver.get_type_tag(ty)?; + (tag, bytes.clone()) + } + Value::Receiving(id, seqno, _) => ( + Receiving::type_tag(), + Receiving::new(*id, *seqno).to_bcs_bytes(), + ), + }; + Ok((bytes, type_tag)) +} diff --git a/sui-execution/replay_cut/sui-adapter/src/execution_value.rs b/sui-execution/replay_cut/sui-adapter/src/execution_value.rs new file mode 100644 index 0000000000000..5f6a946508b86 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/execution_value.rs @@ -0,0 +1,385 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::BTreeSet; + +use move_binary_format::file_format::AbilitySet; +use move_core_types::u256::U256; +use move_vm_types::loaded_data::runtime_types::Type; +use serde::Deserialize; +use sui_types::{ + base_types::{ObjectID, SequenceNumber, SuiAddress}, + coin::Coin, + error::{ExecutionError, ExecutionErrorKind}, + execution_status::CommandArgumentError, + funds_accumulator::Withdrawal, + object::Owner, + storage::{BackingPackageStore, ChildObjectResolver, StorageView}, + transfer::Receiving, +}; + +pub trait SuiResolver: BackingPackageStore { + fn as_backing_package_store(&self) -> &dyn BackingPackageStore; +} + +impl SuiResolver for T +where + T: BackingPackageStore, +{ + fn as_backing_package_store(&self) -> &dyn BackingPackageStore { + self + } +} + +/// Interface with the store necessary to execute a programmable transaction +pub trait ExecutionState: StorageView + SuiResolver { + fn as_sui_resolver(&self) -> &dyn SuiResolver; + fn as_child_resolver(&self) -> &dyn ChildObjectResolver; +} + +impl ExecutionState for T +where + T: StorageView, + T: SuiResolver, +{ + fn as_sui_resolver(&self) -> &dyn SuiResolver { + self + } + + fn as_child_resolver(&self) -> &dyn ChildObjectResolver { + self + } +} + +#[derive(Clone, Debug)] +pub enum Mutability { + Mutable, + Immutable, + NonExclusiveWrite, +} + +#[derive(Clone, Debug)] +pub enum InputObjectMetadata { + Receiving { + id: ObjectID, + version: SequenceNumber, + }, + InputObject { + id: ObjectID, + mutability: Mutability, + owner: Owner, + version: SequenceNumber, + }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UsageKind { + BorrowImm, + BorrowMut, + ByValue, +} + +#[derive(Clone, Copy)] +pub enum CommandKind { + MoveCall, + MakeMoveVec, + TransferObjects, + SplitCoins, + MergeCoins, + Publish, + Upgrade, +} + +#[derive(Clone, Debug)] +pub struct InputValue { + /// Used to remember the object ID and owner even if the value is taken + pub object_metadata: Option, + pub inner: ResultValue, +} + +#[derive(Clone, Debug)] +pub struct ResultValue { + /// This is used primarily for values that have `copy` but not `drop` as they must have been + /// copied after the last borrow, otherwise we cannot consider the last "copy" to be instead + /// a "move" of the value. + pub last_usage_kind: Option, + pub value: Option, + pub shared_object_ids: BTreeSet, +} + +#[derive(Debug, Clone)] +pub enum Value { + Object(ObjectValue), + Raw(RawValueType, Vec), + Receiving(ObjectID, SequenceNumber, Option), +} + +#[derive(Debug, Clone)] +pub struct ObjectValue { + pub type_: Type, + pub has_public_transfer: bool, + // true if it has been used in a public, non-entry Move call + // In other words, false if all usages have been with non-Move commands or + // entry Move functions + pub used_in_non_entry_move_call: bool, + pub contents: ObjectContents, +} + +#[derive(Debug, Copy, Clone)] +pub enum SizeBound { + Object(u64), + VectorElem(u64), + Raw(u64), +} + +#[derive(Debug, Clone)] +pub enum ObjectContents { + Coin(Coin), + Raw(Vec), +} + +#[derive(Debug, Clone)] +pub enum RawValueType { + Any, + Loaded { + ty: Type, + abilities: AbilitySet, + used_in_non_entry_move_call: bool, + }, +} + +impl InputObjectMetadata { + pub fn id(&self) -> ObjectID { + match self { + InputObjectMetadata::Receiving { id, .. } => *id, + InputObjectMetadata::InputObject { id, .. } => *id, + } + } + + pub fn version(&self) -> SequenceNumber { + match self { + InputObjectMetadata::Receiving { version, .. } => *version, + InputObjectMetadata::InputObject { version, .. } => *version, + } + } +} + +impl InputValue { + pub fn new_object(object_metadata: InputObjectMetadata, value: ObjectValue) -> Self { + let mut inner = ResultValue::new(Value::Object(value)); + if let InputObjectMetadata::InputObject { + id, + owner: Owner::Shared { .. }, + .. + } = &object_metadata + { + inner.shared_object_ids.insert(*id); + } + InputValue { + object_metadata: Some(object_metadata), + inner, + } + } + + pub fn new_raw(ty: RawValueType, value: Vec) -> Self { + InputValue { + object_metadata: None, + inner: ResultValue::new(Value::Raw(ty, value)), + } + } + + pub fn new_receiving_object(id: ObjectID, version: SequenceNumber) -> Self { + InputValue { + object_metadata: Some(InputObjectMetadata::Receiving { id, version }), + inner: ResultValue::new(Value::Receiving(id, version, None)), + } + } + + pub fn withdrawal(withdrawal_ty: RawValueType, owner: SuiAddress, limit: U256) -> Self { + let value = Value::Raw( + withdrawal_ty, + bcs::to_bytes(&Withdrawal::new(owner, limit)).unwrap(), + ); + InputValue { + object_metadata: None, + inner: ResultValue { + last_usage_kind: None, + value: Some(value), + shared_object_ids: BTreeSet::new(), + }, + } + } +} + +impl ResultValue { + pub fn new(value: Value) -> Self { + Self { + last_usage_kind: None, + value: Some(value), + shared_object_ids: BTreeSet::new(), + } + } +} + +impl Value { + pub fn is_copyable(&self) -> bool { + match self { + Value::Object(_) => false, + Value::Raw(RawValueType::Any, _) => true, + Value::Raw(RawValueType::Loaded { abilities, .. }, _) => abilities.has_copy(), + Value::Receiving(_, _, _) => false, + } + } + + pub fn write_bcs_bytes( + &self, + buf: &mut Vec, + bound: Option, + ) -> Result<(), ExecutionError> { + match self { + Value::Object(obj_value) => obj_value.write_bcs_bytes(buf, bound)?, + Value::Raw(_, bytes) => buf.extend(bytes), + Value::Receiving(id, version, _) => { + buf.extend(Receiving::new(*id, *version).to_bcs_bytes()) + } + } + if let Some(bound) = bound { + ensure_serialized_size(buf.len() as u64, bound)?; + } + + Ok(()) + } + + pub fn was_used_in_non_entry_move_call(&self) -> bool { + match self { + Value::Object(obj) => obj.used_in_non_entry_move_call, + // Any is only used for Pure inputs, and if it was used by &mut it would have switched + // to Loaded + Value::Raw(RawValueType::Any, _) => false, + Value::Raw( + RawValueType::Loaded { + used_in_non_entry_move_call, + .. + }, + _, + ) => *used_in_non_entry_move_call, + // Only thing you can do with a `Receiving` is consume it, so once it's used it + // can't be used again. + Value::Receiving(_, _, _) => false, + } + } +} + +impl ObjectValue { + /// # Safety + /// We must have the Type is the coin type, but we are unable to check it at this spot + pub unsafe fn coin(type_: Type, coin: Coin) -> Self { + Self { + type_, + has_public_transfer: true, + used_in_non_entry_move_call: false, + contents: ObjectContents::Coin(coin), + } + } + + pub fn ensure_public_transfer_eligible(&self) -> Result<(), ExecutionError> { + if !self.has_public_transfer { + return Err(ExecutionErrorKind::InvalidTransferObject.into()); + } + Ok(()) + } + + pub fn write_bcs_bytes( + &self, + buf: &mut Vec, + bound: Option, + ) -> Result<(), ExecutionError> { + match &self.contents { + ObjectContents::Raw(bytes) => buf.extend(bytes), + ObjectContents::Coin(coin) => buf.extend(coin.to_bcs_bytes()), + } + if let Some(bound) = bound { + ensure_serialized_size(buf.len() as u64, bound)?; + } + Ok(()) + } +} + +pub fn ensure_serialized_size(size: u64, bound: SizeBound) -> Result<(), ExecutionError> { + let bound_size = match bound { + SizeBound::Object(bound_size) + | SizeBound::VectorElem(bound_size) + | SizeBound::Raw(bound_size) => bound_size, + }; + if size > bound_size { + let e = match bound { + SizeBound::Object(_) => ExecutionErrorKind::MoveObjectTooBig { + object_size: size, + max_object_size: bound_size, + }, + SizeBound::VectorElem(_) => ExecutionErrorKind::MoveVectorElemTooBig { + value_size: size, + max_scaled_size: bound_size, + }, + SizeBound::Raw(_) => ExecutionErrorKind::MoveRawValueTooBig { + value_size: size, + max_scaled_size: bound_size, + }, + }; + let msg = "Serialized bytes of value too large".to_owned(); + return Err(ExecutionError::new_with_source(e, msg)); + } + Ok(()) +} + +pub trait TryFromValue: Sized { + fn try_from_value(value: Value) -> Result; +} + +impl TryFromValue for Value { + fn try_from_value(value: Value) -> Result { + Ok(value) + } +} + +impl TryFromValue for ObjectValue { + fn try_from_value(value: Value) -> Result { + match value { + Value::Object(o) => Ok(o), + Value::Raw(RawValueType::Any, _) => Err(CommandArgumentError::TypeMismatch), + Value::Raw(RawValueType::Loaded { .. }, _) => Err(CommandArgumentError::TypeMismatch), + Value::Receiving(_, _, _) => Err(CommandArgumentError::TypeMismatch), + } + } +} + +impl TryFromValue for SuiAddress { + fn try_from_value(value: Value) -> Result { + try_from_value_prim(&value, Type::Address) + } +} + +impl TryFromValue for u64 { + fn try_from_value(value: Value) -> Result { + try_from_value_prim(&value, Type::U64) + } +} + +fn try_from_value_prim<'a, T: Deserialize<'a>>( + value: &'a Value, + expected_ty: Type, +) -> Result { + match value { + Value::Object(_) => Err(CommandArgumentError::TypeMismatch), + Value::Receiving(_, _, _) => Err(CommandArgumentError::TypeMismatch), + Value::Raw(RawValueType::Any, bytes) => { + bcs::from_bytes(bytes).map_err(|_| CommandArgumentError::InvalidBCSBytes) + } + Value::Raw(RawValueType::Loaded { ty, .. }, bytes) => { + if ty != &expected_ty { + return Err(CommandArgumentError::TypeMismatch); + } + bcs::from_bytes(bytes).map_err(|_| CommandArgumentError::InvalidBCSBytes) + } + } +} diff --git a/sui-execution/replay_cut/sui-adapter/src/gas_charger.rs b/sui-execution/replay_cut/sui-adapter/src/gas_charger.rs new file mode 100644 index 0000000000000..508af32f170a8 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/gas_charger.rs @@ -0,0 +1,432 @@ +// Copyright (c) 2021, Facebook, Inc. and its affiliates +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +pub use checked::*; + +#[sui_macros::with_checked_arithmetic] +pub mod checked { + + use crate::sui_types::gas::SuiGasStatusAPI; + use crate::temporary_store::TemporaryStore; + use sui_protocol_config::ProtocolConfig; + use sui_types::deny_list_v2::CONFIG_SETTING_DYNAMIC_FIELD_SIZE_FOR_GAS; + use sui_types::gas::{GasCostSummary, SuiGasStatus, deduct_gas}; + use sui_types::gas_model::gas_predicates::{ + charge_upgrades, dont_charge_budget_on_storage_oog, + }; + use sui_types::{ + accumulator_event::AccumulatorEvent, + base_types::{ObjectID, ObjectRef, SuiAddress}, + digests::TransactionDigest, + error::ExecutionError, + gas_model::tables::GasStatus, + is_system_package, + object::Data, + }; + use tracing::trace; + + /// Tracks all gas operations for a single transaction. + /// This is the main entry point for gas accounting. + /// All the information about gas is stored in this object. + /// The objective here is two-fold: + /// 1- Isolate al version info into a single entry point. This file and the other gas + /// related files are the only one that check for gas version. + /// 2- Isolate all gas accounting into a single implementation. Gas objects are not + /// passed around, and they are retrieved from this instance. + #[derive(Debug)] + pub struct GasCharger { + tx_digest: TransactionDigest, + gas_model_version: u64, + gas_coins: Vec, + // this is the first gas coin in `gas_coins` and the one that all others will + // be smashed into. It can be None for system transactions when `gas_coins` is empty. + smashed_gas_coin: Option, + gas_status: SuiGasStatus, + // For address balance payments: sender or sponsor address to charge + address_balance_gas_payer: Option, + } + + impl GasCharger { + pub fn new( + tx_digest: TransactionDigest, + gas_coins: Vec, + gas_status: SuiGasStatus, + protocol_config: &ProtocolConfig, + address_balance_gas_payer: Option, + ) -> Self { + let gas_model_version = protocol_config.gas_model_version(); + Self { + tx_digest, + gas_model_version, + gas_coins, + smashed_gas_coin: None, + gas_status, + address_balance_gas_payer, + } + } + + pub fn new_unmetered(tx_digest: TransactionDigest) -> Self { + Self { + tx_digest, + gas_model_version: 6, // pick any of the latest, it should not matter + gas_coins: vec![], + smashed_gas_coin: None, + gas_status: SuiGasStatus::new_unmetered(), + address_balance_gas_payer: None, + } + } + + // TODO: there is only one caller to this function that should not exist otherwise. + // Explore way to remove it. + pub(crate) fn gas_coins(&self) -> &[ObjectRef] { + &self.gas_coins + } + + // Return the logical gas coin for this transactions or None if no gas coin was present + // (system transactions). + pub fn gas_coin(&self) -> Option { + self.smashed_gas_coin + } + + pub fn gas_budget(&self) -> u64 { + self.gas_status.gas_budget() + } + + pub fn unmetered_storage_rebate(&self) -> u64 { + self.gas_status.unmetered_storage_rebate() + } + + pub fn no_charges(&self) -> bool { + self.gas_status.gas_used() == 0 + && self.gas_status.storage_rebate() == 0 + && self.gas_status.storage_gas_units() == 0 + } + + pub fn is_unmetered(&self) -> bool { + self.gas_status.is_unmetered() + } + + pub fn move_gas_status(&self) -> &GasStatus { + self.gas_status.move_gas_status() + } + + pub fn move_gas_status_mut(&mut self) -> &mut GasStatus { + self.gas_status.move_gas_status_mut() + } + + pub fn into_gas_status(self) -> SuiGasStatus { + self.gas_status + } + + pub fn summary(&self) -> GasCostSummary { + self.gas_status.summary() + } + + // This function is called when the transaction is about to be executed. + // It will smash all gas coins into a single one and set the logical gas coin + // to be the first one in the list. + // After this call, `gas_coin` will return it id of the gas coin. + // This function panics if errors are found while operation on the gas coins. + // Transaction and certificate input checks must have insured that all gas coins + // are correct. + pub fn smash_gas(&mut self, temporary_store: &mut TemporaryStore<'_>) { + let gas_coin_count = self.gas_coins.len(); + if gas_coin_count == 0 || (gas_coin_count == 1 && self.gas_coins[0].0 == ObjectID::ZERO) + { + return; // self.smashed_gas_coin is None + } + // set the first coin to be the transaction only gas coin. + // All others will be smashed into this one. + let gas_coin_id = self.gas_coins[0].0; + self.smashed_gas_coin = Some(gas_coin_id); + if gas_coin_count == 1 { + return; + } + + // sum the value of all gas coins + let new_balance = self + .gas_coins + .iter() + .map(|obj_ref| { + let obj = temporary_store.objects().get(&obj_ref.0).unwrap(); + let Data::Move(move_obj) = &obj.data else { + return Err(ExecutionError::invariant_violation( + "Provided non-gas coin object as input for gas!", + )); + }; + if !move_obj.type_().is_gas_coin() { + return Err(ExecutionError::invariant_violation( + "Provided non-gas coin object as input for gas!", + )); + } + Ok(move_obj.get_coin_value_unsafe()) + }) + .collect::, ExecutionError>>() + // transaction and certificate input checks must have insured that all gas coins + // are valid + .unwrap_or_else(|_| { + panic!( + "Invariant violation: non-gas coin object as input for gas in txn {}", + self.tx_digest + ) + }) + .iter() + .sum(); + let mut primary_gas_object = temporary_store + .objects() + .get(&gas_coin_id) + // unwrap should be safe because we checked that this exists in `self.objects()` above + .unwrap_or_else(|| { + panic!( + "Invariant violation: gas coin not found in store in txn {}", + self.tx_digest + ) + }) + .clone(); + // delete all gas objects except the primary_gas_object + for (id, _version, _digest) in &self.gas_coins[1..] { + debug_assert_ne!(*id, primary_gas_object.id()); + temporary_store.delete_input_object(id); + } + primary_gas_object + .data + .try_as_move_mut() + // unwrap should be safe because we checked that the primary gas object was a coin object above. + .unwrap_or_else(|| { + panic!( + "Invariant violation: invalid coin object in txn {}", + self.tx_digest + ) + }) + .set_coin_value_unsafe(new_balance); + temporary_store.mutate_input_object(primary_gas_object); + } + + // + // Gas charging operations + // + + pub fn track_storage_mutation( + &mut self, + object_id: ObjectID, + new_size: usize, + storage_rebate: u64, + ) -> u64 { + self.gas_status + .track_storage_mutation(object_id, new_size, storage_rebate) + } + + pub fn reset_storage_cost_and_rebate(&mut self) { + self.gas_status.reset_storage_cost_and_rebate(); + } + + pub fn charge_publish_package(&mut self, size: usize) -> Result<(), ExecutionError> { + self.gas_status.charge_publish_package(size) + } + + pub fn charge_upgrade_package(&mut self, size: usize) -> Result<(), ExecutionError> { + if charge_upgrades(self.gas_model_version) { + self.gas_status.charge_publish_package(size) + } else { + Ok(()) + } + } + + pub fn charge_input_objects( + &mut self, + temporary_store: &TemporaryStore<'_>, + ) -> Result<(), ExecutionError> { + let objects = temporary_store.objects(); + // TODO: Charge input object count. + let _object_count = objects.len(); + // Charge bytes read + let total_size = temporary_store + .objects() + .iter() + // don't charge for loading Sui Framework or Move stdlib + .filter(|(id, _)| !is_system_package(**id)) + .map(|(_, obj)| obj.object_size_for_gas_metering()) + .sum(); + self.gas_status.charge_storage_read(total_size) + } + + pub fn charge_coin_transfers( + &mut self, + protocol_config: &ProtocolConfig, + num_non_gas_coin_owners: u64, + ) -> Result<(), ExecutionError> { + // times two for the global pause and per-address settings + // this "overcharges" slightly since it does not check the global pause for each owner + // but rather each coin type. + let bytes_read_per_owner = CONFIG_SETTING_DYNAMIC_FIELD_SIZE_FOR_GAS; + // associate the cost with dynamic field access so that it will increase if/when this + // cost increases + let cost_per_byte = + protocol_config.dynamic_field_borrow_child_object_type_cost_per_byte() as usize; + let cost_per_owner = bytes_read_per_owner * cost_per_byte; + let owner_cost = cost_per_owner * (num_non_gas_coin_owners as usize); + self.gas_status.charge_storage_read(owner_cost) + } + + /// Resets any mutations, deletions, and events recorded in the store, as well as any storage costs and + /// rebates, then Re-runs gas smashing. Effects on store are now as if we were about to begin execution + pub fn reset(&mut self, temporary_store: &mut TemporaryStore<'_>) { + temporary_store.drop_writes(); + self.gas_status.reset_storage_cost_and_rebate(); + self.smash_gas(temporary_store); + } + + /// Entry point for gas charging. + /// 1. Compute tx storage gas costs and tx storage rebates, update storage_rebate field of + /// mutated objects + /// 2. Deduct computation gas costs and storage costs, credit storage rebates. + /// The happy path of this function follows (1) + (2) and is fairly simple. + /// Most of the complexity is in the unhappy paths: + /// - if execution aborted before calling this function, we have to dump all writes + + /// re-smash gas, then charge for storage + /// - if we run out of gas while charging for storage, we have to dump all writes + + /// re-smash gas, then charge for storage again + pub fn charge_gas( + &mut self, + temporary_store: &mut TemporaryStore<'_>, + execution_result: &mut Result, + ) -> GasCostSummary { + // at this point, we have done *all* charging for computation, + // but have not yet set the storage rebate or storage gas units + debug_assert!(self.gas_status.storage_rebate() == 0); + debug_assert!(self.gas_status.storage_gas_units() == 0); + + if self.smashed_gas_coin.is_some() || self.address_balance_gas_payer.is_some() { + // bucketize computation cost + let is_move_abort = execution_result + .as_ref() + .err() + .map(|err| { + matches!( + err.kind(), + sui_types::execution_status::ExecutionFailureStatus::MoveAbort(_, _) + ) + }) + .unwrap_or(false); + // bucketize computation cost + if let Err(err) = self.gas_status.bucketize_computation(Some(is_move_abort)) + && execution_result.is_ok() + { + *execution_result = Err(err); + } + + // On error we need to dump writes, deletes, etc before charging storage gas + if execution_result.is_err() { + self.reset(temporary_store); + } + } + + // compute and collect storage charges + temporary_store.ensure_active_inputs_mutated(); + temporary_store.collect_storage_and_rebate(self); + + if self.smashed_gas_coin.is_some() { + #[skip_checked_arithmetic] + trace!(target: "replay_gas_info", "Gas smashing has occurred for this transaction"); + } + + // system transactions (None smashed_gas_coin) do not have gas and so do not charge + // for storage, however they track storage values to check for conservation rules + if let Some(payer_address) = self.address_balance_gas_payer { + let is_insufficient_balance_error = execution_result + .as_ref() + .err() + .map(|err| matches!(err.kind(), sui_types::execution_status::ExecutionFailureStatus::InsufficientBalanceForWithdraw)) + .unwrap_or(false); + + // If we don't have enough balance to withdraw, don't charge for gas + // TODO: consider charging gas if we have enough to reserve but not enough to cover all withdraws + if is_insufficient_balance_error { + GasCostSummary::default() + } else { + let cost_summary = self.gas_status.summary(); + let net_change = cost_summary.net_gas_usage(); + + if net_change != 0 { + let balance_type = sui_types::balance::Balance::type_tag( + sui_types::gas_coin::GAS::type_tag(), + ); + let accumulator_event = AccumulatorEvent::from_balance_change( + payer_address, + balance_type, + net_change, + ) + .expect("Failed to create accumulator event for gas balance"); + + temporary_store.add_accumulator_event(accumulator_event); + } + + cost_summary + } + } else if let Some(gas_object_id) = self.smashed_gas_coin { + if dont_charge_budget_on_storage_oog(self.gas_model_version) { + self.handle_storage_and_rebate_v2(temporary_store, execution_result) + } else { + self.handle_storage_and_rebate_v1(temporary_store, execution_result) + } + + let cost_summary = self.gas_status.summary(); + let gas_used = cost_summary.net_gas_usage(); + + let mut gas_object = temporary_store.read_object(&gas_object_id).unwrap().clone(); + deduct_gas(&mut gas_object, gas_used); + #[skip_checked_arithmetic] + trace!(gas_used, gas_obj_id =? gas_object.id(), gas_obj_ver =? gas_object.version(), "Updated gas object"); + + temporary_store.mutate_input_object(gas_object); + cost_summary + } else { + GasCostSummary::default() + } + } + + fn handle_storage_and_rebate_v1( + &mut self, + temporary_store: &mut TemporaryStore<'_>, + execution_result: &mut Result, + ) { + if let Err(err) = self.gas_status.charge_storage_and_rebate() { + self.reset(temporary_store); + self.gas_status.adjust_computation_on_out_of_gas(); + temporary_store.ensure_active_inputs_mutated(); + temporary_store.collect_rebate(self); + if execution_result.is_ok() { + *execution_result = Err(err); + } + } + } + + fn handle_storage_and_rebate_v2( + &mut self, + temporary_store: &mut TemporaryStore<'_>, + execution_result: &mut Result, + ) { + if let Err(err) = self.gas_status.charge_storage_and_rebate() { + // we run out of gas charging storage, reset and try charging for storage again. + // Input objects are touched and so they have a storage cost + self.reset(temporary_store); + temporary_store.ensure_active_inputs_mutated(); + temporary_store.collect_storage_and_rebate(self); + if let Err(err) = self.gas_status.charge_storage_and_rebate() { + // we run out of gas attempting to charge for the input objects exclusively, + // deal with this edge case by not charging for storage + self.reset(temporary_store); + self.gas_status.adjust_computation_on_out_of_gas(); + temporary_store.ensure_active_inputs_mutated(); + temporary_store.collect_rebate(self); + if execution_result.is_ok() { + *execution_result = Err(err); + } + } else if execution_result.is_ok() { + *execution_result = Err(err); + } + } + } + } +} diff --git a/sui-execution/replay_cut/sui-adapter/src/gas_meter.rs b/sui-execution/replay_cut/sui-adapter/src/gas_meter.rs new file mode 100644 index 0000000000000..289cb99ba3d79 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/gas_meter.rs @@ -0,0 +1,423 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use move_binary_format::errors::PartialVMResult; +use move_core_types::{ + gas_algebra::{AbstractMemorySize, InternalGas, NumArgs, NumBytes}, + language_storage::ModuleId, +}; +use move_vm_types::{ + gas::{GasMeter, SimpleInstruction}, + loaded_data::runtime_types::Type, + views::{SizeConfig, TypeView, ValueView}, +}; +use sui_types::gas_model::{ + gas_predicates::{native_function_threshold_exceeded, use_legacy_abstract_size}, + tables::{GasStatus, REFERENCE_SIZE, STRUCT_SIZE, VEC_SIZE}, +}; + +pub struct SuiGasMeter<'g>(pub &'g mut GasStatus); + +/// Returns a tuple of (, , , ) +fn get_simple_instruction_stack_change( + instr: SimpleInstruction, +) -> (u64, u64, AbstractMemorySize, AbstractMemorySize) { + use SimpleInstruction::*; + + match instr { + // NB: The `Ret` pops are accounted for in `Call` instructions, so we say `Ret` has no pops. + Nop | Ret => (0, 0, 0.into(), 0.into()), + BrTrue | BrFalse => (1, 0, Type::Bool.size(), 0.into()), + Branch => (0, 0, 0.into(), 0.into()), + LdU8 => (0, 1, 0.into(), Type::U8.size()), + LdU16 => (0, 1, 0.into(), Type::U16.size()), + LdU32 => (0, 1, 0.into(), Type::U32.size()), + LdU64 => (0, 1, 0.into(), Type::U64.size()), + LdU128 => (0, 1, 0.into(), Type::U128.size()), + LdU256 => (0, 1, 0.into(), Type::U256.size()), + LdTrue | LdFalse => (0, 1, 0.into(), Type::Bool.size()), + FreezeRef => (1, 1, REFERENCE_SIZE, REFERENCE_SIZE), + ImmBorrowLoc | MutBorrowLoc => (0, 1, 0.into(), REFERENCE_SIZE), + ImmBorrowField | MutBorrowField | ImmBorrowFieldGeneric | MutBorrowFieldGeneric => { + (1, 1, REFERENCE_SIZE, REFERENCE_SIZE) + } + // Since we don't have the size of the value being cast here we take a conservative + // over-approximation: it is _always_ getting cast from the smallest integer type. + CastU8 => (1, 1, Type::U8.size(), Type::U8.size()), + CastU16 => (1, 1, Type::U8.size(), Type::U16.size()), + CastU32 => (1, 1, Type::U8.size(), Type::U32.size()), + CastU64 => (1, 1, Type::U8.size(), Type::U64.size()), + CastU128 => (1, 1, Type::U8.size(), Type::U128.size()), + CastU256 => (1, 1, Type::U8.size(), Type::U256.size()), + // NB: We don't know the size of what integers we're dealing with, so we conservatively + // over-approximate by popping the smallest integers, and push the largest. + Add | Sub | Mul | Mod | Div => (2, 1, Type::U8.size() + Type::U8.size(), Type::U256.size()), + BitOr | BitAnd | Xor => (2, 1, Type::U8.size() + Type::U8.size(), Type::U256.size()), + Shl | Shr => (2, 1, Type::U8.size() + Type::U8.size(), Type::U256.size()), + Or | And => ( + 2, + 1, + Type::Bool.size() + Type::Bool.size(), + Type::Bool.size(), + ), + Lt | Gt | Le | Ge => (2, 1, Type::U8.size() + Type::U8.size(), Type::Bool.size()), + Not => (1, 1, Type::Bool.size(), Type::Bool.size()), + Abort => (1, 0, Type::U64.size(), 0.into()), + } +} + +impl GasMeter for SuiGasMeter<'_> { + /// Charge an instruction and fail if not enough gas units are left. + fn charge_simple_instr(&mut self, instr: SimpleInstruction) -> PartialVMResult<()> { + let (pops, pushes, pop_size, push_size) = get_simple_instruction_stack_change(instr); + self.0 + .charge(1, pushes, pops, push_size.into(), pop_size.into()) + } + + fn charge_pop(&mut self, popped_val: impl ValueView) -> PartialVMResult<()> { + self.0 + .charge(1, 0, 1, 0, abstract_memory_size(self.0, popped_val).into()) + } + + fn charge_native_function( + &mut self, + amount: InternalGas, + ret_vals: Option>, + ) -> PartialVMResult<()> { + // Charge for the number of pushes on to the stack that the return of this function is + // going to cause. + let pushes = ret_vals + .as_ref() + .map(|ret_vals| ret_vals.len()) + .unwrap_or(0) as u64; + // Calculate the number of bytes that are getting pushed onto the stack. + let size_increase = ret_vals + .map(|ret_vals| { + ret_vals.fold(AbstractMemorySize::zero(), |acc, elem| { + acc + abstract_memory_size(self.0, elem) + }) + }) + .unwrap_or_else(AbstractMemorySize::zero); + self.0.record_native_call(); + if native_function_threshold_exceeded(self.0.gas_model_version, self.0.num_native_calls) { + // Charge for the stack operations. We don't count this as an "instruction" since we + // already accounted for the `Call` instruction in the + // `charge_native_function_before_execution` call. + // The amount returned by the native function is viewed as the "virtual" instruction cost + // for the native function, and will be charged and contribute to the overall cost tier of + // the transaction accordingly. + self.0 + .charge(amount.into(), pushes, 0, size_increase.into(), 0) + } else { + // Charge for the stack operations. We don't count this as an "instruction" since we + // already accounted for the `Call` instruction in the + // `charge_native_function_before_execution` call. + self.0.charge(0, pushes, 0, size_increase.into(), 0)?; + // Now charge the gas that the native function told us to charge. + self.0.deduct_gas(amount) + } + } + + fn charge_native_function_before_execution( + &mut self, + _ty_args: impl ExactSizeIterator, + args: impl ExactSizeIterator, + ) -> PartialVMResult<()> { + // Determine the number of pops that are going to be needed for this function call, and + // charge for them. + let pops = args.len() as u64; + // Calculate the size decrease of the stack from the above pops. + let stack_reduction_size = args.fold(AbstractMemorySize::new(pops), |acc, elem| { + acc + abstract_memory_size(self.0, elem) + }); + // Track that this is going to be popping from the operand stack. We also increment the + // instruction count as we need to account for the `Call` bytecode that initiated this + // native call. + self.0.charge(1, 0, pops, 0, stack_reduction_size.into()) + } + + fn charge_call( + &mut self, + _module_id: &ModuleId, + _func_name: &str, + args: impl ExactSizeIterator, + _num_locals: NumArgs, + ) -> PartialVMResult<()> { + // We will have to perform this many pops for the call. + let pops = args.len() as u64; + // Size stays the same -- we're just moving it from the operand stack to the locals. But + // the size on the operand stack is reduced by sum_{args} arg.size(). + let stack_reduction_size = args.fold(AbstractMemorySize::new(0), |acc, elem| { + acc + abstract_memory_size(self.0, elem) + }); + self.0.charge(1, 0, pops, 0, stack_reduction_size.into()) + } + + fn charge_call_generic( + &mut self, + _module_id: &ModuleId, + _func_name: &str, + _ty_args: impl ExactSizeIterator, + args: impl ExactSizeIterator, + _num_locals: NumArgs, + ) -> PartialVMResult<()> { + // We have to perform this many pops from the operand stack for this function call. + let pops = args.len() as u64; + // Calculate the size reduction on the operand stack. + let stack_reduction_size = args.fold(AbstractMemorySize::new(0), |acc, elem| { + acc + abstract_memory_size(self.0, elem) + }); + // Charge for the pops, no pushes, and account for the stack size decrease. Also track the + // `CallGeneric` instruction we must have encountered for this. + self.0.charge(1, 0, pops, 0, stack_reduction_size.into()) + } + + fn charge_ld_const(&mut self, size: NumBytes) -> PartialVMResult<()> { + // Charge for the load from the locals onto the stack. + self.0.charge(1, 1, 0, u64::from(size), 0) + } + + fn charge_ld_const_after_deserialization( + &mut self, + _val: impl ValueView, + ) -> PartialVMResult<()> { + // We already charged for this based on the bytes that we're loading so don't charge again. + Ok(()) + } + + fn charge_copy_loc(&mut self, val: impl ValueView) -> PartialVMResult<()> { + // Charge for the copy of the local onto the stack. + self.0 + .charge(1, 1, 0, abstract_memory_size(self.0, val).into(), 0) + } + + fn charge_move_loc(&mut self, val: impl ValueView) -> PartialVMResult<()> { + if reweight_move_loc(self.0.gas_model_version) { + self.0.charge(1, 1, 0, REFERENCE_SIZE.into(), 0) + } else { + // Charge for the move of the local on to the stack. Note that we charge here since we + // aren't tracking the local size (at least not yet). If we were, this should be a net-zero + // operation in terms of memory usage. + self.0 + .charge(1, 1, 0, abstract_memory_size(self.0, val).into(), 0) + } + } + + fn charge_store_loc(&mut self, val: impl ValueView) -> PartialVMResult<()> { + // Charge for the storing of the value on the stack into a local. Note here that if we were + // also accounting for the size of the locals that this would be a net-zero operation in + // terms of memory. + self.0 + .charge(1, 0, 1, 0, abstract_memory_size(self.0, val).into()) + } + + fn charge_pack( + &mut self, + _is_generic: bool, + args: impl ExactSizeIterator, + ) -> PartialVMResult<()> { + // We perform `num_fields` number of pops. + let num_fields = args.len() as u64; + // The actual amount of memory on the stack is staying the same with the addition of some + // extra size for the struct, so the size doesn't really change much. + self.0.charge(1, 1, num_fields, STRUCT_SIZE.into(), 0) + } + + fn charge_unpack( + &mut self, + _is_generic: bool, + args: impl ExactSizeIterator, + ) -> PartialVMResult<()> { + // We perform `num_fields` number of pushes. + let num_fields = args.len() as u64; + self.0.charge(1, num_fields, 1, 0, STRUCT_SIZE.into()) + } + + fn charge_variant_switch(&mut self, val: impl ValueView) -> PartialVMResult<()> { + // We perform a single pop of a value from the stack. + self.0 + .charge(1, 0, 1, 0, abstract_memory_size(self.0, val).into()) + } + + fn charge_read_ref(&mut self, ref_val: impl ValueView) -> PartialVMResult<()> { + // We read the reference so we are decreasing the size of the stack by the size of the + // reference, and adding to it the size of the value that has been read from that + // reference. + let size = if reweight_read_ref(self.0.gas_model_version) { + abstract_memory_size_with_traversal(self.0, ref_val) + } else { + abstract_memory_size(self.0, ref_val) + }; + self.0.charge(1, 1, 1, size.into(), REFERENCE_SIZE.into()) + } + + fn charge_write_ref( + &mut self, + new_val: impl ValueView, + old_val: impl ValueView, + ) -> PartialVMResult<()> { + // TODO(tzakian): We should account for this elsewhere as the owner of data the + // reference points to won't be on the stack. For now though, we treat it as adding to the + // stack size. + self.0.charge( + 1, + 1, + 2, + abstract_memory_size(self.0, new_val).into(), + abstract_memory_size(self.0, old_val).into(), + ) + } + + fn charge_eq(&mut self, lhs: impl ValueView, rhs: impl ValueView) -> PartialVMResult<()> { + let size_reduction = abstract_memory_size_with_traversal(self.0, lhs) + + abstract_memory_size_with_traversal(self.0, rhs); + self.0.charge( + 1, + 1, + 2, + (Type::Bool.size() + size_reduction).into(), + size_reduction.into(), + ) + } + + fn charge_neq(&mut self, lhs: impl ValueView, rhs: impl ValueView) -> PartialVMResult<()> { + let size_reduction = abstract_memory_size_with_traversal(self.0, lhs) + + abstract_memory_size_with_traversal(self.0, rhs); + let size_increase = if enable_traverse_refs(self.0.gas_model_version) { + Type::Bool.size() + size_reduction + } else { + Type::Bool.size() + }; + self.0 + .charge(1, 1, 2, size_increase.into(), size_reduction.into()) + } + + fn charge_vec_pack<'a>( + &mut self, + _ty: impl TypeView + 'a, + args: impl ExactSizeIterator, + ) -> PartialVMResult<()> { + // We will perform `num_args` number of pops. + let num_args = args.len() as u64; + // The amount of data on the stack stays constant except we have some extra metadata for + // the vector to hold the length of the vector. + self.0.charge(1, 1, num_args, VEC_SIZE.into(), 0) + } + + fn charge_vec_len(&mut self, _ty: impl TypeView) -> PartialVMResult<()> { + self.0 + .charge(1, 1, 1, Type::U64.size().into(), REFERENCE_SIZE.into()) + } + + fn charge_vec_borrow( + &mut self, + _is_mut: bool, + _ty: impl TypeView, + _is_success: bool, + ) -> PartialVMResult<()> { + self.0.charge( + 1, + 1, + 2, + REFERENCE_SIZE.into(), + (REFERENCE_SIZE + Type::U64.size()).into(), + ) + } + + fn charge_vec_push_back( + &mut self, + _ty: impl TypeView, + _val: impl ValueView, + ) -> PartialVMResult<()> { + // The value was already on the stack, so we aren't increasing the number of bytes on the stack. + self.0.charge(1, 0, 2, 0, REFERENCE_SIZE.into()) + } + + fn charge_vec_pop_back( + &mut self, + _ty: impl TypeView, + _val: Option, + ) -> PartialVMResult<()> { + self.0.charge(1, 1, 1, 0, REFERENCE_SIZE.into()) + } + + fn charge_vec_unpack( + &mut self, + _ty: impl TypeView, + expect_num_elements: NumArgs, + _elems: impl ExactSizeIterator, + ) -> PartialVMResult<()> { + // Charge for the pushes + let pushes = u64::from(expect_num_elements); + // The stack size stays pretty much the same modulo the additional vector size + self.0.charge(1, pushes, 1, 0, VEC_SIZE.into()) + } + + fn charge_vec_swap(&mut self, _ty: impl TypeView) -> PartialVMResult<()> { + let size_decrease = REFERENCE_SIZE + Type::U64.size() + Type::U64.size(); + self.0.charge(1, 1, 1, 0, size_decrease.into()) + } + + fn charge_drop_frame( + &mut self, + _locals: impl Iterator, + ) -> PartialVMResult<()> { + Ok(()) + } + + fn remaining_gas(&self) -> InternalGas { + if !self.0.charge { + return InternalGas::new(u64::MAX); + } + self.0.gas_left + } +} + +fn abstract_memory_size(status: &GasStatus, val: impl ValueView) -> AbstractMemorySize { + let config = size_config_for_gas_model_version(status.gas_model_version, false); + val.abstract_memory_size(&config) +} + +fn abstract_memory_size_with_traversal( + status: &GasStatus, + val: impl ValueView, +) -> AbstractMemorySize { + let config = size_config_for_gas_model_version(status.gas_model_version, true); + val.abstract_memory_size(&config) +} + +fn enable_traverse_refs(gas_model_version: u64) -> bool { + gas_model_version > 9 +} + +fn reweight_read_ref(gas_model_version: u64) -> bool { + // Reweighting `ReadRef` is only done in gas model versions 10 and above. + gas_model_version > 10 +} + +fn reweight_move_loc(gas_model_version: u64) -> bool { + // Reweighting `MoveLoc` is only done in gas model versions 10 and above. + gas_model_version > 10 +} + +fn size_config_for_gas_model_version( + gas_model_version: u64, + should_traverse_refs: bool, +) -> SizeConfig { + if use_legacy_abstract_size(gas_model_version) { + SizeConfig { + traverse_references: false, + include_vector_size: false, + } + } else if should_traverse_refs { + SizeConfig { + traverse_references: enable_traverse_refs(gas_model_version), + include_vector_size: true, + } + } else { + SizeConfig { + traverse_references: false, + include_vector_size: true, + } + } +} diff --git a/sui-execution/replay_cut/sui-adapter/src/lib.rs b/sui-execution/replay_cut/sui-adapter/src/lib.rs new file mode 100644 index 0000000000000..ed4e0bdcb4816 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/lib.rs @@ -0,0 +1,19 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[macro_use] +extern crate sui_types; + +pub mod adapter; +pub mod data_store; +pub mod error; +pub mod execution_engine; +pub mod execution_mode; +pub mod execution_value; +pub mod gas_charger; +pub mod gas_meter; +pub mod programmable_transactions; +pub mod static_programmable_transactions; +pub mod temporary_store; +pub mod type_layout_resolver; +pub mod type_resolver; diff --git a/sui-execution/replay_cut/sui-adapter/src/programmable_transactions/context.rs b/sui-execution/replay_cut/sui-adapter/src/programmable_transactions/context.rs new file mode 100644 index 0000000000000..63f6a756c1462 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/programmable_transactions/context.rs @@ -0,0 +1,2181 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +pub use checked::*; + +#[sui_macros::with_checked_arithmetic] +#[allow(clippy::type_complexity)] +mod checked { + use crate::{ + adapter::new_native_extensions, + data_store::{ + PackageStore, + cached_package_store::CachedPackageStore, + legacy::{linkage_view::LinkageView, sui_data_store::SuiDataStore}, + }, + execution_mode::ExecutionMode, + execution_value::{ + CommandKind, ExecutionState, InputObjectMetadata, InputValue, Mutability, + ObjectContents, ObjectValue, RawValueType, ResultValue, SizeBound, TryFromValue, + UsageKind, Value, + }, + gas_charger::GasCharger, + gas_meter::SuiGasMeter, + type_resolver::TypeTagResolver, + }; + use indexmap::IndexSet; + use move_binary_format::{ + CompiledModule, + errors::{Location, PartialVMError, VMError, VMResult}, + file_format::{AbilitySet, CodeOffset, FunctionDefinitionIndex, TypeParameterIndex}, + }; + use move_core_types::{ + account_address::AccountAddress, + identifier::IdentStr, + language_storage::{ModuleId, StructTag, TypeTag}, + u256::U256, + vm_status::StatusCode, + }; + use move_trace_format::format::MoveTraceBuilder; + use move_vm_runtime::{ + move_vm::MoveVM, + native_extensions::NativeContextExtensions, + session::{LoadedFunctionInstantiation, SerializedReturnValues}, + }; + use move_vm_types::{data_store::MoveResolver, loaded_data::runtime_types::Type}; + use mysten_common::debug_fatal; + use nonempty::nonempty; + use std::{ + borrow::Borrow, + cell::RefCell, + collections::{BTreeMap, BTreeSet, HashMap}, + rc::Rc, + sync::Arc, + }; + use sui_move_natives::object_runtime::{ + self, LoadedRuntimeObject, MoveAccumulatorEvent, MoveAccumulatorValue, ObjectRuntime, + RuntimeResults, get_all_uids, max_event_error, + }; + use sui_protocol_config::ProtocolConfig; + use sui_types::{ + accumulator_event::AccumulatorEvent, + accumulator_root::AccumulatorObjId, + balance::Balance, + base_types::{MoveObjectType, ObjectID, SuiAddress, TxContext}, + coin::Coin, + effects::{AccumulatorAddress, AccumulatorValue, AccumulatorWriteV1}, + error::{ExecutionError, ExecutionErrorKind, SuiError, command_argument_error}, + event::Event, + execution::{ExecutionResults, ExecutionResultsV2}, + execution_status::CommandArgumentError, + funds_accumulator::Withdrawal, + metrics::LimitsMetrics, + move_package::MovePackage, + object::{Data, MoveObject, Object, ObjectInner, Owner}, + storage::DenyListResult, + transaction::{ + Argument, CallArg, FundsWithdrawalArg, ObjectArg, SharedObjectMutability, WithdrawFrom, + }, + }; + use tracing::instrument; + + /// Maintains all runtime state specific to programmable transactions + pub struct ExecutionContext<'vm, 'state, 'a> { + /// The protocol config + pub protocol_config: &'a ProtocolConfig, + /// Metrics for reporting exceeded limits + pub metrics: Arc, + /// The MoveVM + pub vm: &'vm MoveVM, + /// The LinkageView for this session + pub linkage_view: LinkageView<'state>, + pub native_extensions: NativeContextExtensions<'state>, + /// The global state, used for resolving packages + pub state_view: &'state dyn ExecutionState, + /// A shared transaction context, contains transaction digest information and manages the + /// creation of new object IDs + pub tx_context: Rc>, + /// The gas charger used for metering + pub gas_charger: &'a mut GasCharger, + /// Additional transfers not from the Move runtime + additional_transfers: Vec<(/* new owner */ SuiAddress, ObjectValue)>, + /// Newly published packages + new_packages: Vec, + /// User events are claimed after each Move call + user_events: Vec<(ModuleId, StructTag, Vec)>, + // runtime data + /// The runtime value for the Gas coin, None if it has been taken/moved + gas: InputValue, + /// The runtime value for the inputs/call args, None if it has been taken/moved + inputs: Vec, + /// The results of a given command. For most commands, the inner vector will have length 1. + /// It will only not be 1 for Move calls with multiple return values. + /// Inner values are None if taken/moved by-value + results: Vec>, + /// Map of arguments that are currently borrowed in this command, true if the borrow is mutable + /// This gets cleared out when new results are pushed, i.e. the end of a command + borrowed: HashMap, + /// Set of the by-value shared objects taken in the current command + per_command_by_value_shared_objects: BTreeSet, + } + + /// A write for an object that was generated outside of the Move ObjectRuntime + struct AdditionalWrite { + /// The new owner of the object + recipient: Owner, + /// the type of the object, + type_: Type, + /// if the object has public transfer or not, i.e. if it has store + has_public_transfer: bool, + /// contents of the object + bytes: Vec, + } + + #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] + pub struct Arg(Arg_); + + #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] + enum Arg_ { + V1(Argument), + V2(NormalizedArg), + } + + #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] + enum NormalizedArg { + GasCoin, + Input(u16), + Result(u16, u16), + } + + impl<'vm, 'state, 'a> ExecutionContext<'vm, 'state, 'a> { + #[instrument(name = "ExecutionContext::new", level = "trace", skip_all)] + pub fn new( + protocol_config: &'a ProtocolConfig, + metrics: Arc, + vm: &'vm MoveVM, + state_view: &'state dyn ExecutionState, + tx_context: Rc>, + gas_charger: &'a mut GasCharger, + inputs: Vec, + ) -> Result + where + 'a: 'state, + { + let mut linkage_view = LinkageView::new(Box::new(CachedPackageStore::new(Box::new( + state_view.as_sui_resolver(), + )))); + let mut input_object_map = BTreeMap::new(); + let tx_context_ref = RefCell::borrow(&tx_context); + let inputs = inputs + .into_iter() + .map(|call_arg| { + load_call_arg( + protocol_config, + vm, + state_view, + &mut linkage_view, + &[], + &mut input_object_map, + &tx_context_ref, + call_arg, + ) + }) + .collect::>()?; + std::mem::drop(tx_context_ref); + let gas = if let Some(gas_coin) = gas_charger.gas_coin() { + let mut gas = load_object( + protocol_config, + vm, + state_view, + &mut linkage_view, + &[], + &mut input_object_map, + /* mutability override */ None, + gas_coin, + )?; + // subtract the max gas budget. This amount is off limits in the programmable transaction, + // so to mimic this "off limits" behavior, we act as if the coin has less balance than + // it really does + let Some(Value::Object(ObjectValue { + contents: ObjectContents::Coin(coin), + .. + })) = &mut gas.inner.value + else { + invariant_violation!("Gas object should be a populated coin") + }; + + let max_gas_in_balance = gas_charger.gas_budget(); + let Some(new_balance) = coin.balance.value().checked_sub(max_gas_in_balance) else { + invariant_violation!( + "Transaction input checker should check that there is enough gas" + ); + }; + coin.balance = Balance::new(new_balance); + gas + } else { + InputValue { + object_metadata: None, + inner: ResultValue { + last_usage_kind: None, + value: None, + shared_object_ids: BTreeSet::new(), + }, + } + }; + let native_extensions = new_native_extensions( + state_view.as_child_resolver(), + input_object_map, + !gas_charger.is_unmetered(), + protocol_config, + metrics.clone(), + tx_context.clone(), + ); + + Ok(Self { + protocol_config, + metrics, + vm, + linkage_view, + native_extensions, + state_view, + tx_context, + gas_charger, + gas, + inputs, + results: vec![], + additional_transfers: vec![], + new_packages: vec![], + user_events: vec![], + borrowed: HashMap::new(), + per_command_by_value_shared_objects: BTreeSet::new(), + }) + } + + pub fn object_runtime(&self) -> Result<&ObjectRuntime<'_>, ExecutionError> { + self.native_extensions + .get::() + .map_err(|e| self.convert_vm_error(e.finish(Location::Undefined))) + } + + /// Create a new ID and update the state + pub fn fresh_id(&mut self) -> Result { + let object_id = self.tx_context.borrow_mut().fresh_id(); + self.native_extensions + .get_mut() + .and_then(|object_runtime: &mut ObjectRuntime| object_runtime.new_id(object_id)) + .map_err(|e| self.convert_vm_error(e.finish(Location::Undefined)))?; + Ok(object_id) + } + + /// Delete an ID and update the state + pub fn delete_id(&mut self, object_id: ObjectID) -> Result<(), ExecutionError> { + self.native_extensions + .get_mut() + .and_then(|object_runtime: &mut ObjectRuntime| object_runtime.delete_id(object_id)) + .map_err(|e| self.convert_vm_error(e.finish(Location::Undefined))) + } + + /// Set the link context for the session from the linkage information in the MovePackage found + /// at `package_id`. Returns the runtime ID of the link context package on success. + pub fn set_link_context( + &mut self, + package_id: ObjectID, + ) -> Result { + if self.linkage_view.has_linkage(package_id)? { + // Setting same context again, can skip. + return Ok(self + .linkage_view + .original_package_id()? + .unwrap_or(*package_id)); + } + + let move_package = get_package(&self.linkage_view, package_id) + .map_err(|e| self.convert_vm_error(e))?; + + self.linkage_view.set_linkage(&move_package) + } + + /// Load a type using the context's current session. + pub fn load_type(&mut self, type_tag: &TypeTag) -> VMResult { + load_type(self.vm, &self.linkage_view, &self.new_packages, type_tag) + } + + /// Load a type using the context's current session. + pub fn load_type_from_struct(&mut self, struct_tag: &StructTag) -> VMResult { + load_type_from_struct(self.vm, &self.linkage_view, &self.new_packages, struct_tag) + } + + pub fn get_type_abilities(&self, t: &Type) -> Result { + self.vm + .get_runtime() + .get_type_abilities(t) + .map_err(|e| self.convert_vm_error(e)) + } + + /// Takes the user events from the runtime and tags them with the Move module of the function + /// that was invoked for the command + pub fn take_user_events( + &mut self, + module_id: &ModuleId, + function: FunctionDefinitionIndex, + last_offset: CodeOffset, + ) -> Result<(), ExecutionError> { + let events = self + .native_extensions + .get_mut() + .map(|object_runtime: &mut ObjectRuntime| object_runtime.take_user_events()) + .map_err(|e| self.convert_vm_error(e.finish(Location::Undefined)))?; + let num_events = self.user_events.len() + events.len(); + let max_events = self.protocol_config.max_num_event_emit(); + if num_events as u64 > max_events { + let err = max_event_error(max_events) + .at_code_offset(function, last_offset) + .finish(Location::Module(module_id.clone())); + return Err(self.convert_vm_error(err)); + } + let new_events = events + .into_iter() + .map(|(tag, value)| { + let type_tag = TypeTag::Struct(Box::new(tag.clone())); + let ty = unwrap_type_tag_load( + self.protocol_config, + self.vm + .get_runtime() + .try_load_cached_type(&type_tag) + .map_err(|e| self.convert_vm_error(e))? + .ok_or_else(|| { + make_invariant_violation!( + "Failed to load type for event tag: {}", + tag + ) + }), + )?; + let layout = self + .vm + .get_runtime() + .type_to_type_layout(&ty) + .map_err(|e| self.convert_vm_error(e))?; + let Some(bytes) = value.typed_serialize(&layout) else { + invariant_violation!("Failed to deserialize already serialized Move value"); + }; + Ok((module_id.clone(), tag, bytes)) + }) + .collect::, ExecutionError>>()?; + self.user_events.extend(new_events); + Ok(()) + } + + /// Takes an iterator of arguments and flattens a Result into a NestedResult if there + /// is more than one result. + /// However, it is currently gated to 1 result, so this function is in place for future + /// changes. This is currently blocked by more invasive work needed to update argument idx + /// in errors + pub fn splat_args>( + &self, + start_idx: usize, + args: Items, + ) -> Result, ExecutionError> + where + Items::IntoIter: ExactSizeIterator, + { + if !self.protocol_config.normalize_ptb_arguments() { + Ok(args.into_iter().map(|arg| Arg(Arg_::V1(arg))).collect()) + } else { + let args = args.into_iter(); + let _args_len = args.len(); + let mut res = vec![]; + for (arg_idx, arg) in args.enumerate() { + self.splat_arg(&mut res, arg) + .map_err(|e| e.into_execution_error(start_idx + arg_idx))?; + } + debug_assert_eq!(res.len(), _args_len); + Ok(res) + } + } + + fn splat_arg(&self, res: &mut Vec, arg: Argument) -> Result<(), EitherError> { + match arg { + Argument::GasCoin => res.push(Arg(Arg_::V2(NormalizedArg::GasCoin))), + Argument::Input(i) => { + if i as usize >= self.inputs.len() { + return Err(CommandArgumentError::IndexOutOfBounds { idx: i }.into()); + } + res.push(Arg(Arg_::V2(NormalizedArg::Input(i)))) + } + Argument::NestedResult(i, j) => { + let Some(command_result) = self.results.get(i as usize) else { + return Err(CommandArgumentError::IndexOutOfBounds { idx: i }.into()); + }; + if j as usize >= command_result.len() { + return Err(CommandArgumentError::SecondaryIndexOutOfBounds { + result_idx: i, + secondary_idx: j, + } + .into()); + }; + res.push(Arg(Arg_::V2(NormalizedArg::Result(i, j)))) + } + Argument::Result(i) => { + let Some(result) = self.results.get(i as usize) else { + return Err(CommandArgumentError::IndexOutOfBounds { idx: i }.into()); + }; + let Ok(len): Result = result.len().try_into() else { + invariant_violation!("Result of length greater than u16::MAX"); + }; + if len != 1 { + // TODO protocol config to allow splatting of args + return Err( + CommandArgumentError::InvalidResultArity { result_idx: i }.into() + ); + } + res.extend((0..len).map(|j| Arg(Arg_::V2(NormalizedArg::Result(i, j))))) + } + } + Ok(()) + } + + pub fn one_arg( + &self, + command_arg_idx: usize, + arg: Argument, + ) -> Result { + let args = self.splat_args(command_arg_idx, vec![arg])?; + let Ok([arg]): Result<[Arg; 1], _> = args.try_into() else { + return Err(command_argument_error( + CommandArgumentError::InvalidArgumentArity, + command_arg_idx, + )); + }; + Ok(arg) + } + + /// Get the argument value. Cloning the value if it is copyable, and setting its value to None + /// if it is not (making it unavailable). + /// Errors if out of bounds, if the argument is borrowed, if it is unavailable (already taken), + /// or if it is an object that cannot be taken by value (shared or immutable) + pub fn by_value_arg( + &mut self, + command_kind: CommandKind, + arg_idx: usize, + arg: Arg, + ) -> Result { + self.by_value_arg_(command_kind, arg) + .map_err(|e| e.into_execution_error(arg_idx)) + } + fn by_value_arg_( + &mut self, + command_kind: CommandKind, + arg: Arg, + ) -> Result { + let shared_obj_deletion_enabled = self.protocol_config.shared_object_deletion(); + let per_command_shared_object_transfer_rules = self + .protocol_config + .per_command_shared_object_transfer_rules(); + let is_borrowed = self.arg_is_borrowed(&arg); + let (input_metadata_opt, val_opt, shared_object_ids) = + self.borrow_mut(arg, UsageKind::ByValue)?; + let is_copyable = if let Some(val) = val_opt { + val.is_copyable() + } else { + return Err(CommandArgumentError::InvalidValueUsage.into()); + }; + // If it was taken, we catch this above. + // If it was not copyable and was borrowed, error as it creates a dangling reference in + // effect. + // We allow copyable values to be copied out even if borrowed, as we do not care about + // referential transparency at this level. + if !is_copyable && is_borrowed { + return Err(CommandArgumentError::InvalidValueUsage.into()); + } + // Gas coin cannot be taken by value, except in TransferObjects + if arg.is_gas_coin() && !matches!(command_kind, CommandKind::TransferObjects) { + return Err(CommandArgumentError::InvalidGasCoinUsage.into()); + } + // Immutable objects cannot be taken by value + if matches!( + input_metadata_opt, + Some(InputObjectMetadata::InputObject { + owner: Owner::Immutable, + .. + }) + ) { + return Err(CommandArgumentError::InvalidObjectByValue.into()); + } + if ( + // this check can be removed after shared_object_deletion feature flag is removed + matches!( + input_metadata_opt, + Some(InputObjectMetadata::InputObject { + owner: Owner::Shared { .. }, + .. + }) + ) && !shared_obj_deletion_enabled + ) { + return Err(CommandArgumentError::InvalidObjectByValue.into()); + } + + // Any input object taken by value must be exclusively mutable + match input_metadata_opt { + Some(InputObjectMetadata::InputObject { mutability, .. }) => match mutability { + // NonExclusiveWrite can be taken by value, but unless it is re-shared + // with no mutations, the transaction will abort. + Mutability::Mutable | Mutability::NonExclusiveWrite => (), + Mutability::Immutable => { + return Err(CommandArgumentError::InvalidObjectByValue.into()); + } + }, + Some(InputObjectMetadata::Receiving { .. }) => (), + None => (), + } + + let val = if is_copyable { + val_opt.as_ref().unwrap().clone() + } else { + val_opt.take().unwrap() + }; + if per_command_shared_object_transfer_rules { + // Track the shared object ID if `per_command_shared_object_transfer_rules` + // is enabled + let shared_object_ids = shared_object_ids.clone(); + self.per_command_by_value_shared_objects + .extend(shared_object_ids); + } + Ok(V::try_from_value(val)?) + } + + /// Mimic a mutable borrow by taking the argument value, setting its value to None, + /// making it unavailable. The value will be marked as borrowed and must be returned with + /// restore_arg + /// Errors if out of bounds, if the argument is borrowed, if it is unavailable (already taken), + /// or if it is an object that cannot be mutably borrowed (immutable) + pub fn borrow_arg_mut( + &mut self, + arg_idx: usize, + arg: Arg, + ) -> Result { + self.borrow_arg_mut_(arg) + .map_err(|e| e.into_execution_error(arg_idx)) + } + fn borrow_arg_mut_(&mut self, arg: Arg) -> Result { + let per_command_shared_object_transfer_rules = self + .protocol_config + .per_command_shared_object_transfer_rules(); + // mutable borrowing requires unique usage + if self.arg_is_borrowed(&arg) { + return Err(CommandArgumentError::InvalidValueUsage.into()); + } + self.borrowed.insert(arg, /* is_mut */ true); + let (input_metadata_opt, val_opt, shared_object_ids) = + self.borrow_mut(arg, UsageKind::BorrowMut)?; + let is_copyable = if let Some(val) = val_opt { + val.is_copyable() + } else { + // error if taken + return Err(CommandArgumentError::InvalidValueUsage.into()); + }; + match input_metadata_opt { + Some(InputObjectMetadata::InputObject { mutability, .. }) => match mutability { + Mutability::Mutable | Mutability::NonExclusiveWrite => (), + Mutability::Immutable => { + return Err(CommandArgumentError::InvalidObjectByMutRef.into()); + } + }, + Some(InputObjectMetadata::Receiving { .. }) => (), + None => (), + } + // if it is copyable, don't take it as we allow for the value to be copied even if + // mutably borrowed + let val = if is_copyable { + val_opt.as_ref().unwrap().clone() + } else { + val_opt.take().unwrap() + }; + if per_command_shared_object_transfer_rules { + // track shared object IDs if `per_command_shared_object_transfer_rules` + // is enabled--but only for vectors from MakeMoveVec + let normalized_arg = match arg { + Arg(Arg_::V2(arg)) => arg, + Arg(Arg_::V1(_)) => { + invariant_violation!( + "v1 should not be used with per_command_shared_object_transfer_rules" + ) + } + }; + match normalized_arg { + NormalizedArg::GasCoin | NormalizedArg::Input(_) => (), + NormalizedArg::Result(_, _) => { + // these will be populated only for results from MakeMoveVec, if the + // vector is used mutably, we must assume that it could have been + // taken from the vector + let shared_object_ids = shared_object_ids.clone(); + self.per_command_by_value_shared_objects + .extend(shared_object_ids); + } + } + } + Ok(V::try_from_value(val)?) + } + + /// Mimics an immutable borrow by cloning the argument value without setting its value to None + /// Errors if out of bounds, if the argument is mutably borrowed, + /// or if it is unavailable (already taken) + pub fn borrow_arg( + &mut self, + arg_idx: usize, + arg: Arg, + type_: &Type, + ) -> Result { + self.borrow_arg_(arg, type_) + .map_err(|e| e.into_execution_error(arg_idx)) + } + fn borrow_arg_( + &mut self, + arg: Arg, + arg_type: &Type, + ) -> Result { + // immutable borrowing requires the value was not mutably borrowed. + // If it was copied, that is okay. + // If it was taken/moved, we will find out below + if self.arg_is_mut_borrowed(&arg) { + return Err(CommandArgumentError::InvalidValueUsage.into()); + } + self.borrowed.insert(arg, /* is_mut */ false); + let (_input_metadata_opt, val_opt, _shared_object_ids) = + self.borrow_mut(arg, UsageKind::BorrowImm)?; + if val_opt.is_none() { + return Err(CommandArgumentError::InvalidValueUsage.into()); + } + + // We eagerly reify receiving argument types at the first usage of them. + if let &mut Some(Value::Receiving(_, _, ref mut recv_arg_type @ None)) = val_opt { + let Type::Reference(inner) = arg_type else { + return Err(CommandArgumentError::InvalidValueUsage.into()); + }; + *recv_arg_type = Some(*(*inner).clone()); + } + + Ok(V::try_from_value(val_opt.as_ref().unwrap().clone())?) + } + + /// Restore an argument after being mutably borrowed + pub fn restore_arg( + &mut self, + updates: &mut Mode::ArgumentUpdates, + arg: Arg, + value: Value, + ) -> Result<(), ExecutionError> { + let per_command_shared_object_transfer_rules = self + .protocol_config + .per_command_shared_object_transfer_rules(); + Mode::add_argument_update(self, updates, arg.into(), &value)?; + let was_mut_opt = self.borrowed.remove(&arg); + assert_invariant!( + was_mut_opt.is_some() && was_mut_opt.unwrap(), + "Should never restore a non-mut borrowed value. \ + The take+restore is an implementation detail of mutable references" + ); + // restore is exclusively used for mut + let Ok((_, value_opt, shared_object_ids)) = self.borrow_mut_impl(arg, None) else { + invariant_violation!("Should be able to borrow argument to restore it") + }; + if per_command_shared_object_transfer_rules { + let normalized_arg = match arg { + Arg(Arg_::V2(arg)) => arg, + Arg(Arg_::V1(_)) => { + invariant_violation!( + "v1 should not be used with per_command_shared_object_transfer_rules" + ) + } + }; + match normalized_arg { + NormalizedArg::GasCoin | NormalizedArg::Input(_) => (), + NormalizedArg::Result(_, _) => { + // these will be populated only for results from MakeMoveVec, if the + // vector is used mutably, we must assume that it could have been + // taken from the vector + shared_object_ids.clear(); + } + } + } + + let old_value = value_opt.replace(value); + assert_invariant!( + old_value.is_none() || old_value.unwrap().is_copyable(), + "Should never restore a non-taken value, unless it is copyable. \ + The take+restore is an implementation detail of mutable references" + ); + + Ok(()) + } + + /// Transfer the object to a new owner + pub fn transfer_object( + &mut self, + obj: ObjectValue, + addr: SuiAddress, + ) -> Result<(), ExecutionError> { + self.additional_transfers.push((addr, obj)); + Ok(()) + } + + /// Create a new package + pub fn new_package<'p>( + &self, + modules: &[CompiledModule], + dependencies: impl IntoIterator, + ) -> Result { + MovePackage::new_initial(modules, self.protocol_config, dependencies) + } + + /// Create a package upgrade from `previous_package` with `new_modules` and `dependencies` + pub fn upgrade_package<'p>( + &self, + storage_id: ObjectID, + previous_package: &MovePackage, + new_modules: &[CompiledModule], + dependencies: impl IntoIterator, + ) -> Result { + previous_package.new_upgraded( + storage_id, + new_modules, + self.protocol_config, + dependencies, + ) + } + + /// Add a newly created package to write as an effect of the transaction + pub fn write_package(&mut self, package: MovePackage) { + self.new_packages.push(package); + } + + /// Return the last package pushed in `write_package`. + /// This function should be used in block of codes that push a package, verify + /// it, run the init and in case of error will remove the package. + /// The package has to be pushed for the init to run correctly. + pub fn pop_package(&mut self) -> Option { + self.new_packages.pop() + } + + /// Finish a command: clearing the borrows and adding the results to the result vector + pub fn push_command_results( + &mut self, + command_kind: CommandKind, + mut results: Vec, + ) -> Result<(), ExecutionError> { + assert_invariant!( + self.borrowed.values().all(|is_mut| !is_mut), + "all mut borrows should be restored" + ); + // clear borrow state + self.borrowed = HashMap::new(); + let results = if self + .protocol_config + .per_command_shared_object_transfer_rules() + { + if let CommandKind::MakeMoveVec = command_kind { + // For make MoveVec, we propagate the shared objects taken by value + assert_invariant!( + results.len() == 1, + "MakeMoveVec should return a single result" + ); + let mut result = ResultValue::new(results.pop().unwrap()); + result.shared_object_ids = + std::mem::take(&mut self.per_command_by_value_shared_objects); + vec![result] + } else { + // For all other commands, we check the shared objects taken by value + // are deleted or re-shared + check_shared_object_usage( + self.object_runtime()?, + &self.per_command_by_value_shared_objects, + )?; + self.per_command_by_value_shared_objects.clear(); + results.into_iter().map(ResultValue::new).collect() + } + } else { + results.into_iter().map(ResultValue::new).collect() + }; + self.results.push(results); + Ok(()) + } + + /// Determine the object changes and collect all user events + pub fn finish(self) -> Result { + let Self { + protocol_config, + vm, + linkage_view, + mut native_extensions, + tx_context, + gas_charger, + additional_transfers, + new_packages, + gas, + inputs, + results, + user_events, + state_view, + .. + } = self; + let ref_context: &RefCell = tx_context.borrow(); + let tx_digest = ref_context.borrow().digest(); + let gas_id_opt = gas.object_metadata.as_ref().map(|info| info.id()); + let mut loaded_runtime_objects = BTreeMap::new(); + let mut additional_writes = BTreeMap::new(); + let mut by_value_shared_objects = BTreeSet::new(); + let mut consensus_owner_objects = BTreeMap::new(); + for input in inputs.into_iter().chain(std::iter::once(gas)) { + let InputValue { + object_metadata: + Some(InputObjectMetadata::InputObject { + // Both Mutable and NonExclusiveWrite are passed as &mut inputs. + // Therefore, the type checker will allow mutation to either. It + // is illegal to mutate NonExclusiveWrite objects, but we check this + // post-execution. + // Note that NonExclusiveWrite is not currently available to user + // transactions. + mutability: Mutability::Mutable | Mutability::NonExclusiveWrite, + id, + version, + owner, + }), + inner: ResultValue { value, .. }, + } = input + else { + continue; + }; + loaded_runtime_objects.insert( + id, + LoadedRuntimeObject { + version, + is_modified: true, + }, + ); + if let Some(Value::Object(object_value)) = value { + add_additional_write(&mut additional_writes, owner, object_value)?; + } else if owner.is_shared() { + by_value_shared_objects.insert(id); + } else if matches!(owner, Owner::ConsensusAddressOwner { .. }) { + consensus_owner_objects.insert(id, owner.clone()); + } + } + // check for unused values + // disable this check for dev inspect + if !Mode::allow_arbitrary_values() { + for (i, command_result) in results.iter().enumerate() { + for (j, result_value) in command_result.iter().enumerate() { + let ResultValue { + last_usage_kind, + value, + shared_object_ids: _, + } = result_value; + match value { + None => (), + Some(Value::Object(_)) => { + return Err(ExecutionErrorKind::UnusedValueWithoutDrop { + result_idx: i as u16, + secondary_idx: j as u16, + } + .into()); + } + Some(Value::Raw(RawValueType::Any, _)) => (), + Some(Value::Raw(RawValueType::Loaded { abilities, .. }, _)) => { + // - nothing to check for drop + // - if it does not have drop, but has copy, + // the last usage must be by value in order to "lie" and say that the + // last usage is actually a take instead of a clone + // - Otherwise, an error + if abilities.has_drop() + || (abilities.has_copy() + && matches!(last_usage_kind, Some(UsageKind::ByValue))) + { + } else { + let msg = if abilities.has_copy() { + "The value has copy, but not drop. \ + Its last usage must be by-value so it can be taken." + } else { + "Unused value without drop" + }; + return Err(ExecutionError::new_with_source( + ExecutionErrorKind::UnusedValueWithoutDrop { + result_idx: i as u16, + secondary_idx: j as u16, + }, + msg, + )); + } + } + // Receiving arguments can be dropped without being received + Some(Value::Receiving(_, _, _)) => (), + } + } + } + } + // add transfers from TransferObjects command + for (recipient, object_value) in additional_transfers { + let owner = Owner::AddressOwner(recipient); + add_additional_write(&mut additional_writes, owner, object_value)?; + } + // Refund unused gas + if let Some(gas_id) = gas_id_opt { + refund_max_gas_budget(&mut additional_writes, gas_charger, gas_id)?; + } + + let object_runtime: ObjectRuntime = native_extensions.remove().map_err(|e| { + convert_vm_error( + e.finish(Location::Undefined), + vm, + &linkage_view, + protocol_config.resolve_abort_locations_to_package_id(), + ) + })?; + + let RuntimeResults { + writes, + user_events: remaining_events, + accumulator_events, + loaded_child_objects, + mut created_object_ids, + deleted_object_ids, + settlement_input_sui, + settlement_output_sui, + } = object_runtime.finish()?; + assert_invariant!( + remaining_events.is_empty(), + "Events should be taken after every Move call" + ); + + loaded_runtime_objects.extend(loaded_child_objects); + + let mut written_objects = BTreeMap::new(); + for package in new_packages { + let package_obj = Object::new_from_package(package, tx_digest); + let id = package_obj.id(); + created_object_ids.insert(id); + written_objects.insert(id, package_obj); + } + for (id, additional_write) in additional_writes { + let AdditionalWrite { + recipient, + type_, + has_public_transfer, + bytes, + } = additional_write; + // safe given the invariant that the runtime correctly propagates has_public_transfer + let move_object = unsafe { + create_written_object::( + vm, + &linkage_view, + protocol_config, + &loaded_runtime_objects, + id, + type_, + has_public_transfer, + bytes, + )? + }; + let object = Object::new_move(move_object, recipient, tx_digest); + written_objects.insert(id, object); + if let Some(loaded) = loaded_runtime_objects.get_mut(&id) { + loaded.is_modified = true; + } + } + + for (id, (recipient, tag, value)) in writes { + let ty = unwrap_type_tag_load( + protocol_config, + vm.get_runtime() + .try_load_cached_type(&TypeTag::from(tag.clone())) + .map_err(|e| { + convert_vm_error( + e, + vm, + &linkage_view, + protocol_config.resolve_abort_locations_to_package_id(), + ) + })? + .ok_or_else(|| { + make_invariant_violation!("Failed to load type for event tag: {}", tag) + }), + )?; + let abilities = vm.get_runtime().get_type_abilities(&ty).map_err(|e| { + convert_vm_error( + e, + vm, + &linkage_view, + protocol_config.resolve_abort_locations_to_package_id(), + ) + })?; + let has_public_transfer = abilities.has_store(); + let layout = vm.get_runtime().type_to_type_layout(&ty).map_err(|e| { + convert_vm_error( + e, + vm, + &linkage_view, + protocol_config.resolve_abort_locations_to_package_id(), + ) + })?; + let Some(bytes) = value.typed_serialize(&layout) else { + invariant_violation!("Failed to deserialize already serialized Move value"); + }; + // safe because has_public_transfer has been determined by the abilities + let move_object = unsafe { + create_written_object::( + vm, + &linkage_view, + protocol_config, + &loaded_runtime_objects, + id, + ty, + has_public_transfer, + bytes, + )? + }; + let object = Object::new_move(move_object, recipient, tx_digest); + written_objects.insert(id, object); + } + + finish( + protocol_config, + state_view, + gas_charger, + &ref_context.borrow(), + &by_value_shared_objects, + &consensus_owner_objects, + loaded_runtime_objects, + written_objects, + created_object_ids, + deleted_object_ids, + user_events, + accumulator_events, + settlement_input_sui, + settlement_output_sui, + ) + } + + /// Convert a VM Error to an execution one + pub fn convert_vm_error(&self, error: VMError) -> ExecutionError { + convert_vm_error( + error, + self.vm, + &self.linkage_view, + self.protocol_config.resolve_abort_locations_to_package_id(), + ) + } + + /// Special case errors for type arguments to Move functions + pub fn convert_type_argument_error(&self, idx: usize, error: VMError) -> ExecutionError { + convert_type_argument_error( + idx, + error, + self.vm, + &self.linkage_view, + self.protocol_config.resolve_abort_locations_to_package_id(), + ) + } + + /// Returns true if the value at the argument's location is borrowed, mutably or immutably + fn arg_is_borrowed(&self, arg: &Arg) -> bool { + self.borrowed.contains_key(arg) + } + + /// Returns true if the value at the argument's location is mutably borrowed + fn arg_is_mut_borrowed(&self, arg: &Arg) -> bool { + matches!(self.borrowed.get(arg), Some(/* mut */ true)) + } + + /// Internal helper to borrow the value for an argument and update the most recent usage + fn borrow_mut( + &mut self, + arg: Arg, + usage: UsageKind, + ) -> Result< + ( + Option<&InputObjectMetadata>, + &mut Option, + &mut BTreeSet, + ), + EitherError, + > { + self.borrow_mut_impl(arg, Some(usage)) + } + + /// Internal helper to borrow the value for an argument + /// Updates the most recent usage if specified + fn borrow_mut_impl( + &mut self, + arg: Arg, + update_last_usage: Option, + ) -> Result< + ( + Option<&InputObjectMetadata>, + &mut Option, + &mut BTreeSet, + ), + EitherError, + > { + match arg.0 { + Arg_::V1(arg) => { + assert_invariant!( + !self.protocol_config.normalize_ptb_arguments(), + "Should not be using v1 args with normalized args" + ); + Ok(self.borrow_mut_impl_v1(arg, update_last_usage)?) + } + Arg_::V2(arg) => { + assert_invariant!( + self.protocol_config.normalize_ptb_arguments(), + "Should be using only v2 args with normalized args" + ); + Ok(self.borrow_mut_impl_v2(arg, update_last_usage)?) + } + } + } + + // v1 of borrow_mut_impl + fn borrow_mut_impl_v1( + &mut self, + arg: Argument, + update_last_usage: Option, + ) -> Result< + ( + Option<&InputObjectMetadata>, + &mut Option, + &mut BTreeSet, + ), + CommandArgumentError, + > { + let (metadata, result_value) = match arg { + Argument::GasCoin => (self.gas.object_metadata.as_ref(), &mut self.gas.inner), + Argument::Input(i) => { + let Some(input_value) = self.inputs.get_mut(i as usize) else { + return Err(CommandArgumentError::IndexOutOfBounds { idx: i }); + }; + (input_value.object_metadata.as_ref(), &mut input_value.inner) + } + Argument::Result(i) => { + let Some(command_result) = self.results.get_mut(i as usize) else { + return Err(CommandArgumentError::IndexOutOfBounds { idx: i }); + }; + if command_result.len() != 1 { + return Err(CommandArgumentError::InvalidResultArity { result_idx: i }); + } + (None, &mut command_result[0]) + } + Argument::NestedResult(i, j) => { + let Some(command_result) = self.results.get_mut(i as usize) else { + return Err(CommandArgumentError::IndexOutOfBounds { idx: i }); + }; + let Some(result_value) = command_result.get_mut(j as usize) else { + return Err(CommandArgumentError::SecondaryIndexOutOfBounds { + result_idx: i, + secondary_idx: j, + }); + }; + (None, result_value) + } + }; + if let Some(usage) = update_last_usage { + result_value.last_usage_kind = Some(usage); + } + Ok(( + metadata, + &mut result_value.value, + &mut result_value.shared_object_ids, + )) + } + + // v2 of borrow_mut_impl + fn borrow_mut_impl_v2( + &mut self, + arg: NormalizedArg, + update_last_usage: Option, + ) -> Result< + ( + Option<&InputObjectMetadata>, + &mut Option, + &mut BTreeSet, + ), + ExecutionError, + > { + let (metadata, result_value) = match arg { + NormalizedArg::GasCoin => (self.gas.object_metadata.as_ref(), &mut self.gas.inner), + NormalizedArg::Input(i) => { + let input_value = self + .inputs + .get_mut(i as usize) + .ok_or_else(|| make_invariant_violation!("bounds already checked"))?; + let metadata = input_value.object_metadata.as_ref(); + (metadata, &mut input_value.inner) + } + NormalizedArg::Result(i, j) => { + let result_value = self + .results + .get_mut(i as usize) + .ok_or_else(|| make_invariant_violation!("bounds already checked"))? + .get_mut(j as usize) + .ok_or_else(|| make_invariant_violation!("bounds already checked"))?; + (None, result_value) + } + }; + if let Some(usage) = update_last_usage { + result_value.last_usage_kind = Some(usage); + } + Ok(( + metadata, + &mut result_value.value, + &mut result_value.shared_object_ids, + )) + } + + pub(crate) fn execute_function_bypass_visibility( + &mut self, + module: &ModuleId, + function_name: &IdentStr, + ty_args: Vec, + args: Vec>, + tracer: &mut Option, + ) -> VMResult { + let gas_status = self.gas_charger.move_gas_status_mut(); + let mut data_store = SuiDataStore::new(&self.linkage_view, &self.new_packages); + self.vm.get_runtime().execute_function_bypass_visibility( + module, + function_name, + ty_args, + args, + &mut data_store, + &mut SuiGasMeter(gas_status), + &mut self.native_extensions, + tracer.as_mut(), + ) + } + + pub(crate) fn load_function( + &mut self, + module_id: &ModuleId, + function_name: &IdentStr, + type_arguments: &[Type], + ) -> VMResult { + let mut data_store = SuiDataStore::new(&self.linkage_view, &self.new_packages); + self.vm.get_runtime().load_function( + module_id, + function_name, + type_arguments, + &mut data_store, + ) + } + + pub(crate) fn make_object_value( + &mut self, + type_: MoveObjectType, + has_public_transfer: bool, + used_in_non_entry_move_call: bool, + contents: &[u8], + ) -> Result { + make_object_value( + self.protocol_config, + self.vm, + &mut self.linkage_view, + &self.new_packages, + type_, + has_public_transfer, + used_in_non_entry_move_call, + contents, + ) + } + + pub fn publish_module_bundle( + &mut self, + modules: Vec>, + sender: AccountAddress, + ) -> VMResult<()> { + // TODO: publish_module_bundle() currently doesn't charge gas. + // Do we want to charge there? + let mut data_store = SuiDataStore::new(&self.linkage_view, &self.new_packages); + self.vm.get_runtime().publish_module_bundle( + modules, + sender, + &mut data_store, + &mut SuiGasMeter(self.gas_charger.move_gas_status_mut()), + ) + } + + pub fn size_bound_raw(&self, bound: u64) -> SizeBound { + if self.protocol_config.max_ptb_value_size_v2() { + SizeBound::Raw(bound) + } else { + SizeBound::Object(bound) + } + } + + pub fn size_bound_vector_elem(&self, bound: u64) -> SizeBound { + if self.protocol_config.max_ptb_value_size_v2() { + SizeBound::VectorElem(bound) + } else { + SizeBound::Object(bound) + } + } + + pub(crate) fn deserialize_modules( + &self, + module_bytes: &[Vec], + ) -> Result, ExecutionError> { + let binary_config = self.protocol_config.binary_config(None); + let modules = module_bytes + .iter() + .map(|b| { + CompiledModule::deserialize_with_config(b, &binary_config) + .map_err(|e| e.finish(Location::Undefined)) + }) + .collect::>>() + .map_err(|e| self.convert_vm_error(e))?; + + assert_invariant!( + !modules.is_empty(), + "input checker ensures package is not empty" + ); + + Ok(modules) + } + } + + impl Arg { + fn is_gas_coin(&self) -> bool { + // kept as two separate matches for exhaustiveness + match self { + Arg(Arg_::V1(a)) => matches!(a, Argument::GasCoin), + Arg(Arg_::V2(n)) => matches!(n, NormalizedArg::GasCoin), + } + } + } + + impl From for Argument { + fn from(arg: Arg) -> Self { + match arg.0 { + Arg_::V1(a) => a, + Arg_::V2(normalized) => match normalized { + NormalizedArg::GasCoin => Argument::GasCoin, + NormalizedArg::Input(i) => Argument::Input(i), + NormalizedArg::Result(i, j) => Argument::NestedResult(i, j), + }, + } + } + } + + impl TypeTagResolver for ExecutionContext<'_, '_, '_> { + fn get_type_tag(&self, type_: &Type) -> Result { + self.vm + .get_runtime() + .get_type_tag(type_) + .map_err(|e| self.convert_vm_error(e)) + } + } + + /// Fetch the package at `package_id` with a view to using it as a link context. Produces an error + /// if the object at that ID does not exist, or is not a package. + fn get_package( + package_store: &dyn PackageStore, + package_id: ObjectID, + ) -> VMResult> { + match package_store.get_package(&package_id) { + Ok(Some(package)) => Ok(package), + Ok(None) => Err(PartialVMError::new(StatusCode::LINKER_ERROR) + .with_message(format!("Cannot find link context {package_id} in store")) + .finish(Location::Undefined)), + Err(err) => Err(PartialVMError::new(StatusCode::LINKER_ERROR) + .with_message(format!("Error loading {package_id} from store: {err}")) + .finish(Location::Undefined)), + } + } + + /// Check for valid shared object usage, either deleted or re-shared, at the end of a command + pub fn check_shared_object_usage<'a>( + object_runtime: &ObjectRuntime, + consumed_shared_objects: impl IntoIterator, + ) -> Result<(), ExecutionError> { + for id in consumed_shared_objects { + // valid if done deleted or re-shared + let is_valid_usage = object_runtime.is_deleted(id) + || matches!( + object_runtime.is_transferred(id), + Some(Owner::Shared { .. }) + ); + if !is_valid_usage { + return Err(ExecutionError::new_with_source( + ExecutionErrorKind::SharedObjectOperationNotAllowed, + format!( + "Shared object operation on {} not allowed: \ + cannot be frozen, transferred, or wrapped", + id + ), + )); + } + } + Ok(()) + } + + pub fn finish( + protocol_config: &ProtocolConfig, + state_view: &dyn ExecutionState, + gas_charger: &mut GasCharger, + tx_context: &TxContext, + by_value_shared_objects: &BTreeSet, + consensus_owner_objects: &BTreeMap, + loaded_runtime_objects: BTreeMap, + written_objects: BTreeMap, + created_object_ids: IndexSet, + deleted_object_ids: IndexSet, + user_events: Vec<(ModuleId, StructTag, Vec)>, + accumulator_events: Vec, + settlement_input_sui: u64, + settlement_output_sui: u64, + ) -> Result { + // Before finishing, ensure that any shared object taken by value by the transaction is either: + // 1. Mutated (and still has a shared ownership); or + // 2. Deleted. + // Otherwise, the shared object operation is not allowed and we fail the transaction. + for id in by_value_shared_objects { + // If it's been written it must have been reshared so must still have an ownership + // of `Shared`. + if let Some(obj) = written_objects.get(id) { + if !obj.is_shared() { + if protocol_config.per_command_shared_object_transfer_rules() { + invariant_violation!( + "There should be no shared objects unaccounted for when \ + per_command_shared_object_transfer_rules is enabled" + ) + } else { + return Err(ExecutionError::new( + ExecutionErrorKind::SharedObjectOperationNotAllowed, + Some( + format!( + "Shared object operation on {} not allowed: \ + cannot be frozen, transferred, or wrapped", + id + ) + .into(), + ), + )); + } + } + } else { + // If it's not in the written objects, the object must have been deleted. Otherwise + // it's an error. + if !deleted_object_ids.contains(id) { + if protocol_config.per_command_shared_object_transfer_rules() { + invariant_violation!( + "There should be no shared objects unaccounted for when \ + per_command_shared_object_transfer_rules is enabled" + ) + } else { + return Err(ExecutionError::new( + ExecutionErrorKind::SharedObjectOperationNotAllowed, + Some( + format!("Shared object operation on {} not allowed: \ + shared objects used by value must be re-shared if not deleted", id).into(), + ), + )); + } + } + } + } + + // Before finishing, enforce auth restrictions on consensus objects. + for (id, original_owner) in consensus_owner_objects { + let Owner::ConsensusAddressOwner { owner, .. } = original_owner else { + panic!( + "verified before adding to `consensus_owner_objects` that these are ConsensusAddressOwner" + ); + }; + // Already verified in pre-execution checks that tx sender is the object owner. + // Owner is allowed to do anything with the object. + if tx_context.sender() != *owner { + debug_fatal!( + "transaction with a singly owned input object where the tx sender is not the owner should never be executed" + ); + if protocol_config.per_command_shared_object_transfer_rules() { + invariant_violation!( + "Shared object operation on {} not allowed: \ + transaction with singly owned input object must be sent by the owner", + id, + ); + } else { + return Err(ExecutionError::new( + ExecutionErrorKind::SharedObjectOperationNotAllowed, + Some( + format!("Shared object operation on {} not allowed: \ + transaction with singly owned input object must be sent by the owner", id).into(), + ), + )); + } + } + // If an Owner type is implemented with support for more fine-grained authorization, + // checks should be performed here. For example, transfers and wraps can be detected + // by comparing `original_owner` with: + // let new_owner = written_objects.get(&id).map(|obj| obj.owner); + // + // Deletions can be detected with: + // let deleted = deleted_object_ids.contains(&id); + } + + let user_events: Vec = user_events + .into_iter() + .map(|(module_id, tag, contents)| { + Event::new( + module_id.address(), + module_id.name(), + tx_context.sender(), + tag, + contents, + ) + }) + .collect(); + + let mut receiving_funds_type_and_owners = BTreeMap::new(); + let accumulator_events = accumulator_events + .into_iter() + .map(|accum_event| { + if let Some(ty) = Balance::maybe_get_balance_type_param(&accum_event.target_ty) { + receiving_funds_type_and_owners + .entry(ty) + .or_insert_with(BTreeSet::new) + .insert(accum_event.target_addr.into()); + } + let value = match accum_event.value { + MoveAccumulatorValue::U64(amount) => AccumulatorValue::Integer(amount), + MoveAccumulatorValue::EventRef(event_idx) => { + let Some(event) = user_events.get(event_idx as usize) else { + invariant_violation!( + "Could not find authenticated event at index {}", + event_idx + ); + }; + let digest = event.digest(); + AccumulatorValue::EventDigest(nonempty![(event_idx, digest)]) + } + }; + + let address = + AccumulatorAddress::new(accum_event.target_addr.into(), accum_event.target_ty); + + let write = AccumulatorWriteV1 { + address, + operation: accum_event.action.into_sui_accumulator_action(), + value, + }; + + Ok(AccumulatorEvent::new( + AccumulatorObjId::new_unchecked(accum_event.accumulator_id), + write, + )) + }) + .collect::, ExecutionError>>()?; + + if protocol_config.enable_coin_deny_list_v2() { + for object in written_objects.values() { + let coin_type = object.type_().and_then(|ty| ty.coin_type_maybe()); + let owner = object.owner.get_address_owner_address(); + if let (Some(ty), Ok(owner)) = (coin_type, owner) { + receiving_funds_type_and_owners + .entry(ty) + .or_insert_with(BTreeSet::new) + .insert(owner); + } + } + let DenyListResult { + result, + num_non_gas_coin_owners, + } = state_view.check_coin_deny_list(receiving_funds_type_and_owners); + gas_charger.charge_coin_transfers(protocol_config, num_non_gas_coin_owners)?; + result?; + } + + Ok(ExecutionResults::V2(ExecutionResultsV2 { + written_objects, + modified_objects: loaded_runtime_objects + .into_iter() + .filter_map(|(id, loaded)| loaded.is_modified.then_some(id)) + .collect(), + created_object_ids: created_object_ids.into_iter().collect(), + deleted_object_ids: deleted_object_ids.into_iter().collect(), + user_events, + accumulator_events, + settlement_input_sui, + settlement_output_sui, + })) + } + + pub fn load_type_from_struct( + vm: &MoveVM, + linkage_view: &LinkageView, + new_packages: &[MovePackage], + struct_tag: &StructTag, + ) -> VMResult { + fn verification_error(code: StatusCode) -> VMResult { + Err(PartialVMError::new(code).finish(Location::Undefined)) + } + + let StructTag { + address, + module, + name, + type_params, + } = struct_tag; + + // Load the package that the struct is defined in, in storage + let defining_id = ObjectID::from_address(*address); + + let data_store = SuiDataStore::new(linkage_view, new_packages); + let move_package = get_package(&data_store, defining_id)?; + + // Set the defining package as the link context while loading the + // struct + let original_address = linkage_view.set_linkage(&move_package).map_err(|e| { + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message(e.to_string()) + .finish(Location::Undefined) + })?; + + let runtime_id = ModuleId::new(original_address, module.clone()); + let data_store = SuiDataStore::new(linkage_view, new_packages); + let res = vm.get_runtime().load_type(&runtime_id, name, &data_store); + linkage_view.reset_linkage().map_err(|e| { + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message(e.to_string()) + .finish(Location::Undefined) + })?; + let (idx, struct_type) = res?; + + // Recursively load type parameters, if necessary + let type_param_constraints = struct_type.type_param_constraints(); + if type_param_constraints.len() != type_params.len() { + return verification_error(StatusCode::NUMBER_OF_TYPE_ARGUMENTS_MISMATCH); + } + + if type_params.is_empty() { + Ok(Type::Datatype(idx)) + } else { + let loaded_type_params = type_params + .iter() + .map(|type_param| load_type(vm, linkage_view, new_packages, type_param)) + .collect::>>()?; + + // Verify that the type parameter constraints on the struct are met + for (constraint, param) in type_param_constraints.zip(&loaded_type_params) { + let abilities = vm.get_runtime().get_type_abilities(param)?; + if !constraint.is_subset(abilities) { + return verification_error(StatusCode::CONSTRAINT_NOT_SATISFIED); + } + } + + Ok(Type::DatatypeInstantiation(Box::new(( + idx, + loaded_type_params, + )))) + } + } + + /// Load `type_tag` to get a `Type` in the provided `session`. `session`'s linkage context may be + /// reset after this operation, because during the operation, it may change when loading a struct. + pub fn load_type( + vm: &MoveVM, + linkage_view: &LinkageView, + new_packages: &[MovePackage], + type_tag: &TypeTag, + ) -> VMResult { + Ok(match type_tag { + TypeTag::Bool => Type::Bool, + TypeTag::U8 => Type::U8, + TypeTag::U16 => Type::U16, + TypeTag::U32 => Type::U32, + TypeTag::U64 => Type::U64, + TypeTag::U128 => Type::U128, + TypeTag::U256 => Type::U256, + TypeTag::Address => Type::Address, + TypeTag::Signer => Type::Signer, + + TypeTag::Vector(inner) => { + Type::Vector(Box::new(load_type(vm, linkage_view, new_packages, inner)?)) + } + TypeTag::Struct(struct_tag) => { + return load_type_from_struct(vm, linkage_view, new_packages, struct_tag); + } + }) + } + + pub(crate) fn make_object_value( + protocol_config: &ProtocolConfig, + vm: &MoveVM, + linkage_view: &mut LinkageView, + new_packages: &[MovePackage], + type_: MoveObjectType, + has_public_transfer: bool, + used_in_non_entry_move_call: bool, + contents: &[u8], + ) -> Result { + let contents = if type_.is_coin() { + let Ok(coin) = Coin::from_bcs_bytes(contents) else { + invariant_violation!("Could not deserialize a coin") + }; + ObjectContents::Coin(coin) + } else { + ObjectContents::Raw(contents.to_vec()) + }; + + let tag: StructTag = type_.into(); + let type_ = load_type_from_struct(vm, linkage_view, new_packages, &tag).map_err(|e| { + convert_vm_error( + e, + vm, + linkage_view, + protocol_config.resolve_abort_locations_to_package_id(), + ) + })?; + let has_public_transfer = if protocol_config.recompute_has_public_transfer_in_execution() { + let abilities = vm.get_runtime().get_type_abilities(&type_).map_err(|e| { + convert_vm_error( + e, + vm, + linkage_view, + protocol_config.resolve_abort_locations_to_package_id(), + ) + })?; + abilities.has_store() + } else { + has_public_transfer + }; + Ok(ObjectValue { + type_, + has_public_transfer, + used_in_non_entry_move_call, + contents, + }) + } + + pub(crate) fn value_from_object( + protocol_config: &ProtocolConfig, + vm: &MoveVM, + linkage_view: &mut LinkageView, + new_packages: &[MovePackage], + object: &Object, + ) -> Result { + let ObjectInner { + data: Data::Move(object), + .. + } = object.as_inner() + else { + invariant_violation!("Expected a Move object"); + }; + + let used_in_non_entry_move_call = false; + make_object_value( + protocol_config, + vm, + linkage_view, + new_packages, + object.type_().clone(), + object.has_public_transfer(), + used_in_non_entry_move_call, + object.contents(), + ) + } + + /// Load an input object from the state_view + fn load_object( + protocol_config: &ProtocolConfig, + vm: &MoveVM, + state_view: &dyn ExecutionState, + linkage_view: &mut LinkageView, + new_packages: &[MovePackage], + input_object_map: &mut BTreeMap, + mutability_override: Option, + id: ObjectID, + ) -> Result { + let Some(obj) = state_view.read_object(&id) else { + // protected by transaction input checker + invariant_violation!("Object {} does not exist yet", id); + }; + // override_as_immutable ==> Owner::Shared or Owner::ConsensusAddressOwner + assert_invariant!( + mutability_override.is_none() + || matches!( + obj.owner, + Owner::Shared { .. } | Owner::ConsensusAddressOwner { .. } + ), + "override_as_immutable should only be set for consensus objects" + ); + let mutability = match obj.owner { + Owner::AddressOwner(_) => Mutability::Mutable, + Owner::Shared { .. } | Owner::ConsensusAddressOwner { .. } => { + mutability_override.unwrap_or(Mutability::Mutable) + } + Owner::Immutable => Mutability::Immutable, + Owner::ObjectOwner(_) => { + // protected by transaction input checker + invariant_violation!("ObjectOwner objects cannot be input") + } + }; + let owner = obj.owner.clone(); + let version = obj.version(); + let object_metadata = InputObjectMetadata::InputObject { + id, + mutability, + owner: owner.clone(), + version, + }; + let obj_value = value_from_object(protocol_config, vm, linkage_view, new_packages, obj)?; + let contained_uids = { + let fully_annotated_layout = vm + .get_runtime() + .type_to_fully_annotated_layout(&obj_value.type_) + .map_err(|e| { + convert_vm_error( + e, + vm, + linkage_view, + protocol_config.resolve_abort_locations_to_package_id(), + ) + })?; + let mut bytes = vec![]; + obj_value.write_bcs_bytes(&mut bytes, None)?; + match get_all_uids(&fully_annotated_layout, &bytes) { + Err(e) => { + invariant_violation!("Unable to retrieve UIDs for object. Got error: {e}") + } + Ok(uids) => uids, + } + }; + let runtime_input = object_runtime::InputObject { + contained_uids, + owner, + version, + }; + let prev = input_object_map.insert(id, runtime_input); + // protected by transaction input checker + assert_invariant!(prev.is_none(), "Duplicate input object {}", id); + Ok(InputValue::new_object(object_metadata, obj_value)) + } + + /// Load a CallArg, either an object or a raw set of BCS bytes + fn load_call_arg( + protocol_config: &ProtocolConfig, + vm: &MoveVM, + state_view: &dyn ExecutionState, + linkage_view: &mut LinkageView, + new_packages: &[MovePackage], + input_object_map: &mut BTreeMap, + tx_context: &TxContext, + call_arg: CallArg, + ) -> Result { + Ok(match call_arg { + CallArg::Pure(bytes) => InputValue::new_raw(RawValueType::Any, bytes), + CallArg::Object(obj_arg) => load_object_arg( + protocol_config, + vm, + state_view, + linkage_view, + new_packages, + input_object_map, + obj_arg, + )?, + CallArg::FundsWithdrawal(FundsWithdrawalArg { + reservation, + type_arg, + withdraw_from, + }) => { + let Ok(type_arg) = type_arg.to_type_tag() else { + // TODO(address-balances): ensure this is caught at signing + invariant_violation!( + "FundsWithdrawArg type arg should have been \ + checked at signing" + ); + }; + let withdrawal_ty = Withdrawal::type_tag(type_arg); + let ty = + load_type(vm, linkage_view, new_packages, &withdrawal_ty).map_err(|e| { + convert_type_argument_error( + 0, + e, + vm, + linkage_view, + protocol_config.resolve_abort_locations_to_package_id(), + ) + })?; + let abilities = vm.get_runtime().get_type_abilities(&ty).map_err(|e| { + convert_vm_error( + e, + vm, + linkage_view, + protocol_config.resolve_abort_locations_to_package_id(), + ) + })?; + let loaded_ty = RawValueType::Loaded { + ty, + abilities, + used_in_non_entry_move_call: false, + }; + let owner = match withdraw_from { + WithdrawFrom::Sender => tx_context.sender(), + WithdrawFrom::Sponsor => { + invariant_violation!( + "WithdrawFrom::Sponsor call arg not supported, \ + should have been checked at signing" + ); + } + }; + // After this point, we can treat this like any other returned/loaded value, e.g. + // from a Move call. As such, sanity check Withdrawal should have only drop. + debug_assert!({ + !abilities.has_key() + && !abilities.has_store() + && !abilities.has_copy() + && abilities.has_drop() + }); + let limit = match reservation { + sui_types::transaction::Reservation::EntireBalance => { + // TODO(address-balances): support entire balance withdrawal + todo!("Entire balance withdrawal not yet supported") + } + sui_types::transaction::Reservation::MaxAmountU64(u) => U256::from(u), + }; + InputValue::withdrawal(loaded_ty, owner, limit) + } + }) + } + + /// Load an ObjectArg from state view, marking if it can be treated as mutable or not + fn load_object_arg( + protocol_config: &ProtocolConfig, + vm: &MoveVM, + state_view: &dyn ExecutionState, + linkage_view: &mut LinkageView, + new_packages: &[MovePackage], + input_object_map: &mut BTreeMap, + obj_arg: ObjectArg, + ) -> Result { + match obj_arg { + ObjectArg::ImmOrOwnedObject((id, _, _)) => load_object( + protocol_config, + vm, + state_view, + linkage_view, + new_packages, + input_object_map, + /* mutability override */ None, + id, + ), + ObjectArg::SharedObject { id, mutability, .. } => { + let mutability = match mutability { + SharedObjectMutability::Mutable => Mutability::Mutable, + // From the perspective of the adapter, non-exclusive write are mutable, + // in that the move code will receive a &mut arg. The object itself + // cannot be written to, this is enforced by post execution checks. + SharedObjectMutability::NonExclusiveWrite => Mutability::NonExclusiveWrite, + SharedObjectMutability::Immutable => Mutability::Immutable, + }; + load_object( + protocol_config, + vm, + state_view, + linkage_view, + new_packages, + input_object_map, + Some(mutability), + id, + ) + } + ObjectArg::Receiving((id, version, _)) => { + Ok(InputValue::new_receiving_object(id, version)) + } + } + } + + /// Generate an additional write for an ObjectValue + fn add_additional_write( + additional_writes: &mut BTreeMap, + owner: Owner, + object_value: ObjectValue, + ) -> Result<(), ExecutionError> { + let ObjectValue { + type_, + has_public_transfer, + contents, + .. + } = object_value; + let bytes = match contents { + ObjectContents::Coin(coin) => coin.to_bcs_bytes(), + ObjectContents::Raw(bytes) => bytes, + }; + let object_id = MoveObject::id_opt(&bytes).map_err(|e| { + ExecutionError::invariant_violation(format!("No id for Raw object bytes. {e}")) + })?; + let additional_write = AdditionalWrite { + recipient: owner, + type_, + has_public_transfer, + bytes, + }; + additional_writes.insert(object_id, additional_write); + Ok(()) + } + + /// The max budget was deducted from the gas coin at the beginning of the transaction, + /// now we return exactly that amount. Gas will be charged by the execution engine + fn refund_max_gas_budget( + additional_writes: &mut BTreeMap, + gas_charger: &mut GasCharger, + gas_id: ObjectID, + ) -> Result<(), ExecutionError> { + let Some(AdditionalWrite { bytes, .. }) = additional_writes.get_mut(&gas_id) else { + invariant_violation!("Gas object cannot be wrapped or destroyed") + }; + let Ok(mut coin) = Coin::from_bcs_bytes(bytes) else { + invariant_violation!("Gas object must be a coin") + }; + let Some(new_balance) = coin.balance.value().checked_add(gas_charger.gas_budget()) else { + return Err(ExecutionError::new_with_source( + ExecutionErrorKind::CoinBalanceOverflow, + "Gas coin too large after returning the max gas budget", + )); + }; + coin.balance = Balance::new(new_balance); + *bytes = coin.to_bcs_bytes(); + Ok(()) + } + + /// Generate an MoveObject given an updated/written object + /// # Safety + /// + /// This function assumes proper generation of has_public_transfer, either from the abilities of + /// the StructTag, or from the runtime correctly propagating from the inputs + unsafe fn create_written_object( + vm: &MoveVM, + linkage_view: &LinkageView, + protocol_config: &ProtocolConfig, + objects_modified_at: &BTreeMap, + id: ObjectID, + type_: Type, + has_public_transfer: bool, + contents: Vec, + ) -> Result { + debug_assert_eq!( + id, + MoveObject::id_opt(&contents).expect("object contents should start with an id") + ); + let old_obj_ver = objects_modified_at + .get(&id) + .map(|obj: &LoadedRuntimeObject| obj.version); + + let type_tag = vm.get_runtime().get_type_tag(&type_).map_err(|e| { + convert_vm_error( + e, + vm, + linkage_view, + protocol_config.resolve_abort_locations_to_package_id(), + ) + })?; + + let struct_tag = match type_tag { + TypeTag::Struct(inner) => *inner, + _ => invariant_violation!("Non struct type for object"), + }; + unsafe { + MoveObject::new_from_execution( + struct_tag.into(), + has_public_transfer, + old_obj_ver.unwrap_or_default(), + contents, + protocol_config, + Mode::packages_are_predefined(), + ) + } + } + + fn unwrap_type_tag_load( + protocol_config: &ProtocolConfig, + ty: Result, + ) -> Result { + if ty.is_err() && !protocol_config.type_tags_in_object_runtime() { + panic!("Failed to load a type tag from the object runtime -- this shouldn't happen") + } else { + ty + } + } + + pub enum EitherError { + CommandArgument(CommandArgumentError), + Execution(ExecutionError), + } + + impl From for EitherError { + fn from(e: ExecutionError) -> Self { + EitherError::Execution(e) + } + } + + impl From for EitherError { + fn from(e: CommandArgumentError) -> Self { + EitherError::CommandArgument(e) + } + } + + impl EitherError { + pub fn into_execution_error(self, command_index: usize) -> ExecutionError { + match self { + EitherError::CommandArgument(e) => command_argument_error(e, command_index), + EitherError::Execution(e) => e, + } + } + } + + fn convert_vm_error>( + error: VMError, + vm: &MoveVM, + state_view: &S, + resolve_abort_location_to_package_id: bool, + ) -> ExecutionError { + crate::error::convert_vm_error_impl( + error, + &|id: &ModuleId| { + if resolve_abort_location_to_package_id { + state_view.relocate(id).unwrap_or_else(|_| id.clone()) + } else { + id.clone() + } + }, + &|id, function| { + vm.load_module(id, state_view).ok().map(|module| { + let fdef = module.function_def_at(function); + let fhandle = module.function_handle_at(fdef.function); + module.identifier_at(fhandle.name).to_string() + }) + }, + ) + } + /// Special case errors for type arguments to Move functions + fn convert_type_argument_error>( + idx: usize, + error: VMError, + vm: &MoveVM, + state_view: &S, + resolve_abort_location_to_package_id: bool, + ) -> ExecutionError { + use sui_types::execution_status::TypeArgumentError; + match error.major_status() { + StatusCode::NUMBER_OF_TYPE_ARGUMENTS_MISMATCH => { + ExecutionErrorKind::TypeArityMismatch.into() + } + StatusCode::TYPE_RESOLUTION_FAILURE => ExecutionErrorKind::TypeArgumentError { + argument_idx: idx as TypeParameterIndex, + kind: TypeArgumentError::TypeNotFound, + } + .into(), + StatusCode::CONSTRAINT_NOT_SATISFIED => ExecutionErrorKind::TypeArgumentError { + argument_idx: idx as TypeParameterIndex, + kind: TypeArgumentError::ConstraintNotSatisfied, + } + .into(), + _ => convert_vm_error(error, vm, state_view, resolve_abort_location_to_package_id), + } + } +} diff --git a/sui-execution/replay_cut/sui-adapter/src/programmable_transactions/execution.rs b/sui-execution/replay_cut/sui-adapter/src/programmable_transactions/execution.rs new file mode 100644 index 0000000000000..333287e76bd25 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/programmable_transactions/execution.rs @@ -0,0 +1,2154 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +pub use checked::*; + +#[sui_macros::with_checked_arithmetic] +mod checked { + use crate::{ + adapter::substitute_package_id, + data_store::{PackageStore, legacy::sui_data_store::SuiDataStore}, + execution_mode::ExecutionMode, + execution_value::{ + CommandKind, ExecutionState, ObjectContents, ObjectValue, RawValueType, Value, + ensure_serialized_size, + }, + gas_charger::GasCharger, + programmable_transactions::{context::*, trace_utils}, + static_programmable_transactions, + type_resolver::TypeTagResolver, + }; + use move_binary_format::file_format::AbilitySet; + use move_binary_format::{ + CompiledModule, + compatibility::{Compatibility, InclusionCheck}, + errors::{Location, PartialVMResult, VMResult}, + file_format::{CodeOffset, FunctionDefinitionIndex, LocalIndex, Visibility}, + file_format_common::VERSION_6, + normalized, + }; + use move_core_types::{ + account_address::AccountAddress, + identifier::{IdentStr, Identifier}, + language_storage::{ModuleId, StructTag, TypeTag}, + u256::U256, + }; + use move_trace_format::format::MoveTraceBuilder; + use move_vm_runtime::{ + move_vm::MoveVM, + session::{LoadedFunctionInstantiation, SerializedReturnValues}, + }; + use move_vm_types::loaded_data::runtime_types::{CachedDatatype, Type}; + use serde::{Deserialize, de::DeserializeSeed}; + use std::{ + cell::{OnceCell, RefCell}, + collections::{BTreeMap, BTreeSet}, + fmt, + rc::Rc, + sync::Arc, + time::Instant, + }; + use sui_move_natives::object_runtime::ObjectRuntime; + use sui_protocol_config::ProtocolConfig; + use sui_types::{ + SUI_FRAMEWORK_ADDRESS, + base_types::{ + MoveLegacyTxContext, MoveObjectType, ObjectID, RESOLVED_ASCII_STR, RESOLVED_STD_OPTION, + RESOLVED_UTF8_STR, SuiAddress, TX_CONTEXT_MODULE_NAME, TX_CONTEXT_STRUCT_NAME, + TxContext, TxContextKind, + }, + coin::Coin, + error::{ExecutionError, ExecutionErrorKind, command_argument_error}, + execution::{ExecutionTiming, ResultWithTimings}, + execution_status::{CommandArgumentError, PackageUpgradeError, TypeArgumentError}, + id::RESOLVED_SUI_ID, + metrics::LimitsMetrics, + move_package::{ + MovePackage, UpgradeCap, UpgradePolicy, UpgradeReceipt, UpgradeTicket, + normalize_deserialized_modules, + }, + storage::{BackingPackageStore, PackageObject, get_package_objects}, + transaction::{Command, ProgrammableMoveCall, ProgrammableTransaction}, + transfer::RESOLVED_RECEIVING_STRUCT, + type_input::{StructInput, TypeInput}, + }; + use sui_verifier::{ + INIT_FN_NAME, + private_generics::{EVENT_MODULE, PRIVATE_TRANSFER_FUNCTIONS, TRANSFER_MODULE}, + private_generics_verifier_v2, + }; + use tracing::instrument; + + pub fn execute( + protocol_config: &ProtocolConfig, + metrics: Arc, + vm: &MoveVM, + state_view: &mut dyn ExecutionState, + package_store: &dyn BackingPackageStore, + tx_context: Rc>, + gas_charger: &mut GasCharger, + pt: ProgrammableTransaction, + trace_builder_opt: &mut Option, + ) -> ResultWithTimings { + if protocol_config.enable_ptb_execution_v2() { + return static_programmable_transactions::execute::( + protocol_config, + metrics, + vm, + state_view, + package_store, + tx_context, + gas_charger, + pt, + trace_builder_opt, + ); + } + + let mut timings = vec![]; + let result = execute_inner::( + &mut timings, + protocol_config, + metrics, + vm, + state_view, + tx_context, + gas_charger, + pt, + trace_builder_opt, + ); + + match result { + Ok(result) => Ok((result, timings)), + Err(e) => { + trace_utils::trace_execution_error(trace_builder_opt, e.to_string()); + + Err((e, timings)) + } + } + } + + pub fn execute_inner( + timings: &mut Vec, + protocol_config: &ProtocolConfig, + metrics: Arc, + vm: &MoveVM, + state_view: &mut dyn ExecutionState, + tx_context: Rc>, + gas_charger: &mut GasCharger, + pt: ProgrammableTransaction, + trace_builder_opt: &mut Option, + ) -> Result { + let ProgrammableTransaction { inputs, commands } = pt; + let mut context = ExecutionContext::new( + protocol_config, + metrics, + vm, + state_view, + tx_context, + gas_charger, + inputs, + )?; + + trace_utils::trace_ptb_summary::(&mut context, trace_builder_opt, &commands)?; + + // execute commands + let mut mode_results = Mode::empty_results(); + for (idx, command) in commands.into_iter().enumerate() { + let start = Instant::now(); + if let Err(err) = + execute_command::(&mut context, &mut mode_results, command, trace_builder_opt) + { + let object_runtime: &ObjectRuntime = context.object_runtime()?; + // We still need to record the loaded child objects for replay + let loaded_runtime_objects = object_runtime.loaded_runtime_objects(); + // we do not save the wrapped objects since on error, they should not be modified + drop(context); + state_view.save_loaded_runtime_objects(loaded_runtime_objects); + timings.push(ExecutionTiming::Abort(start.elapsed())); + return Err(err.with_command_index(idx)); + }; + timings.push(ExecutionTiming::Success(start.elapsed())); + } + + // Save loaded objects table in case we fail in post execution + let object_runtime: &ObjectRuntime = context.object_runtime()?; + // We still need to record the loaded child objects for replay + // Record the objects loaded at runtime (dynamic fields + received) for + // storage rebate calculation. + let loaded_runtime_objects = object_runtime.loaded_runtime_objects(); + // We record what objects were contained in at the start of the transaction + // for expensive invariant checks + let wrapped_object_containers = object_runtime.wrapped_object_containers(); + // We record the generated object IDs for expensive invariant checks + let generated_object_ids = object_runtime.generated_object_ids(); + + // apply changes + let finished = context.finish::(); + // Save loaded objects for debug. We dont want to lose the info + state_view.save_loaded_runtime_objects(loaded_runtime_objects); + state_view.save_wrapped_object_containers(wrapped_object_containers); + state_view.record_execution_results(finished?)?; + state_view.record_generated_object_ids(generated_object_ids); + Ok(mode_results) + } + + /// Execute a single command + #[instrument(level = "trace", skip_all)] + fn execute_command( + context: &mut ExecutionContext<'_, '_, '_>, + mode_results: &mut Mode::ExecutionResults, + command: Command, + trace_builder_opt: &mut Option, + ) -> Result<(), ExecutionError> { + let mut argument_updates = Mode::empty_arguments(); + + let kind = match &command { + Command::MakeMoveVec(_, _) => CommandKind::MakeMoveVec, + Command::TransferObjects(_, _) => CommandKind::TransferObjects, + Command::SplitCoins(_, _) => CommandKind::SplitCoins, + Command::MergeCoins(_, _) => CommandKind::MergeCoins, + Command::MoveCall(_) => CommandKind::MoveCall, + Command::Publish(_, _) => CommandKind::Publish, + Command::Upgrade(_, _, _, _) => CommandKind::Upgrade, + }; + let results = match command { + Command::MakeMoveVec(tag_opt, args) if args.is_empty() => { + let Some(tag) = tag_opt else { + invariant_violation!( + "input checker ensures if args are empty, there is a type specified" + ); + }; + + let tag = to_type_tag(context, tag, 0)?; + + let elem_ty = context.load_type(&tag).map_err(|e| { + if context.protocol_config.convert_type_argument_error() { + context.convert_type_argument_error(0, e) + } else { + context.convert_vm_error(e) + } + })?; + + let ty = Type::Vector(Box::new(elem_ty)); + let abilities = context.get_type_abilities(&ty)?; + // BCS layout for any empty vector should be the same + let bytes = bcs::to_bytes::>(&vec![]).unwrap(); + + trace_utils::trace_make_move_vec(context, trace_builder_opt, vec![], &ty)?; + + vec![Value::Raw( + RawValueType::Loaded { + ty, + abilities, + used_in_non_entry_move_call: false, + }, + bytes, + )] + } + Command::MakeMoveVec(tag_opt, args) => { + let args = context.splat_args(0, args)?; + let elem_abilities = OnceCell::::new(); + let mut res = vec![]; + leb128::write::unsigned(&mut res, args.len() as u64).unwrap(); + let mut arg_iter = args.into_iter().enumerate(); + let mut move_values = vec![]; + let (mut used_in_non_entry_move_call, elem_ty) = match tag_opt { + Some(tag) => { + let tag = to_type_tag(context, tag, 0)?; + let elem_ty = context.load_type(&tag).map_err(|e| { + if context.protocol_config.convert_type_argument_error() { + context.convert_type_argument_error(0, e) + } else { + context.convert_vm_error(e) + } + })?; + (false, elem_ty) + } + // If no tag specified, it _must_ be an object + None => { + // empty args covered above + let (idx, arg) = arg_iter.next().unwrap(); + let obj: ObjectValue = + context.by_value_arg(CommandKind::MakeMoveVec, idx, arg)?; + trace_utils::add_move_value_info_from_obj_value( + context, + trace_builder_opt, + &mut move_values, + &obj, + )?; + let bound = + amplification_bound::(context, &obj.type_, &elem_abilities)?; + obj.write_bcs_bytes( + &mut res, + bound.map(|b| context.size_bound_vector_elem(b)), + )?; + (obj.used_in_non_entry_move_call, obj.type_) + } + }; + for (idx, arg) in arg_iter { + let value: Value = context.by_value_arg(CommandKind::MakeMoveVec, idx, arg)?; + trace_utils::add_move_value_info_from_value( + context, + trace_builder_opt, + &mut move_values, + &elem_ty, + &value, + )?; + check_param_type::(context, idx, &value, &elem_ty)?; + used_in_non_entry_move_call = + used_in_non_entry_move_call || value.was_used_in_non_entry_move_call(); + let bound = amplification_bound::(context, &elem_ty, &elem_abilities)?; + value.write_bcs_bytes( + &mut res, + bound.map(|b| context.size_bound_vector_elem(b)), + )?; + } + let ty = Type::Vector(Box::new(elem_ty)); + let abilities = context.get_type_abilities(&ty)?; + + trace_utils::trace_make_move_vec(context, trace_builder_opt, move_values, &ty)?; + + vec![Value::Raw( + RawValueType::Loaded { + ty, + abilities, + used_in_non_entry_move_call, + }, + res, + )] + } + Command::TransferObjects(objs, addr_arg) => { + let unsplat_objs_len = objs.len(); + let objs = context.splat_args(0, objs)?; + let addr_arg = context.one_arg(unsplat_objs_len, addr_arg)?; + let objs: Vec = objs + .into_iter() + .enumerate() + .map(|(idx, arg)| context.by_value_arg(CommandKind::TransferObjects, idx, arg)) + .collect::>()?; + let addr: SuiAddress = + context.by_value_arg(CommandKind::TransferObjects, objs.len(), addr_arg)?; + + trace_utils::trace_transfer(context, trace_builder_opt, &objs)?; + + for obj in objs { + obj.ensure_public_transfer_eligible()?; + context.transfer_object(obj, addr)?; + } + vec![] + } + Command::SplitCoins(coin_arg, amount_args) => { + let coin_arg = context.one_arg(0, coin_arg)?; + let amount_args = context.splat_args(1, amount_args)?; + let mut obj: ObjectValue = context.borrow_arg_mut(0, coin_arg)?; + let ObjectContents::Coin(coin) = &mut obj.contents else { + let e = ExecutionErrorKind::command_argument_error( + CommandArgumentError::TypeMismatch, + 0, + ); + let msg = "Expected a coin but got an non coin object".to_owned(); + return Err(ExecutionError::new_with_source(e, msg)); + }; + let split_coins: Vec = amount_args + .into_iter() + .map(|amount_arg| { + let amount: u64 = + context.by_value_arg(CommandKind::SplitCoins, 1, amount_arg)?; + let new_coin_id = context.fresh_id()?; + let new_coin = coin.split(amount, new_coin_id)?; + let coin_type = obj.type_.clone(); + // safe because we are propagating the coin type, and relying on the internal + // invariant that coin values have a coin type + let new_coin = unsafe { ObjectValue::coin(coin_type, new_coin) }; + Ok(Value::Object(new_coin)) + }) + .collect::>()?; + + trace_utils::trace_split_coins( + context, + trace_builder_opt, + &obj.type_, + coin, + &split_coins, + )?; + + context.restore_arg::(&mut argument_updates, coin_arg, Value::Object(obj))?; + split_coins + } + Command::MergeCoins(target_arg, coin_args) => { + let target_arg = context.one_arg(0, target_arg)?; + let coin_args = context.splat_args(1, coin_args)?; + let mut target: ObjectValue = context.borrow_arg_mut(0, target_arg)?; + let ObjectContents::Coin(target_coin) = &mut target.contents else { + let e = ExecutionErrorKind::command_argument_error( + CommandArgumentError::TypeMismatch, + 0, + ); + let msg = "Expected a coin but got an non coin object".to_owned(); + return Err(ExecutionError::new_with_source(e, msg)); + }; + let coins: Vec = coin_args + .into_iter() + .enumerate() + .map(|(idx, arg)| context.by_value_arg(CommandKind::MergeCoins, idx + 1, arg)) + .collect::>()?; + let mut input_infos = vec![]; + for (idx, coin) in coins.into_iter().enumerate() { + if target.type_ != coin.type_ { + let e = ExecutionErrorKind::command_argument_error( + CommandArgumentError::TypeMismatch, + (idx + 1) as u16, + ); + let msg = "Coins do not have the same type".to_owned(); + return Err(ExecutionError::new_with_source(e, msg)); + } + let ObjectContents::Coin(Coin { id, balance }) = coin.contents else { + invariant_violation!( + "Target coin was a coin, and we already checked for the same type. \ + This should be a coin" + ); + }; + trace_utils::add_coin_obj_info( + trace_builder_opt, + &mut input_infos, + balance.value(), + *id.object_id(), + ); + context.delete_id(*id.object_id())?; + target_coin.add(balance)?; + } + + trace_utils::trace_merge_coins( + context, + trace_builder_opt, + &target.type_, + &input_infos, + target_coin, + )?; + + context.restore_arg::( + &mut argument_updates, + target_arg, + Value::Object(target), + )?; + vec![] + } + Command::MoveCall(move_call) => { + let ProgrammableMoveCall { + package, + module, + function, + type_arguments, + arguments, + } = *move_call; + trace_utils::trace_move_call_start(trace_builder_opt); + + let arguments = context.splat_args(0, arguments)?; + + let module = to_identifier(context, module)?; + let function = to_identifier(context, function)?; + + // Convert type arguments to `Type`s + let mut loaded_type_arguments = Vec::with_capacity(type_arguments.len()); + for (ix, type_arg) in type_arguments.into_iter().enumerate() { + let type_arg = to_type_tag(context, type_arg, ix)?; + let ty = context + .load_type(&type_arg) + .map_err(|e| context.convert_type_argument_error(ix, e))?; + loaded_type_arguments.push(ty); + } + + let original_address = context.set_link_context(package)?; + let storage_id = ModuleId::new(*package, module.clone()); + let runtime_id = ModuleId::new(original_address, module); + let return_values = execute_move_call::( + context, + &mut argument_updates, + &storage_id, + &runtime_id, + &function, + loaded_type_arguments, + arguments, + /* is_init */ false, + trace_builder_opt, + ); + + trace_utils::trace_move_call_end(trace_builder_opt); + + context.linkage_view.reset_linkage()?; + return_values? + } + Command::Publish(modules, dep_ids) => { + trace_utils::trace_publish_event(trace_builder_opt)?; + + execute_move_publish::( + context, + &mut argument_updates, + modules, + dep_ids, + trace_builder_opt, + )? + } + Command::Upgrade(modules, dep_ids, current_package_id, upgrade_ticket) => { + trace_utils::trace_upgrade_event(trace_builder_opt)?; + + let upgrade_ticket = context.one_arg(0, upgrade_ticket)?; + execute_move_upgrade::( + context, + modules, + dep_ids, + current_package_id, + upgrade_ticket, + )? + } + }; + + Mode::finish_command(context, mode_results, argument_updates, &results)?; + context.push_command_results(kind, results)?; + Ok(()) + } + + /// Execute a single Move call + fn execute_move_call( + context: &mut ExecutionContext<'_, '_, '_>, + argument_updates: &mut Mode::ArgumentUpdates, + storage_id: &ModuleId, + runtime_id: &ModuleId, + function: &IdentStr, + type_arguments: Vec, + arguments: Vec, + is_init: bool, + trace_builder_opt: &mut Option, + ) -> Result, ExecutionError> { + // check that the function is either an entry function or a valid public function + let LoadedFunctionInfo { + kind, + signature, + return_value_kinds, + index, + last_instr, + } = check_visibility_and_signature::( + context, + runtime_id, + function, + &type_arguments, + is_init, + )?; + // build the arguments, storing meta data about by-mut-ref args + let (tx_context_kind, by_mut_ref, serialized_arguments) = + build_move_args::(context, runtime_id, function, kind, &signature, &arguments)?; + // invoke the VM + let SerializedReturnValues { + mutable_reference_outputs, + return_values, + } = vm_move_call( + context, + runtime_id, + function, + type_arguments, + tx_context_kind, + serialized_arguments, + trace_builder_opt, + )?; + assert_invariant!( + by_mut_ref.len() == mutable_reference_outputs.len(), + "lost mutable input" + ); + + if context.protocol_config.relocate_event_module() { + context.take_user_events(storage_id, index, last_instr)?; + } else { + context.take_user_events(runtime_id, index, last_instr)?; + } + + // save the link context because calls to `make_value` below can set new ones, and we don't want + // it to be clobbered. + let saved_linkage = context.linkage_view.steal_linkage(); + // write back mutable inputs. We also update if they were used in non entry Move calls + // though we do not care for immutable usages of objects or other values + let used_in_non_entry_move_call = kind == FunctionKind::NonEntry; + let res = write_back_results::( + context, + argument_updates, + &arguments, + used_in_non_entry_move_call, + mutable_reference_outputs + .into_iter() + .map(|(i, bytes, _layout)| (i, bytes)), + by_mut_ref, + return_values.into_iter().map(|(bytes, _layout)| bytes), + return_value_kinds, + ); + + context.linkage_view.restore_linkage(saved_linkage)?; + res + } + + fn write_back_results( + context: &mut ExecutionContext<'_, '_, '_>, + argument_updates: &mut Mode::ArgumentUpdates, + arguments: &[Arg], + non_entry_move_call: bool, + mut_ref_values: impl IntoIterator)>, + mut_ref_kinds: impl IntoIterator, + return_values: impl IntoIterator>, + return_value_kinds: impl IntoIterator, + ) -> Result, ExecutionError> { + for ((i, bytes), (j, kind)) in mut_ref_values.into_iter().zip(mut_ref_kinds) { + assert_invariant!(i == j, "lost mutable input"); + let arg_idx = i as usize; + let value = make_value(context, kind, bytes, non_entry_move_call)?; + context.restore_arg::(argument_updates, arguments[arg_idx], value)?; + } + + return_values + .into_iter() + .zip(return_value_kinds) + .map(|(bytes, kind)| { + // only non entry functions have return values + make_value( + context, kind, bytes, /* used_in_non_entry_move_call */ true, + ) + }) + .collect() + } + + fn make_value( + context: &mut ExecutionContext<'_, '_, '_>, + value_info: ValueKind, + bytes: Vec, + used_in_non_entry_move_call: bool, + ) -> Result { + Ok(match value_info { + ValueKind::Object { + type_, + has_public_transfer, + } => Value::Object(context.make_object_value( + type_, + has_public_transfer, + used_in_non_entry_move_call, + &bytes, + )?), + ValueKind::Raw(ty, abilities) => Value::Raw( + RawValueType::Loaded { + ty, + abilities, + used_in_non_entry_move_call, + }, + bytes, + ), + }) + } + + /// Publish Move modules and call the init functions. Returns an `UpgradeCap` for the newly + /// published package on success. + fn execute_move_publish( + context: &mut ExecutionContext<'_, '_, '_>, + argument_updates: &mut Mode::ArgumentUpdates, + module_bytes: Vec>, + dep_ids: Vec, + trace_builder_opt: &mut Option, + ) -> Result, ExecutionError> { + assert_invariant!( + !module_bytes.is_empty(), + "empty package is checked in transaction input checker" + ); + context + .gas_charger + .charge_publish_package(module_bytes.iter().map(|v| v.len()).sum())?; + + let mut modules = context.deserialize_modules(&module_bytes)?; + + // It should be fine that this does not go through ExecutionContext::fresh_id since the Move + // runtime does not to know about new packages created, since Move objects and Move packages + // cannot interact + let runtime_id = if Mode::packages_are_predefined() { + // do not calculate or substitute id for predefined packages + (*modules[0].self_id().address()).into() + } else { + let id = context.tx_context.borrow_mut().fresh_id(); + substitute_package_id(&mut modules, id)?; + id + }; + + // For newly published packages, runtime ID matches storage ID. + let storage_id = runtime_id; + let dependencies = fetch_packages(&context.state_view, &dep_ids)?; + let package = + context.new_package(&modules, dependencies.iter().map(|p| p.move_package()))?; + + // Here we optimistically push the package that is being published/upgraded + // and if there is an error of any kind (verification or module init) we + // remove it. + // The call to `pop_last_package` later is fine because we cannot re-enter and + // the last package we pushed is the one we are verifying and running the init from + context.linkage_view.set_linkage(&package)?; + context.write_package(package); + let res = publish_and_verify_modules(context, runtime_id, &modules).and_then(|_| { + init_modules::(context, argument_updates, &modules, trace_builder_opt) + }); + context.linkage_view.reset_linkage()?; + if res.is_err() { + context.pop_package(); + } + res?; + + let values = if Mode::packages_are_predefined() { + // no upgrade cap for genesis modules + vec![] + } else { + let cap = &UpgradeCap::new(context.fresh_id()?, storage_id); + vec![Value::Object(context.make_object_value( + UpgradeCap::type_().into(), + /* has_public_transfer */ true, + /* used_in_non_entry_move_call */ false, + &bcs::to_bytes(cap).unwrap(), + )?)] + }; + Ok(values) + } + + /// Upgrade a Move package. Returns an `UpgradeReceipt` for the upgraded package on success. + fn execute_move_upgrade( + context: &mut ExecutionContext<'_, '_, '_>, + module_bytes: Vec>, + dep_ids: Vec, + current_package_id: ObjectID, + upgrade_ticket_arg: Arg, + ) -> Result, ExecutionError> { + assert_invariant!( + !module_bytes.is_empty(), + "empty package is checked in transaction input checker" + ); + context + .gas_charger + .charge_upgrade_package(module_bytes.iter().map(|v| v.len()).sum())?; + + let upgrade_ticket_type = context + .load_type_from_struct(&UpgradeTicket::type_()) + .map_err(|e| context.convert_vm_error(e))?; + let upgrade_receipt_type = context + .load_type_from_struct(&UpgradeReceipt::type_()) + .map_err(|e| context.convert_vm_error(e))?; + + let upgrade_ticket: UpgradeTicket = { + let mut ticket_bytes = Vec::new(); + let ticket_val: Value = + context.by_value_arg(CommandKind::Upgrade, 0, upgrade_ticket_arg)?; + check_param_type::(context, 0, &ticket_val, &upgrade_ticket_type)?; + let bound = + amplification_bound::(context, &upgrade_ticket_type, &OnceCell::new())?; + ticket_val + .write_bcs_bytes(&mut ticket_bytes, bound.map(|b| context.size_bound_raw(b)))?; + bcs::from_bytes(&ticket_bytes).map_err(|_| { + ExecutionError::from_kind(ExecutionErrorKind::CommandArgumentError { + arg_idx: 0, + kind: CommandArgumentError::InvalidBCSBytes, + }) + })? + }; + + // Make sure the passed-in package ID matches the package ID in the `upgrade_ticket`. + if current_package_id != upgrade_ticket.package.bytes { + return Err(ExecutionError::from_kind( + ExecutionErrorKind::PackageUpgradeError { + upgrade_error: PackageUpgradeError::PackageIDDoesNotMatch { + package_id: current_package_id, + ticket_id: upgrade_ticket.package.bytes, + }, + }, + )); + } + + // Check digest. + let hash_modules = true; + let computed_digest = + MovePackage::compute_digest_for_modules_and_deps(&module_bytes, &dep_ids, hash_modules) + .to_vec(); + if computed_digest != upgrade_ticket.digest { + return Err(ExecutionError::from_kind( + ExecutionErrorKind::PackageUpgradeError { + upgrade_error: PackageUpgradeError::DigestDoesNotMatch { + digest: computed_digest, + }, + }, + )); + } + + // Check that this package ID points to a package and get the package we're upgrading. + let current_package = fetch_package(&context.state_view, &upgrade_ticket.package.bytes)?; + + let mut modules = context.deserialize_modules(&module_bytes)?; + let runtime_id = current_package.move_package().original_package_id(); + substitute_package_id(&mut modules, runtime_id)?; + + // Upgraded packages share their predecessor's runtime ID but get a new storage ID. + let storage_id = context.tx_context.borrow_mut().fresh_id(); + + let dependencies = fetch_packages(&context.state_view, &dep_ids)?; + let package = context.upgrade_package( + storage_id, + current_package.move_package(), + &modules, + dependencies.iter().map(|p| p.move_package()), + )?; + + context.linkage_view.set_linkage(&package)?; + let res = publish_and_verify_modules(context, runtime_id, &modules); + context.linkage_view.reset_linkage()?; + res?; + + check_compatibility( + context.protocol_config, + current_package.move_package(), + &modules, + upgrade_ticket.policy, + )?; + if context.protocol_config.check_for_init_during_upgrade() { + // find newly added modules to the package, + // and error if they have init functions + let current_module_names: BTreeSet<&str> = current_package + .move_package() + .serialized_module_map() + .keys() + .map(|s| s.as_str()) + .collect(); + let upgrade_module_names: BTreeSet<&str> = package + .serialized_module_map() + .keys() + .map(|s| s.as_str()) + .collect(); + let new_module_names = upgrade_module_names + .difference(¤t_module_names) + .copied() + .collect::>(); + let new_modules = modules + .iter() + .filter(|m| { + let name = m.identifier_at(m.self_handle().name).as_str(); + new_module_names.contains(name) + }) + .collect::>(); + let new_module_has_init = new_modules.iter().any(|module| { + module.function_defs.iter().any(|fdef| { + let fhandle = module.function_handle_at(fdef.function); + let fname = module.identifier_at(fhandle.name); + fname == INIT_FN_NAME + }) + }); + if new_module_has_init { + // TODO we cannot run 'init' on upgrade yet due to global type cache limitations + return Err(ExecutionError::new_with_source( + ExecutionErrorKind::FeatureNotYetSupported, + "`init` in new modules on upgrade is not yet supported", + )); + } + } + + context.write_package(package); + Ok(vec![Value::Raw( + RawValueType::Loaded { + ty: upgrade_receipt_type, + abilities: AbilitySet::EMPTY, + used_in_non_entry_move_call: false, + }, + bcs::to_bytes(&UpgradeReceipt::new(upgrade_ticket, storage_id)).unwrap(), + )]) + } + + pub fn check_compatibility( + protocol_config: &ProtocolConfig, + existing_package: &MovePackage, + upgrading_modules: &[CompiledModule], + policy: u8, + ) -> Result<(), ExecutionError> { + // Make sure this is a known upgrade policy. + let Ok(policy) = UpgradePolicy::try_from(policy) else { + return Err(ExecutionError::from_kind( + ExecutionErrorKind::PackageUpgradeError { + upgrade_error: PackageUpgradeError::UnknownUpgradePolicy { policy }, + }, + )); + }; + + let pool = &mut normalized::RcPool::new(); + let binary_config = protocol_config.binary_config(None); + let Ok(current_normalized) = + existing_package.normalize(pool, &binary_config, /* include code */ true) + else { + invariant_violation!("Tried to normalize modules in existing package but failed") + }; + + let existing_modules_len = current_normalized.len(); + let upgrading_modules_len = upgrading_modules.len(); + let disallow_new_modules = protocol_config.disallow_new_modules_in_deps_only_packages() + && policy as u8 == UpgradePolicy::DEP_ONLY; + + if disallow_new_modules && existing_modules_len != upgrading_modules_len { + return Err(ExecutionError::new_with_source( + ExecutionErrorKind::PackageUpgradeError { + upgrade_error: PackageUpgradeError::IncompatibleUpgrade, + }, + format!( + "Existing package has {existing_modules_len} modules, but new package has \ + {upgrading_modules_len}. Adding or removing a module to a deps only package is not allowed." + ), + )); + } + + let mut new_normalized = normalize_deserialized_modules( + pool, + upgrading_modules.iter(), + /* include code */ true, + ); + for (name, cur_module) in current_normalized { + let Some(new_module) = new_normalized.remove(&name) else { + return Err(ExecutionError::new_with_source( + ExecutionErrorKind::PackageUpgradeError { + upgrade_error: PackageUpgradeError::IncompatibleUpgrade, + }, + format!("Existing module {name} not found in next version of package"), + )); + }; + + check_module_compatibility(&policy, &cur_module, &new_module)?; + } + + // If we disallow new modules double check that there are no modules left in `new_normalized`. + debug_assert!(!disallow_new_modules || new_normalized.is_empty()); + + Ok(()) + } + + fn check_module_compatibility( + policy: &UpgradePolicy, + cur_module: &move_binary_format::compatibility::Module, + new_module: &move_binary_format::compatibility::Module, + ) -> Result<(), ExecutionError> { + match policy { + UpgradePolicy::Additive => InclusionCheck::Subset.check(cur_module, new_module), + UpgradePolicy::DepOnly => InclusionCheck::Equal.check(cur_module, new_module), + UpgradePolicy::Compatible => { + let compatibility = Compatibility::upgrade_check(); + + compatibility.check(cur_module, new_module) + } + } + .map_err(|e| { + ExecutionError::new_with_source( + ExecutionErrorKind::PackageUpgradeError { + upgrade_error: PackageUpgradeError::IncompatibleUpgrade, + }, + e, + ) + }) + } + + pub fn fetch_package( + state_view: &impl BackingPackageStore, + package_id: &ObjectID, + ) -> Result { + let mut fetched_packages = fetch_packages(state_view, vec![package_id])?; + assert_invariant!( + fetched_packages.len() == 1, + "Number of fetched packages must match the number of package object IDs if successful." + ); + match fetched_packages.pop() { + Some(pkg) => Ok(pkg), + None => invariant_violation!( + "We should always fetch a package for each object or return a dependency error." + ), + } + } + + pub fn fetch_packages<'ctx, 'state>( + state_view: &'state impl BackingPackageStore, + package_ids: impl IntoIterator, + ) -> Result, ExecutionError> { + let package_ids: BTreeSet<_> = package_ids.into_iter().collect(); + match get_package_objects(state_view, package_ids) { + Err(e) => Err(ExecutionError::new_with_source( + ExecutionErrorKind::PublishUpgradeMissingDependency, + e, + )), + Ok(Err(missing_deps)) => { + let msg = format!( + "Missing dependencies: {}", + missing_deps + .into_iter() + .map(|dep| format!("{}", dep)) + .collect::>() + .join(", ") + ); + Err(ExecutionError::new_with_source( + ExecutionErrorKind::PublishUpgradeMissingDependency, + msg, + )) + } + Ok(Ok(pkgs)) => Ok(pkgs), + } + } + + /*************************************************************************************************** + * Move execution + **************************************************************************************************/ + + fn vm_move_call( + context: &mut ExecutionContext<'_, '_, '_>, + module_id: &ModuleId, + function: &IdentStr, + type_arguments: Vec, + tx_context_kind: TxContextKind, + mut serialized_arguments: Vec>, + trace_builder_opt: &mut Option, + ) -> Result { + match tx_context_kind { + TxContextKind::None => (), + TxContextKind::Mutable | TxContextKind::Immutable => { + serialized_arguments.push(context.tx_context.borrow().to_bcs_legacy_context()); + } + } + // script visibility checked manually for entry points + let mut result = context + .execute_function_bypass_visibility( + module_id, + function, + type_arguments, + serialized_arguments, + trace_builder_opt, + ) + .map_err(|e| context.convert_vm_error(e))?; + + // When this function is used during publishing, it + // may be executed several times, with objects being + // created in the Move VM in each Move call. In such + // case, we need to update TxContext value so that it + // reflects what happened each time we call into the + // Move VM (e.g. to account for the number of created + // objects). + if tx_context_kind == TxContextKind::Mutable { + let Some((_, ctx_bytes, _)) = result.mutable_reference_outputs.pop() else { + invariant_violation!("Missing TxContext in reference outputs"); + }; + let updated_ctx: MoveLegacyTxContext = bcs::from_bytes(&ctx_bytes).map_err(|e| { + ExecutionError::invariant_violation(format!( + "Unable to deserialize TxContext bytes. {e}" + )) + })?; + context.tx_context.borrow_mut().update_state(updated_ctx)?; + } + Ok(result) + } + + fn publish_and_verify_modules( + context: &mut ExecutionContext<'_, '_, '_>, + package_id: ObjectID, + modules: &[CompiledModule], + ) -> Result<(), ExecutionError> { + // TODO(https://github.com/MystenLabs/sui/issues/69): avoid this redundant serialization by exposing VM API that allows us to run the linker directly on `Vec` + let binary_version = context.protocol_config.move_binary_format_version(); + let new_module_bytes: Vec<_> = modules + .iter() + .map(|m| { + let mut bytes = Vec::new(); + let version = if binary_version > VERSION_6 { + m.version + } else { + VERSION_6 + }; + m.serialize_with_version(version, &mut bytes).unwrap(); + bytes + }) + .collect(); + context + .publish_module_bundle(new_module_bytes, AccountAddress::from(package_id)) + .map_err(|e| context.convert_vm_error(e))?; + + // run the Sui verifier + for module in modules { + // Run Sui bytecode verifier, which runs some additional checks that assume the Move + // bytecode verifier has passed. + sui_verifier::verifier::sui_verify_module_unmetered( + module, + &BTreeMap::new(), + &context + .protocol_config + .verifier_config(/* signing_limits */ None), + )?; + } + + Ok(()) + } + + fn init_modules<'a, Mode: ExecutionMode>( + context: &mut ExecutionContext<'_, '_, '_>, + argument_updates: &mut Mode::ArgumentUpdates, + modules: impl IntoIterator, + trace_builder_opt: &mut Option, + ) -> Result<(), ExecutionError> { + let modules_to_init = modules.into_iter().filter_map(|module| { + for fdef in &module.function_defs { + let fhandle = module.function_handle_at(fdef.function); + let fname = module.identifier_at(fhandle.name); + if fname == INIT_FN_NAME { + return Some(module.self_id()); + } + } + None + }); + + for module_id in modules_to_init { + trace_utils::trace_move_call_start(trace_builder_opt); + let return_values = execute_move_call::( + context, + argument_updates, + // `init` is currently only called on packages when they are published for the + // first time, meaning their runtime and storage IDs match. If this were to change + // for some reason, then we would need to perform relocation here. + &module_id, + &module_id, + INIT_FN_NAME, + vec![], + vec![], + /* is_init */ true, + trace_builder_opt, + )?; + + assert_invariant!( + return_values.is_empty(), + "init should not have return values" + ); + + trace_utils::trace_move_call_end(trace_builder_opt); + } + + Ok(()) + } + + /*************************************************************************************************** + * Move signatures + **************************************************************************************************/ + + /// Helper marking what function we are invoking + #[derive(PartialEq, Eq, Clone, Copy)] + enum FunctionKind { + PrivateEntry, + PublicEntry, + NonEntry, + Init, + } + + /// Used to remember type information about a type when resolving the signature + enum ValueKind { + Object { + type_: MoveObjectType, + has_public_transfer: bool, + }, + Raw(Type, AbilitySet), + } + + struct LoadedFunctionInfo { + /// The kind of the function, e.g. public or private or init + kind: FunctionKind, + /// The signature information of the function + signature: LoadedFunctionInstantiation, + /// Object or type information for the return values + return_value_kinds: Vec, + /// Definition index of the function + index: FunctionDefinitionIndex, + /// The length of the function used for setting error information, or 0 if native + last_instr: CodeOffset, + } + + /// Checks that the function to be called is either + /// - an entry function + /// - a public function that does not return references + /// - module init (only internal usage) + fn check_visibility_and_signature( + context: &mut ExecutionContext<'_, '_, '_>, + module_id: &ModuleId, + function: &IdentStr, + type_arguments: &[Type], + from_init: bool, + ) -> Result { + if from_init { + let result = context.load_function(module_id, function, type_arguments); + assert_invariant!( + result.is_ok(), + "The modules init should be able to be loaded" + ); + } + let no_new_packages = vec![]; + let data_store = SuiDataStore::new(&context.linkage_view, &no_new_packages); + let module = context + .vm + .get_runtime() + .load_module(module_id, &data_store) + .map_err(|e| context.convert_vm_error(e))?; + let Some((index, fdef)) = module + .function_defs + .iter() + .enumerate() + .find(|(_index, fdef)| { + module.identifier_at(module.function_handle_at(fdef.function).name) == function + }) + else { + return Err(ExecutionError::new_with_source( + ExecutionErrorKind::FunctionNotFound, + format!( + "Could not resolve function '{}' in module {}", + function, &module_id, + ), + )); + }; + + // entry on init is now banned, so ban invoking it + if !from_init && function == INIT_FN_NAME && context.protocol_config.ban_entry_init() { + return Err(ExecutionError::new_with_source( + ExecutionErrorKind::NonEntryFunctionInvoked, + "Cannot call 'init'", + )); + } + + let last_instr: CodeOffset = fdef + .code + .as_ref() + .map(|code| code.code.len() - 1) + .unwrap_or(0) as CodeOffset; + let function_kind = match (fdef.visibility, fdef.is_entry) { + (Visibility::Private | Visibility::Friend, true) => FunctionKind::PrivateEntry, + (Visibility::Public, true) => FunctionKind::PublicEntry, + (Visibility::Public, false) => FunctionKind::NonEntry, + (Visibility::Private, false) if from_init => { + assert_invariant!( + function == INIT_FN_NAME, + "module init specified non-init function" + ); + FunctionKind::Init + } + (Visibility::Private | Visibility::Friend, false) + if Mode::allow_arbitrary_function_calls() => + { + FunctionKind::NonEntry + } + (Visibility::Private | Visibility::Friend, false) => { + return Err(ExecutionError::new_with_source( + ExecutionErrorKind::NonEntryFunctionInvoked, + "Can only call `entry` or `public` functions", + )); + } + }; + let signature = context + .load_function(module_id, function, type_arguments) + .map_err(|e| context.convert_vm_error(e))?; + let signature = + subst_signature(signature, type_arguments).map_err(|e| context.convert_vm_error(e))?; + let return_value_kinds = match function_kind { + FunctionKind::Init => { + assert_invariant!( + signature.return_.is_empty(), + "init functions must have no return values" + ); + vec![] + } + FunctionKind::PrivateEntry | FunctionKind::PublicEntry | FunctionKind::NonEntry => { + check_non_entry_signature::(context, module_id, function, &signature)? + } + }; + if context.protocol_config.private_generics_verifier_v2() { + check_private_generics_v2(module_id, function)?; + } else { + check_private_generics(module_id, function)?; + } + Ok(LoadedFunctionInfo { + kind: function_kind, + signature, + return_value_kinds, + index: FunctionDefinitionIndex(index as u16), + last_instr, + }) + } + + /// substitutes the type arguments into the parameter and return types + pub fn subst_signature( + signature: LoadedFunctionInstantiation, + type_arguments: &[Type], + ) -> VMResult { + let LoadedFunctionInstantiation { + parameters, + return_, + instruction_length, + definition_index, + } = signature; + let parameters = parameters + .into_iter() + .map(|ty| ty.subst(type_arguments)) + .collect::>>() + .map_err(|err| err.finish(Location::Undefined))?; + let return_ = return_ + .into_iter() + .map(|ty| ty.subst(type_arguments)) + .collect::>>() + .map_err(|err| err.finish(Location::Undefined))?; + Ok(LoadedFunctionInstantiation { + parameters, + return_, + instruction_length, + definition_index, + }) + } + + /// Checks that the non-entry function does not return references. And marks the return values + /// as object or non-object return values + fn check_non_entry_signature( + context: &mut ExecutionContext<'_, '_, '_>, + _module_id: &ModuleId, + _function: &IdentStr, + signature: &LoadedFunctionInstantiation, + ) -> Result, ExecutionError> { + signature + .return_ + .iter() + .enumerate() + .map(|(idx, return_type)| { + let return_type = match return_type { + // for dev-inspect, just dereference the value + Type::Reference(inner) | Type::MutableReference(inner) + if Mode::allow_arbitrary_values() => + { + inner + } + Type::Reference(_) | Type::MutableReference(_) => { + return Err(ExecutionError::from_kind( + ExecutionErrorKind::InvalidPublicFunctionReturnType { idx: idx as u16 }, + )); + } + t => t, + }; + let abilities = context.get_type_abilities(return_type)?; + Ok(match return_type { + Type::MutableReference(_) | Type::Reference(_) => unreachable!(), + Type::TyParam(_) => { + invariant_violation!("TyParam should have been substituted") + } + Type::Datatype(_) | Type::DatatypeInstantiation(_) if abilities.has_key() => { + let type_tag = context + .vm + .get_runtime() + .get_type_tag(return_type) + .map_err(|e| context.convert_vm_error(e))?; + let TypeTag::Struct(struct_tag) = type_tag else { + invariant_violation!("Struct type make a non struct type tag") + }; + ValueKind::Object { + type_: MoveObjectType::from(*struct_tag), + has_public_transfer: abilities.has_store(), + } + } + Type::Datatype(_) + | Type::DatatypeInstantiation(_) + | Type::Bool + | Type::U8 + | Type::U64 + | Type::U128 + | Type::Address + | Type::Signer + | Type::Vector(_) + | Type::U16 + | Type::U32 + | Type::U256 => ValueKind::Raw(return_type.clone(), abilities), + }) + }) + .collect() + } + + pub fn check_private_generics( + module_id: &ModuleId, + function: &IdentStr, + ) -> Result<(), ExecutionError> { + let module_ident = (module_id.address(), module_id.name()); + if module_ident == (&SUI_FRAMEWORK_ADDRESS, EVENT_MODULE) { + return Err(ExecutionError::new_with_source( + ExecutionErrorKind::NonEntryFunctionInvoked, + format!("Cannot directly call functions in sui::{}", EVENT_MODULE), + )); + } + + if module_ident == (&SUI_FRAMEWORK_ADDRESS, TRANSFER_MODULE) + && PRIVATE_TRANSFER_FUNCTIONS.contains(&function) + { + let msg = format!( + "Cannot directly call sui::{m}::{f}. \ + Use the public variant instead, sui::{m}::public_{f}", + m = TRANSFER_MODULE, + f = function + ); + return Err(ExecutionError::new_with_source( + ExecutionErrorKind::NonEntryFunctionInvoked, + msg, + )); + } + + Ok(()) + } + + pub fn check_private_generics_v2( + callee_package: &ModuleId, + callee_function: &IdentStr, + ) -> Result<(), ExecutionError> { + let callee_address = *callee_package.address(); + let callee_module = callee_package.name(); + let callee = (callee_address, callee_module, callee_function); + let Some((_f, internal_type_parameters)) = private_generics_verifier_v2::FUNCTIONS_TO_CHECK + .iter() + .find(|(f, _)| &callee == f) + else { + return Ok(()); + }; + // If we find an internal type parameter, the call is automatically invalid--since we + // are not in a module and cannot define any types to satisfy the internal constraint. + let Some((internal_idx, _)) = internal_type_parameters + .iter() + .enumerate() + .find(|(_, is_internal)| **is_internal) + else { + // No `internal` type parameters, so it is ok to call + return Ok(()); + }; + let callee_package_name = + private_generics_verifier_v2::callee_package_name(&callee_address); + let help = private_generics_verifier_v2::help_message( + &callee_address, + callee_module, + callee_function, + ); + let msg = format!( + "Cannot directly call function '{}::{}::{}' since type parameter #{} can \ + only be instantiated with types defined within the caller's module.{}", + callee_package_name, callee_module, callee_function, internal_idx, help, + ); + Err(ExecutionError::new_with_source( + ExecutionErrorKind::NonEntryFunctionInvoked, + msg, + )) + } + + type ArgInfo = ( + TxContextKind, + /* mut ref */ + Vec<(LocalIndex, ValueKind)>, + Vec>, + ); + + /// Serializes the arguments into BCS values for Move. Performs the necessary type checking for + /// each value + fn build_move_args( + context: &mut ExecutionContext<'_, '_, '_>, + _module_id: &ModuleId, + function: &IdentStr, + function_kind: FunctionKind, + signature: &LoadedFunctionInstantiation, + args: &[Arg], + ) -> Result { + // check the arity + let parameters = &signature.parameters; + let tx_ctx_kind = match parameters.last() { + Some(t) => is_tx_context(context, t)?, + None => TxContextKind::None, + }; + // an init function can have one or two arguments, with the last one always being of type + // &mut TxContext and the additional (first) one representing a one time witness type (see + // one_time_witness verifier pass for additional explanation) + let has_one_time_witness = function_kind == FunctionKind::Init && parameters.len() == 2; + let has_tx_context = tx_ctx_kind != TxContextKind::None; + let num_args = args.len() + (has_one_time_witness as usize) + (has_tx_context as usize); + if num_args != parameters.len() { + return Err(ExecutionError::new_with_source( + ExecutionErrorKind::ArityMismatch, + format!( + "Expected {:?} argument{} calling function '{}', but found {:?}", + parameters.len(), + if parameters.len() == 1 { "" } else { "s" }, + function, + num_args + ), + )); + } + + // check the types and remember which are by mutable ref + let mut by_mut_ref = vec![]; + let mut serialized_args = Vec::with_capacity(num_args); + // an init function can have one or two arguments, with the last one always being of type + // &mut TxContext and the additional (first) one representing a one time witness type (see + // one_time_witness verifier pass for additional explanation) + if has_one_time_witness { + // one time witness type is a struct with a single bool filed which in bcs is encoded as + // 0x01 + let bcs_true_value = bcs::to_bytes(&true).unwrap(); + serialized_args.push(bcs_true_value) + } + for ((idx, arg), param_ty) in args.iter().copied().enumerate().zip(parameters) { + let (value, non_ref_param_ty): (Value, &Type) = match param_ty { + Type::MutableReference(inner) => { + let value = context.borrow_arg_mut(idx, arg)?; + let object_info = if let Value::Object(ObjectValue { + type_, + has_public_transfer, + .. + }) = &value + { + let type_tag = context + .vm + .get_runtime() + .get_type_tag(type_) + .map_err(|e| context.convert_vm_error(e))?; + let TypeTag::Struct(struct_tag) = type_tag else { + invariant_violation!("Struct type make a non struct type tag") + }; + let type_ = (*struct_tag).into(); + ValueKind::Object { + type_, + has_public_transfer: *has_public_transfer, + } + } else { + let abilities = context.get_type_abilities(inner)?; + ValueKind::Raw((**inner).clone(), abilities) + }; + by_mut_ref.push((idx as LocalIndex, object_info)); + (value, inner) + } + Type::Reference(inner) => (context.borrow_arg(idx, arg, param_ty)?, inner), + t => { + let value = context.by_value_arg(CommandKind::MoveCall, idx, arg)?; + (value, t) + } + }; + if matches!( + function_kind, + FunctionKind::PrivateEntry | FunctionKind::Init + ) && value.was_used_in_non_entry_move_call() + { + return Err(command_argument_error( + CommandArgumentError::InvalidArgumentToPrivateEntryFunction, + idx, + )); + } + check_param_type::(context, idx, &value, non_ref_param_ty)?; + let bytes = { + let mut v = vec![]; + value.write_bcs_bytes(&mut v, None)?; + v + }; + serialized_args.push(bytes); + } + Ok((tx_ctx_kind, by_mut_ref, serialized_args)) + } + + /// checks that the value is compatible with the specified type + fn check_param_type( + context: &mut ExecutionContext<'_, '_, '_>, + idx: usize, + value: &Value, + param_ty: &Type, + ) -> Result<(), ExecutionError> { + match value { + // For dev-spect, allow any BCS bytes. This does mean internal invariants for types can + // be violated (like for string or Option) + Value::Raw(RawValueType::Any, bytes) if Mode::allow_arbitrary_values() => { + if let Some(bound) = amplification_bound_::(context, param_ty)? { + let bound = context.size_bound_raw(bound); + return ensure_serialized_size(bytes.len() as u64, bound); + } else { + return Ok(()); + } + } + // Any means this was just some bytes passed in as an argument (as opposed to being + // generated from a Move function). Meaning we only allow "primitive" values + // and might need to run validation in addition to the BCS layout + Value::Raw(RawValueType::Any, bytes) => { + let Some(layout) = primitive_serialization_layout(context, param_ty)? else { + let msg = format!( + "Non-primitive argument at index {}. If it is an object, it must be \ + populated by an object", + idx, + ); + return Err(ExecutionError::new_with_source( + ExecutionErrorKind::command_argument_error( + CommandArgumentError::InvalidUsageOfPureArg, + idx as u16, + ), + msg, + )); + }; + bcs_argument_validate(bytes, idx as u16, layout)?; + return Ok(()); + } + Value::Raw(RawValueType::Loaded { ty, abilities, .. }, _) => { + assert_invariant!( + Mode::allow_arbitrary_values() || !abilities.has_key(), + "Raw value should never be an object" + ); + if ty != param_ty { + return Err(command_argument_error( + CommandArgumentError::TypeMismatch, + idx, + )); + } + } + Value::Object(obj) => { + let ty = &obj.type_; + if ty != param_ty { + return Err(command_argument_error( + CommandArgumentError::TypeMismatch, + idx, + )); + } + } + Value::Receiving(_, _, assigned_type) => { + // If the type has been fixed, make sure the types match up + if let Some(assigned_type) = assigned_type + && assigned_type != param_ty + { + return Err(command_argument_error( + CommandArgumentError::TypeMismatch, + idx, + )); + } + + // Now make sure the param type is a struct instantiation of the receiving struct + let Type::DatatypeInstantiation(inst) = param_ty else { + return Err(command_argument_error( + CommandArgumentError::TypeMismatch, + idx, + )); + }; + let (sidx, targs) = &**inst; + let Some(s) = context.vm.get_runtime().get_type(*sidx) else { + invariant_violation!("sui::transfer::Receiving struct not found in session") + }; + let resolved_struct = get_datatype_ident(&s); + + if resolved_struct != RESOLVED_RECEIVING_STRUCT || targs.len() != 1 { + return Err(command_argument_error( + CommandArgumentError::TypeMismatch, + idx, + )); + } + } + } + Ok(()) + } + + fn to_identifier( + context: &mut ExecutionContext<'_, '_, '_>, + ident: String, + ) -> Result { + if context.protocol_config.validate_identifier_inputs() { + Identifier::new(ident).map_err(|e| { + ExecutionError::new_with_source( + ExecutionErrorKind::VMInvariantViolation, + e.to_string(), + ) + }) + } else { + // SAFETY: Preserving existing behaviour for identifier deserialization. + Ok(unsafe { Identifier::new_unchecked(ident) }) + } + } + + // Convert a type input which may refer to a type by multiple different IDs and convert it to a + // TypeTag that only uses defining IDs. + // + // It's suboptimal to traverse the type, load, and then go back to a typetag to resolve to + // defining IDs in the typetag, but it's the cleanest solution ATM without adding in additional + // machinery. With the new linkage resolution that we will be adding this will + // be much cleaner however, we'll hold off on adding that in here, and instead add it in the + // new execution code. + fn to_type_tag( + context: &mut ExecutionContext<'_, '_, '_>, + type_input: TypeInput, + idx: usize, + ) -> Result { + let type_tag_no_def_ids = to_type_tag_(context, type_input, idx)?; + if context + .protocol_config + .resolve_type_input_ids_to_defining_id() + { + let ix = if context + .protocol_config + .better_adapter_type_resolution_errors() + { + idx + } else { + 0 + }; + + let ty = context + .load_type(&type_tag_no_def_ids) + .map_err(|e| context.convert_type_argument_error(ix, e))?; + context.get_type_tag(&ty) + } else { + Ok(type_tag_no_def_ids) + } + } + + fn to_type_tag_( + context: &mut ExecutionContext<'_, '_, '_>, + type_input: TypeInput, + idx: usize, + ) -> Result { + use TypeInput as I; + use TypeTag as T; + Ok(match type_input { + I::Bool => T::Bool, + I::U8 => T::U8, + I::U16 => T::U16, + I::U32 => T::U32, + I::U64 => T::U64, + I::U128 => T::U128, + I::U256 => T::U256, + I::Address => T::Address, + I::Signer => T::Signer, + I::Vector(t) => T::Vector(Box::new(to_type_tag_(context, *t, idx)?)), + I::Struct(s) => { + let StructInput { + address, + module, + name, + type_params, + } = *s; + let type_params = type_params + .into_iter() + .map(|t| to_type_tag_(context, t, idx)) + .collect::>()?; + let (module, name) = resolve_datatype_names(context, address, module, name, idx)?; + T::Struct(Box::new(StructTag { + address, + module, + name, + type_params, + })) + } + }) + } + + fn resolve_datatype_names( + context: &ExecutionContext<'_, '_, '_>, + addr: AccountAddress, + module: String, + name: String, + idx: usize, + ) -> Result<(Identifier, Identifier), ExecutionError> { + let validate_identifiers = context.protocol_config.validate_identifier_inputs(); + let better_resolution_errors = context + .protocol_config + .better_adapter_type_resolution_errors(); + + let to_ident = |s| { + if validate_identifiers { + Identifier::new(s).map_err(|e| { + ExecutionError::new_with_source( + ExecutionErrorKind::VMInvariantViolation, + e.to_string(), + ) + }) + } else { + // SAFETY: Preserving existing behaviour for identifier deserialization within type + // tags and inputs. + unsafe { Ok(Identifier::new_unchecked(s)) } + } + }; + + let module_ident = to_ident(module.clone())?; + let name_ident = to_ident(name.clone())?; + + if better_resolution_errors + && context + .linkage_view + .get_package(&addr.into()) + .ok() + .flatten() + .is_none_or(|pkg| !pkg.type_origin_map().contains_key(&(module, name))) + { + return Err(ExecutionError::from_kind( + ExecutionErrorKind::TypeArgumentError { + argument_idx: idx as u16, + kind: TypeArgumentError::TypeNotFound, + }, + )); + } + + Ok((module_ident, name_ident)) + } + + fn get_datatype_ident(s: &CachedDatatype) -> (&AccountAddress, &IdentStr, &IdentStr) { + let module_id = &s.defining_id; + let struct_name = &s.name; + ( + module_id.address(), + module_id.name(), + struct_name.as_ident_str(), + ) + } + + // Returns Some(kind) if the type is a reference to the TxnContext. kind being Mutable with + // a MutableReference, and Immutable otherwise. + // Returns None for all other types + pub fn is_tx_context( + context: &mut ExecutionContext<'_, '_, '_>, + t: &Type, + ) -> Result { + let (is_mut, inner) = match t { + Type::MutableReference(inner) => (true, inner), + Type::Reference(inner) => (false, inner), + _ => return Ok(TxContextKind::None), + }; + let Type::Datatype(idx) = &**inner else { + return Ok(TxContextKind::None); + }; + let Some(s) = context.vm.get_runtime().get_type(*idx) else { + invariant_violation!("Loaded struct not found") + }; + let (module_addr, module_name, struct_name) = get_datatype_ident(&s); + let is_tx_context_type = module_addr == &SUI_FRAMEWORK_ADDRESS + && module_name == TX_CONTEXT_MODULE_NAME + && struct_name == TX_CONTEXT_STRUCT_NAME; + Ok(if is_tx_context_type { + if is_mut { + TxContextKind::Mutable + } else { + TxContextKind::Immutable + } + } else { + TxContextKind::None + }) + } + + /// Returns Some(layout) iff it is a primitive, an ID, a String, or an option/vector of a valid type + fn primitive_serialization_layout( + context: &mut ExecutionContext<'_, '_, '_>, + param_ty: &Type, + ) -> Result, ExecutionError> { + Ok(match param_ty { + Type::Signer => return Ok(None), + Type::Reference(_) | Type::MutableReference(_) | Type::TyParam(_) => { + invariant_violation!("references and type parameters should be checked elsewhere") + } + Type::Bool => Some(PrimitiveArgumentLayout::Bool), + Type::U8 => Some(PrimitiveArgumentLayout::U8), + Type::U16 => Some(PrimitiveArgumentLayout::U16), + Type::U32 => Some(PrimitiveArgumentLayout::U32), + Type::U64 => Some(PrimitiveArgumentLayout::U64), + Type::U128 => Some(PrimitiveArgumentLayout::U128), + Type::U256 => Some(PrimitiveArgumentLayout::U256), + Type::Address => Some(PrimitiveArgumentLayout::Address), + + Type::Vector(inner) => { + let info_opt = primitive_serialization_layout(context, inner)?; + info_opt.map(|layout| PrimitiveArgumentLayout::Vector(Box::new(layout))) + } + Type::DatatypeInstantiation(inst) => { + let (idx, targs) = &**inst; + let Some(s) = context.vm.get_runtime().get_type(*idx) else { + invariant_violation!("Loaded struct not found") + }; + let resolved_struct = get_datatype_ident(&s); + // is option of a string + if resolved_struct == RESOLVED_STD_OPTION && targs.len() == 1 { + let info_opt = primitive_serialization_layout(context, &targs[0])?; + info_opt.map(|layout| PrimitiveArgumentLayout::Option(Box::new(layout))) + } else { + None + } + } + Type::Datatype(idx) => { + let Some(s) = context.vm.get_runtime().get_type(*idx) else { + invariant_violation!("Loaded struct not found") + }; + let resolved_struct = get_datatype_ident(&s); + if resolved_struct == RESOLVED_SUI_ID { + Some(PrimitiveArgumentLayout::Address) + } else if resolved_struct == RESOLVED_ASCII_STR { + Some(PrimitiveArgumentLayout::Ascii) + } else if resolved_struct == RESOLVED_UTF8_STR { + Some(PrimitiveArgumentLayout::UTF8) + } else { + None + } + } + }) + } + + // We use a `OnceCell` for two reasons. One to cache the ability set for the type so that it + // is not recomputed for each element of the vector. And two, to avoid computing the abilities + // in the case where `max_ptb_value_size_v2` is false--this removes any case of diverging + // based on the result of `get_type_abilities`. + fn amplification_bound( + context: &mut ExecutionContext<'_, '_, '_>, + param_ty: &Type, + abilities: &OnceCell, + ) -> Result, ExecutionError> { + if context.protocol_config.max_ptb_value_size_v2() { + if abilities.get().is_none() { + abilities + .set(context.get_type_abilities(param_ty)?) + .unwrap(); + } + if !abilities.get().unwrap().has_copy() { + return Ok(None); + } + } + amplification_bound_::(context, param_ty) + } + + fn amplification_bound_( + context: &mut ExecutionContext<'_, '_, '_>, + param_ty: &Type, + ) -> Result, ExecutionError> { + // Do not cap size for epoch change/genesis + if Mode::packages_are_predefined() { + return Ok(None); + } + + let Some(bound) = context.protocol_config.max_ptb_value_size_as_option() else { + return Ok(None); + }; + + fn amplification(prim_layout: &PrimitiveArgumentLayout) -> Result { + use PrimitiveArgumentLayout as PAL; + Ok(match prim_layout { + PAL::Option(inner_layout) => 1u64 + amplification(inner_layout)?, + PAL::Vector(inner_layout) => amplification(inner_layout)?, + PAL::Ascii | PAL::UTF8 => 2, + PAL::Bool | PAL::U8 | PAL::U16 | PAL::U32 | PAL::U64 => 1, + PAL::U128 | PAL::U256 | PAL::Address => 2, + }) + } + + let mut amplification = match primitive_serialization_layout(context, param_ty)? { + // No primitive type layout was able to be determined for the type. Assume the worst + // and the value is of maximal depth. + None => context.protocol_config.max_move_value_depth(), + Some(layout) => amplification(&layout)?, + }; + + // Computed amplification should never be zero + debug_assert!(amplification != 0); + // We assume here that any value that can be created must be bounded by the max move value + // depth so assert that this invariant holds. + debug_assert!( + context.protocol_config.max_move_value_depth() + >= context.protocol_config.max_type_argument_depth() as u64 + ); + assert_ne!(context.protocol_config.max_move_value_depth(), 0); + if amplification == 0 { + amplification = context.protocol_config.max_move_value_depth(); + } + Ok(Some(bound / amplification)) + } + + /*************************************************************************************************** + * Special serialization formats + **************************************************************************************************/ + + /// Special enum for values that need additional validation, in other words + /// There is validation to do on top of the BCS layout. Currently only needed for + /// strings + #[derive(Debug)] + pub enum PrimitiveArgumentLayout { + /// An option + Option(Box), + /// A vector + Vector(Box), + /// An ASCII encoded string + Ascii, + /// A UTF8 encoded string + UTF8, + // needed for Option validation + Bool, + U8, + U16, + U32, + U64, + U128, + U256, + Address, + } + + impl PrimitiveArgumentLayout { + /// returns true iff all BCS compatible bytes are actually values for this type. + /// For example, this function returns false for Option and Strings since they need additional + /// validation. + pub fn bcs_only(&self) -> bool { + match self { + // have additional restrictions past BCS + PrimitiveArgumentLayout::Option(_) + | PrimitiveArgumentLayout::Ascii + | PrimitiveArgumentLayout::UTF8 => false, + // Move primitives are BCS compatible and do not need additional validation + PrimitiveArgumentLayout::Bool + | PrimitiveArgumentLayout::U8 + | PrimitiveArgumentLayout::U16 + | PrimitiveArgumentLayout::U32 + | PrimitiveArgumentLayout::U64 + | PrimitiveArgumentLayout::U128 + | PrimitiveArgumentLayout::U256 + | PrimitiveArgumentLayout::Address => true, + // vector only needs validation if it's inner type does + PrimitiveArgumentLayout::Vector(inner) => inner.bcs_only(), + } + } + } + + /// Checks the bytes against the `SpecialArgumentLayout` using `bcs`. It does not actually generate + /// the deserialized value, only walks the bytes. While not necessary if the layout does not contain + /// special arguments (e.g. Option or String) we check the BCS bytes for predictability + pub fn bcs_argument_validate( + bytes: &[u8], + idx: u16, + layout: PrimitiveArgumentLayout, + ) -> Result<(), ExecutionError> { + bcs::from_bytes_seed(&layout, bytes).map_err(|_| { + ExecutionError::new_with_source( + ExecutionErrorKind::command_argument_error( + CommandArgumentError::InvalidBCSBytes, + idx, + ), + format!("Function expects {layout} but provided argument's value does not match",), + ) + }) + } + + impl<'d> serde::de::DeserializeSeed<'d> for &PrimitiveArgumentLayout { + type Value = (); + fn deserialize>( + self, + deserializer: D, + ) -> Result { + use serde::de::Error; + match self { + PrimitiveArgumentLayout::Ascii => { + let s: &str = serde::Deserialize::deserialize(deserializer)?; + if !s.is_ascii() { + Err(D::Error::custom("not an ascii string")) + } else { + Ok(()) + } + } + PrimitiveArgumentLayout::UTF8 => { + deserializer.deserialize_string(serde::de::IgnoredAny)?; + Ok(()) + } + PrimitiveArgumentLayout::Option(layout) => { + deserializer.deserialize_option(OptionElementVisitor(layout)) + } + PrimitiveArgumentLayout::Vector(layout) => { + deserializer.deserialize_seq(VectorElementVisitor(layout)) + } + // primitive move value cases, which are hit to make sure the correct number of bytes + // are removed for elements of an option/vector + PrimitiveArgumentLayout::Bool => { + deserializer.deserialize_bool(serde::de::IgnoredAny)?; + Ok(()) + } + PrimitiveArgumentLayout::U8 => { + deserializer.deserialize_u8(serde::de::IgnoredAny)?; + Ok(()) + } + PrimitiveArgumentLayout::U16 => { + deserializer.deserialize_u16(serde::de::IgnoredAny)?; + Ok(()) + } + PrimitiveArgumentLayout::U32 => { + deserializer.deserialize_u32(serde::de::IgnoredAny)?; + Ok(()) + } + PrimitiveArgumentLayout::U64 => { + deserializer.deserialize_u64(serde::de::IgnoredAny)?; + Ok(()) + } + PrimitiveArgumentLayout::U128 => { + deserializer.deserialize_u128(serde::de::IgnoredAny)?; + Ok(()) + } + PrimitiveArgumentLayout::U256 => { + U256::deserialize(deserializer)?; + Ok(()) + } + PrimitiveArgumentLayout::Address => { + SuiAddress::deserialize(deserializer)?; + Ok(()) + } + } + } + } + + struct VectorElementVisitor<'a>(&'a PrimitiveArgumentLayout); + + impl<'d> serde::de::Visitor<'d> for VectorElementVisitor<'_> { + type Value = (); + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("Vector") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'d>, + { + while seq.next_element_seed(self.0)?.is_some() {} + Ok(()) + } + } + + struct OptionElementVisitor<'a>(&'a PrimitiveArgumentLayout); + + impl<'d> serde::de::Visitor<'d> for OptionElementVisitor<'_> { + type Value = (); + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("Option") + } + + fn visit_none(self) -> Result + where + E: serde::de::Error, + { + Ok(()) + } + + fn visit_some(self, deserializer: D) -> Result + where + D: serde::Deserializer<'d>, + { + self.0.deserialize(deserializer) + } + } + + impl fmt::Display for PrimitiveArgumentLayout { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PrimitiveArgumentLayout::Vector(inner) => { + write!(f, "vector<{inner}>") + } + PrimitiveArgumentLayout::Option(inner) => { + write!(f, "std::option::Option<{inner}>") + } + PrimitiveArgumentLayout::Ascii => { + write!(f, "std::{}::{}", RESOLVED_ASCII_STR.1, RESOLVED_ASCII_STR.2) + } + PrimitiveArgumentLayout::UTF8 => { + write!(f, "std::{}::{}", RESOLVED_UTF8_STR.1, RESOLVED_UTF8_STR.2) + } + PrimitiveArgumentLayout::Bool => write!(f, "bool"), + PrimitiveArgumentLayout::U8 => write!(f, "u8"), + PrimitiveArgumentLayout::U16 => write!(f, "u16"), + PrimitiveArgumentLayout::U32 => write!(f, "u32"), + PrimitiveArgumentLayout::U64 => write!(f, "u64"), + PrimitiveArgumentLayout::U128 => write!(f, "u128"), + PrimitiveArgumentLayout::U256 => write!(f, "u256"), + PrimitiveArgumentLayout::Address => write!(f, "address"), + } + } + } +} diff --git a/sui-execution/replay_cut/sui-adapter/src/programmable_transactions/mod.rs b/sui-execution/replay_cut/sui-adapter/src/programmable_transactions/mod.rs new file mode 100644 index 0000000000000..dfd8e8b2464ce --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/programmable_transactions/mod.rs @@ -0,0 +1,6 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +pub mod context; +pub mod execution; +pub mod trace_utils; diff --git a/sui-execution/replay_cut/sui-adapter/src/programmable_transactions/trace_utils.rs b/sui-execution/replay_cut/sui-adapter/src/programmable_transactions/trace_utils.rs new file mode 100644 index 0000000000000..b5a67fcff5192 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/programmable_transactions/trace_utils.rs @@ -0,0 +1,514 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//! This module implements support for tracing related to PTB execution. IMPORTANT: +//! Bodies of all public functions in this module should be enclosed in a large if statement checking if +//! tracing is enabled or not to make sure that any errors coming from these functions only manifest itself +//! when tracing is enabled. + +use crate::{ + execution_mode::ExecutionMode, + execution_value::{ObjectContents, ObjectValue, Value}, + programmable_transactions::context::*, +}; +use move_core_types::{ + identifier::Identifier, + language_storage::{StructTag, TypeTag}, +}; +use move_trace_format::{ + format::{Effect, MoveTraceBuilder, RefType, TraceEvent, TypeTagWithRefs}, + value::{SerializableMoveValue, SimplifiedMoveStruct}, +}; +use move_vm_types::loaded_data::runtime_types::Type; +use sui_types::{ + base_types::ObjectID, + coin::Coin, + error::{ExecutionError, ExecutionErrorKind}, + object::bounded_visitor::BoundedVisitor, + ptb_trace::{ + ExtMoveValue, ExtMoveValueInfo, ExternalEvent, PTBCommandInfo, PTBEvent, SummaryEvent, + }, + transaction::Command, +}; +use sui_verifier::INIT_FN_NAME; + +/// Inserts Move call start event into the trace. As is the case for all other public functions in this module, +/// its body is (and must be) enclosed in an if statement checking if tracing is enabled. +pub fn trace_move_call_start(trace_builder_opt: &mut Option) { + if let Some(trace_builder) = trace_builder_opt { + trace_builder.push_event(TraceEvent::External(Box::new(serde_json::json!( + PTBEvent::MoveCallStart + )))); + } +} + +/// Inserts Move call end event into the trace. As is the case for all other public functions in this module, +/// its body is (and must be) enclosed in an if statement checking if tracing is enabled. +pub fn trace_move_call_end(trace_builder_opt: &mut Option) { + if let Some(trace_builder) = trace_builder_opt { + trace_builder.push_event(TraceEvent::External(Box::new(serde_json::json!( + PTBEvent::MoveCallEnd + )))); + } +} + +/// Inserts transfer event into the trace. As is the case for all other public functions in this module, +/// its body is (and must be) enclosed in an if statement checking if tracing is enabled. +pub fn trace_transfer( + context: &mut ExecutionContext<'_, '_, '_>, + trace_builder_opt: &mut Option, + obj_values: &[ObjectValue], +) -> Result<(), ExecutionError> { + if let Some(trace_builder) = trace_builder_opt { + let mut to_transfer = vec![]; + for (idx, v) in obj_values.iter().enumerate() { + let obj_info = move_value_info_from_obj_value(context, v)?; + to_transfer.push(ExtMoveValue::Single { + name: format!("obj{idx}"), + info: obj_info, + }); + } + trace_builder.push_event(TraceEvent::External(Box::new(serde_json::json!( + PTBEvent::ExternalEvent(ExternalEvent { + description: "TransferObjects: obj0...objN => ()".to_string(), + name: "Transfer".to_string(), + values: to_transfer, + }) + )))); + } + Ok(()) +} + +/// Inserts PTB summary event into the trace. As is the case for all other public functions in this module, +/// its body is (and must be) enclosed in an if statement checking if tracing is enabled. +pub fn trace_ptb_summary( + context: &mut ExecutionContext<'_, '_, '_>, + trace_builder_opt: &mut Option, + commands: &[Command], +) -> Result<(), ExecutionError> { + if let Some(trace_builder) = trace_builder_opt { + let events = commands + .iter() + .map(|c| match c { + Command::MoveCall(move_call) => { + let pkg = move_call.package.to_string(); + let module = move_call.module.clone(); + let function = move_call.function.clone(); + Ok(vec![PTBCommandInfo::MoveCall { + pkg, + module, + function, + }]) + } + Command::TransferObjects(..) => Ok(vec![PTBCommandInfo::ExternalEvent( + "TransferObjects".to_string(), + )]), + Command::SplitCoins(..) => Ok(vec![PTBCommandInfo::ExternalEvent( + "SplitCoins".to_string(), + )]), + Command::MergeCoins(..) => Ok(vec![PTBCommandInfo::ExternalEvent( + "MergeCoins".to_string(), + )]), + Command::Publish(module_bytes, _) => { + let mut events = vec![]; + events.push(PTBCommandInfo::ExternalEvent("Publish".to_string())); + // Not ideal but it only runs when tracing is enabled so overhead + // should be insignificant + let modules = context.deserialize_modules(module_bytes)?; + events.extend(modules.into_iter().find_map(|m| { + for fdef in &m.function_defs { + let fhandle = m.function_handle_at(fdef.function); + let fname = m.identifier_at(fhandle.name); + if fname == INIT_FN_NAME { + return Some(PTBCommandInfo::MoveCall { + pkg: m.address().to_string(), + module: m.name().to_string(), + function: INIT_FN_NAME.to_string(), + }); + } + } + None + })); + Ok(events) + } + Command::MakeMoveVec(..) => Ok(vec![PTBCommandInfo::ExternalEvent( + "MakeMoveVec".to_string(), + )]), + Command::Upgrade(..) => { + Ok(vec![PTBCommandInfo::ExternalEvent("Upgrade".to_string())]) + } + }) + .collect::>, ExecutionError>>()? + .into_iter() + .flatten() + .collect(); + trace_builder.push_event(TraceEvent::External(Box::new(serde_json::json!( + PTBEvent::Summary(SummaryEvent { + name: "PTBSummary".to_string(), + events, + }) + )))); + } + + Ok(()) +} + +/// Inserts split coins event into the trace. As is the case for all other public functions in this module, +/// its body is (and must be) enclosed in an if statement checking if tracing is enabled. +pub fn trace_split_coins( + context: &mut ExecutionContext<'_, '_, '_>, + trace_builder_opt: &mut Option, + coin_type: &Type, + input_coin: &Coin, + split_coin_values: &[Value], +) -> Result<(), ExecutionError> { + if let Some(trace_builder) = trace_builder_opt { + let type_tag_with_refs = trace_type_to_type_tag_with_refs(context, coin_type)?; + let mut split_coin_move_values = vec![]; + for coin_val in split_coin_values { + let Value::Object(ObjectValue { + contents: ObjectContents::Coin(coin), + .. + }) = coin_val + else { + invariant_violation!("Expected result of split coins PTB command to be a coin"); + }; + split_coin_move_values.push( + coin_move_value_info( + type_tag_with_refs.clone(), + *coin.id.object_id(), + coin.balance.value(), + )? + .value, + ); + } + + let input = coin_move_value_info( + type_tag_with_refs.clone(), + *input_coin.id.object_id(), + input_coin.value(), + )?; + trace_builder.push_event(TraceEvent::External(Box::new(serde_json::json!( + PTBEvent::ExternalEvent(ExternalEvent { + description: "SplitCoins: input => result".to_string(), + name: "SplitCoins".to_string(), + values: vec![ + ExtMoveValue::Single { + name: "input".to_string(), + info: input + }, + ExtMoveValue::Vector { + name: "result".to_string(), + type_: type_tag_with_refs.clone(), + value: split_coin_move_values + }, + ], + }) + )))); + } + Ok(()) +} + +/// Inserts merge coins event into the trace. As is the case for all other public functions in this module, +/// its body is (and must be) enclosed in an if statement checking if tracing is enabled. +pub fn trace_merge_coins( + context: &mut ExecutionContext<'_, '_, '_>, + trace_builder_opt: &mut Option, + coin_type: &Type, + input_infos: &[(u64, ObjectID)], + target_coin: &Coin, +) -> Result<(), ExecutionError> { + if let Some(trace_builder) = trace_builder_opt { + let type_tag_with_refs = trace_type_to_type_tag_with_refs(context, coin_type)?; + let mut input_coin_move_values = vec![]; + let mut to_merge = 0; + for (balance, id) in input_infos { + input_coin_move_values.push(coin_move_value_info( + type_tag_with_refs.clone(), + *id, + *balance, + )?); + to_merge += balance; + } + let merge_target = coin_move_value_info( + type_tag_with_refs.clone(), + *target_coin.id.object_id(), + target_coin.value() - to_merge, + )?; + let mut values = vec![ExtMoveValue::Single { + name: "merge_target".to_string(), + info: merge_target, + }]; + for (idx, input_value) in input_coin_move_values.into_iter().enumerate() { + values.push(ExtMoveValue::Single { + name: format!("coin{idx}"), + info: input_value, + }); + } + let merge_result = coin_move_value_info( + type_tag_with_refs.clone(), + *target_coin.id.object_id(), + target_coin.value(), + )?; + values.push(ExtMoveValue::Single { + name: "merge_result".to_string(), + info: merge_result, + }); + trace_builder.push_event(TraceEvent::External(Box::new(serde_json::json!( + PTBEvent::ExternalEvent(ExternalEvent { + description: "MergeCoins: merge_target, coin0...coinN => mergeresult".to_string(), + name: "MergeCoins".to_string(), + values, + }) + )))); + } + Ok(()) +} + +/// Inserts make move vec event into the trace. As is the case for all other public functions in this module, +/// its body is (and must be) enclosed in an if statement checking if tracing is enabled. +pub fn trace_make_move_vec( + context: &mut ExecutionContext<'_, '_, '_>, + trace_builder_opt: &mut Option, + move_values: Vec, + type_: &Type, +) -> Result<(), ExecutionError> { + if let Some(trace_builder) = trace_builder_opt { + let type_tag_with_refs = trace_type_to_type_tag_with_refs(context, type_)?; + trace_builder.push_event(TraceEvent::External(Box::new(serde_json::json!( + PTBEvent::ExternalEvent(ExternalEvent { + description: "MakeMoveVec: vector".to_string(), + name: "MakeMoveVec".to_string(), + values: vec![ExtMoveValue::Vector { + name: "vector".to_string(), + type_: type_tag_with_refs, + value: move_values + .into_iter() + .map(|move_value| move_value.value) + .collect(), + }], + }) + )))); + } + Ok(()) +} + +/// Inserts publish event into the trace. As is the case for all other public functions in this module, +/// its body is (and must be) enclosed in an if statement checking if tracing is enabled. +pub fn trace_publish_event( + trace_builder_opt: &mut Option, +) -> Result<(), ExecutionError> { + if let Some(trace_builder) = trace_builder_opt { + trace_builder.push_event(TraceEvent::External(Box::new(serde_json::json!( + PTBEvent::ExternalEvent(ExternalEvent { + description: "Publish: ()".to_string(), + name: "Publish".to_string(), + values: vec![], + }) + )))); + } + Ok(()) +} + +/// Inserts upgrade event into the trace. As is the case for all other public functions in this module, +/// its body is (and must be) enclosed in an if statement checking if tracing is enabled. +pub fn trace_upgrade_event( + trace_builder_opt: &mut Option, +) -> Result<(), ExecutionError> { + if let Some(trace_builder) = trace_builder_opt { + trace_builder.push_event(TraceEvent::External(Box::new(serde_json::json!( + PTBEvent::ExternalEvent(ExternalEvent { + description: "Upgrade: ()".to_string(), + name: "Upgrade".to_string(), + values: vec![], + }) + )))); + } + Ok(()) +} + +/// Inserts execution error event into the trace. As is the case for all other public functions in this module, +/// its body is (and must be) enclosed in an if statement checking if tracing is enabled. +pub fn trace_execution_error(trace_builder_opt: &mut Option, msg: String) { + if let Some(trace_builder) = trace_builder_opt { + trace_builder.push_event(TraceEvent::Effect(Box::new(Effect::ExecutionError(msg)))); + } +} + +/// Adds `ExtMoveValueInfo` to the mutable vector passed as an argument. +/// As is the case for all other public functions in this module, +/// its body is (and must be) enclosed in an if statement checking if tracing is enabled. +pub fn add_move_value_info_from_value( + context: &mut ExecutionContext<'_, '_, '_>, + trace_builder_opt: &mut Option, + move_values: &mut Vec, + type_: &Type, + value: &Value, +) -> Result<(), ExecutionError> { + if trace_builder_opt.is_some() + && let Some(move_value_info) = move_value_info_from_value(context, type_, value)? + { + move_values.push(move_value_info); + } + Ok(()) +} + +/// Adds `ExtMoveValueInfo` to the mutable vector passed as an argument. +/// As is the case for all other public functions in this module, +/// its body is (and must be) enclosed in an if statement checking if tracing is enabled. +pub fn add_move_value_info_from_obj_value( + context: &mut ExecutionContext<'_, '_, '_>, + trace_builder_opt: &mut Option, + move_values: &mut Vec, + obj_val: &ObjectValue, +) -> Result<(), ExecutionError> { + if trace_builder_opt.is_some() { + let move_value_info = move_value_info_from_obj_value(context, obj_val)?; + move_values.push(move_value_info); + } + Ok(()) +} + +/// Adds coin object info to the mutable vector passed as an argument. +/// As is the case for all other public functions in this module, +/// its body is (and must be) enclosed in an if statement checking if tracing is enabled. +pub fn add_coin_obj_info( + trace_builder_opt: &mut Option, + coin_infos: &mut Vec<(u64, ObjectID)>, + balance: u64, + id: ObjectID, +) { + if trace_builder_opt.is_some() { + coin_infos.push((balance, id)); + } +} + +/// Creates `ExtMoveValueInfo` from raw bytes. +fn move_value_info_from_raw_bytes( + context: &mut ExecutionContext<'_, '_, '_>, + type_: &Type, + bytes: &[u8], +) -> Result { + let type_tag_with_refs = trace_type_to_type_tag_with_refs(context, type_)?; + let layout = context + .vm + .get_runtime() + .type_to_fully_annotated_layout(type_) + .map_err(|e| ExecutionError::new_with_source(ExecutionErrorKind::InvariantViolation, e))?; + let move_value = BoundedVisitor::deserialize_value(bytes, &layout) + .map_err(|e| ExecutionError::new_with_source(ExecutionErrorKind::InvariantViolation, e))?; + let serialized_move_value = SerializableMoveValue::from(move_value); + Ok(ExtMoveValueInfo { + type_: type_tag_with_refs, + value: serialized_move_value, + }) +} + +/// Creates `ExtMoveValueInfo` from `Value`. +fn move_value_info_from_value( + context: &mut ExecutionContext<'_, '_, '_>, + type_: &Type, + value: &Value, +) -> Result, ExecutionError> { + match value { + Value::Object(obj_val) => Ok(Some(move_value_info_from_obj_value(context, obj_val)?)), + Value::Raw(_, bytes) => Ok(Some(move_value_info_from_raw_bytes(context, type_, bytes)?)), + Value::Receiving(_, _, _) => Ok(None), + } +} + +/// Creates `ExtMoveValueInfo` from `ObjectValue`. +fn move_value_info_from_obj_value( + context: &mut ExecutionContext<'_, '_, '_>, + obj_val: &ObjectValue, +) -> Result { + let type_tag_with_refs = trace_type_to_type_tag_with_refs(context, &obj_val.type_)?; + match &obj_val.contents { + ObjectContents::Coin(coin) => { + coin_move_value_info(type_tag_with_refs, *coin.id.object_id(), coin.value()) + } + ObjectContents::Raw(bytes) => { + move_value_info_from_raw_bytes(context, &obj_val.type_, bytes) + } + } +} + +/// Creates `ExtMoveValueInfo` for a coin. +fn coin_move_value_info( + type_tag_with_refs: TypeTagWithRefs, + object_id: ObjectID, + balance: u64, +) -> Result { + let coin_type_tag = match type_tag_with_refs.type_.clone() { + TypeTag::Struct(tag) => tag, + _ => invariant_violation!("Expected a struct type tag when creating a Move coin value"), + }; + // object.ID + let object_id = SerializableMoveValue::Address(object_id.into()); + let object_id_struct_tag = StructTag { + address: coin_type_tag.address, + module: Identifier::new("object").unwrap(), + name: Identifier::new("ID").unwrap(), + type_params: vec![], + }; + let object_id_struct = SimplifiedMoveStruct { + type_: object_id_struct_tag, + fields: vec![(Identifier::new("value").unwrap(), object_id)], + }; + let serializable_object_id = SerializableMoveValue::Struct(object_id_struct); + // object.UID + let object_uid_struct_tag = StructTag { + address: coin_type_tag.address, + module: Identifier::new("object").unwrap(), + name: Identifier::new("UID").unwrap(), + type_params: vec![], + }; + let object_uid_struct = SimplifiedMoveStruct { + type_: object_uid_struct_tag, + fields: vec![(Identifier::new("id").unwrap(), serializable_object_id)], + }; + let serializable_object_uid = SerializableMoveValue::Struct(object_uid_struct); + // coin.Balance + let serializable_value = SerializableMoveValue::U64(balance); + let balance_struct_tag = StructTag { + address: coin_type_tag.address, + module: Identifier::new("balance").unwrap(), + name: Identifier::new("Balance").unwrap(), + type_params: coin_type_tag.type_params.clone(), + }; + let balance_struct = SimplifiedMoveStruct { + type_: balance_struct_tag, + fields: vec![(Identifier::new("value").unwrap(), serializable_value)], + }; + let serializable_balance = SerializableMoveValue::Struct(balance_struct); + // coin.Coin + let coin_obj = SimplifiedMoveStruct { + type_: *coin_type_tag, + fields: vec![ + (Identifier::new("id").unwrap(), serializable_object_uid), + (Identifier::new("balance").unwrap(), serializable_balance), + ], + }; + Ok(ExtMoveValueInfo { + type_: type_tag_with_refs, + value: SerializableMoveValue::Struct(coin_obj), + }) +} + +/// Converts a type to type tag format used in tracing. +fn trace_type_to_type_tag_with_refs( + context: &mut ExecutionContext<'_, '_, '_>, + type_: &Type, +) -> Result { + let (deref_type, ref_type) = match type_ { + Type::Reference(t) => (t.as_ref(), Some(RefType::Imm)), + Type::MutableReference(t) => (t.as_ref(), Some(RefType::Mut)), + t => (t, None), + }; + let type_ = context + .vm + .get_runtime() + .get_type_tag(deref_type) + .map_err(|e| ExecutionError::new_with_source(ExecutionErrorKind::InvariantViolation, e))?; + Ok(TypeTagWithRefs { type_, ref_type }) +} diff --git a/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/env.rs b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/env.rs new file mode 100644 index 0000000000000..3af54776525d7 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/env.rs @@ -0,0 +1,711 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//! This module defines the shared environment, `Env`, used for the compilation/translation and +//! execution of programmable transactions. While the "context" for each pass might be different, +//! the `Env` provides consistent access to shared components such as the VM or the protocol config. + +use crate::{ + data_store::{ + PackageStore, cached_package_store::CachedPackageStore, linked_data_store::LinkedDataStore, + }, + execution_value::ExecutionState, + programmable_transactions::execution::subst_signature, + static_programmable_transactions::{ + linkage::{ + analysis::LinkageAnalyzer, + resolved_linkage::{ResolvedLinkage, RootedLinkage}, + }, + loading::ast::{ + self as L, Datatype, LoadedFunction, LoadedFunctionInstantiation, Type, Vector, + }, + }, +}; +use move_binary_format::{ + CompiledModule, + errors::{Location, PartialVMError, VMError}, + file_format::{Ability, AbilitySet, TypeParameterIndex}, +}; +use move_core_types::{ + annotated_value, + language_storage::{ModuleId, StructTag}, + runtime_value::{self, MoveTypeLayout}, + vm_status::StatusCode, +}; +use move_vm_runtime::move_vm::MoveVM; +use move_vm_types::{data_store::DataStore, loaded_data::runtime_types as vm_runtime_type}; +use std::{cell::OnceCell, rc::Rc, sync::Arc}; +use sui_protocol_config::ProtocolConfig; +use sui_types::{ + Identifier, TypeTag, + balance::RESOLVED_BALANCE_STRUCT, + base_types::{ObjectID, TxContext}, + error::{ExecutionError, ExecutionErrorKind}, + execution_status::TypeArgumentError, + funds_accumulator::RESOLVED_WITHDRAWAL_STRUCT, + gas_coin::GasCoin, + move_package::{UpgradeCap, UpgradeReceipt, UpgradeTicket}, + object::Object, + type_input::{StructInput, TypeInput}, +}; + +pub struct Env<'pc, 'vm, 'state, 'linkage> { + pub protocol_config: &'pc ProtocolConfig, + pub vm: &'vm MoveVM, + pub state_view: &'state mut dyn ExecutionState, + pub linkable_store: &'linkage CachedPackageStore<'state>, + pub linkage_analysis: &'linkage LinkageAnalyzer, + gas_coin_type: OnceCell, + upgrade_ticket_type: OnceCell, + upgrade_receipt_type: OnceCell, + upgrade_cap_type: OnceCell, + tx_context_type: OnceCell, +} + +macro_rules! get_or_init_ty { + ($env:expr, $ident:ident, $tag:expr) => {{ + let env = $env; + if env.$ident.get().is_none() { + let tag = $tag; + let ty = env.load_type_from_struct(&tag)?; + env.$ident.set(ty.clone()).unwrap(); + } + Ok(env.$ident.get().unwrap().clone()) + }}; +} + +impl<'pc, 'vm, 'state, 'linkage> Env<'pc, 'vm, 'state, 'linkage> { + pub fn new( + protocol_config: &'pc ProtocolConfig, + vm: &'vm MoveVM, + state_view: &'state mut dyn ExecutionState, + linkable_store: &'linkage CachedPackageStore<'state>, + linkage_analysis: &'linkage LinkageAnalyzer, + ) -> Self { + Self { + protocol_config, + vm, + state_view, + linkable_store, + linkage_analysis, + gas_coin_type: OnceCell::new(), + upgrade_ticket_type: OnceCell::new(), + upgrade_receipt_type: OnceCell::new(), + upgrade_cap_type: OnceCell::new(), + tx_context_type: OnceCell::new(), + } + } + + pub fn convert_linked_vm_error(&self, e: VMError, linkage: &RootedLinkage) -> ExecutionError { + convert_vm_error(e, self.vm, self.linkable_store, Some(linkage)) + } + + pub fn convert_vm_error(&self, e: VMError) -> ExecutionError { + convert_vm_error(e, self.vm, self.linkable_store, None) + } + + pub fn convert_type_argument_error( + &self, + idx: usize, + e: VMError, + linkage: &RootedLinkage, + ) -> ExecutionError { + use move_core_types::vm_status::StatusCode; + match e.major_status() { + StatusCode::NUMBER_OF_TYPE_ARGUMENTS_MISMATCH => { + ExecutionErrorKind::TypeArityMismatch.into() + } + StatusCode::TYPE_RESOLUTION_FAILURE => ExecutionErrorKind::TypeArgumentError { + argument_idx: idx as TypeParameterIndex, + kind: TypeArgumentError::TypeNotFound, + } + .into(), + StatusCode::CONSTRAINT_NOT_SATISFIED => ExecutionErrorKind::TypeArgumentError { + argument_idx: idx as TypeParameterIndex, + kind: TypeArgumentError::ConstraintNotSatisfied, + } + .into(), + _ => self.convert_linked_vm_error(e, linkage), + } + } + + pub fn module_definition( + &self, + module_id: &ModuleId, + linkage: &RootedLinkage, + ) -> Result, ExecutionError> { + let linked_data_store = LinkedDataStore::new(linkage, self.linkable_store); + self.vm + .get_runtime() + .load_module(module_id, &linked_data_store) + .map_err(|e| self.convert_linked_vm_error(e, linkage)) + } + + pub fn fully_annotated_layout( + &self, + ty: &Type, + ) -> Result { + let ty = self.load_vm_type_from_adapter_type(None, ty)?; + self.vm + .get_runtime() + .type_to_fully_annotated_layout(&ty) + .map_err(|e| self.convert_vm_error(e)) + } + + pub fn runtime_layout( + &self, + ty: &Type, + ) -> Result { + let ty = self.load_vm_type_from_adapter_type(None, ty)?; + self.vm + .get_runtime() + .type_to_type_layout(&ty) + .map_err(|e| self.convert_vm_error(e)) + } + + pub fn load_function( + &self, + package: ObjectID, + module: String, + function: String, + type_arguments: Vec, + linkage: RootedLinkage, + ) -> Result { + let Some(original_id) = linkage.resolved_linkage.resolve_to_original_id(&package) else { + invariant_violation!( + "Package ID {:?} is not found in linkage generated for that package", + package + ); + }; + let module = to_identifier(module)?; + let name = to_identifier(function)?; + let storage_id = ModuleId::new(package.into(), module.clone()); + let runtime_id = ModuleId::new(original_id.into(), module); + let mut data_store = LinkedDataStore::new(&linkage, self.linkable_store); + let loaded_type_arguments = type_arguments + .iter() + .enumerate() + .map(|(idx, ty)| self.load_vm_type_argument_from_adapter_type(idx, ty)) + .collect::, _>>()?; + let runtime_signature = self + .vm + .get_runtime() + .load_function( + &runtime_id, + name.as_ident_str(), + &loaded_type_arguments, + &mut data_store, + ) + .map_err(|e| { + if e.major_status() == StatusCode::FUNCTION_RESOLUTION_FAILURE { + ExecutionError::new_with_source( + ExecutionErrorKind::FunctionNotFound, + format!( + "Could not resolve function '{}' in module {}", + name, &storage_id, + ), + ) + } else { + self.convert_linked_vm_error(e, &linkage) + } + })?; + let runtime_signature = subst_signature(runtime_signature, &loaded_type_arguments) + .map_err(|e| self.convert_linked_vm_error(e, &linkage))?; + let parameters = runtime_signature + .parameters + .into_iter() + .map(|ty| self.adapter_type_from_vm_type(&ty)) + .collect::, _>>()?; + let return_ = runtime_signature + .return_ + .into_iter() + .map(|ty| self.adapter_type_from_vm_type(&ty)) + .collect::, _>>()?; + let signature = LoadedFunctionInstantiation { + parameters, + return_, + }; + Ok(LoadedFunction { + storage_id, + runtime_id, + name, + type_arguments, + signature, + linkage, + instruction_length: runtime_signature.instruction_length, + definition_index: runtime_signature.definition_index, + }) + } + + pub fn load_type_input(&self, idx: usize, ty: TypeInput) -> Result { + let runtime_type = self.load_vm_type_from_type_input(idx, ty)?; + self.adapter_type_from_vm_type(&runtime_type) + } + + /// We verify that all types in the `StructTag` are defining ID-based types. + pub fn load_type_from_struct(&self, tag: &StructTag) -> Result { + let vm_type = + self.load_vm_type_from_type_tag(None, &TypeTag::Struct(Box::new(tag.clone())))?; + self.adapter_type_from_vm_type(&vm_type) + } + + /// Load the type and layout for a struct tag. + /// This is an optimization to avoid loading the VM type twice when both adapter type and type + /// layout are needed. + pub fn load_type_and_layout_from_struct( + &self, + tag: &StructTag, + ) -> Result<(Type, MoveTypeLayout), ExecutionError> { + let vm_type = + self.load_vm_type_from_type_tag(None, &TypeTag::Struct(Box::new(tag.clone())))?; + let type_layout = self + .vm + .get_runtime() + .type_to_type_layout(&vm_type) + .map_err(|e| self.convert_vm_error(e))?; + self.adapter_type_from_vm_type(&vm_type) + .map(|ty| (ty, type_layout)) + } + + pub fn type_layout_for_struct( + &self, + tag: &StructTag, + ) -> Result { + let ty: Type = self.load_type_from_struct(tag)?; + self.runtime_layout(&ty) + } + + pub fn gas_coin_type(&self) -> Result { + get_or_init_ty!(self, gas_coin_type, GasCoin::type_()) + } + + pub fn upgrade_ticket_type(&self) -> Result { + get_or_init_ty!(self, upgrade_ticket_type, UpgradeTicket::type_()) + } + + pub fn upgrade_receipt_type(&self) -> Result { + get_or_init_ty!(self, upgrade_receipt_type, UpgradeReceipt::type_()) + } + + pub fn upgrade_cap_type(&self) -> Result { + get_or_init_ty!(self, upgrade_cap_type, UpgradeCap::type_()) + } + + pub fn tx_context_type(&self) -> Result { + get_or_init_ty!(self, tx_context_type, TxContext::type_()) + } + + pub fn balance_type(&self, inner_type: Type) -> Result { + let Some(abilities) = AbilitySet::from_u8(Ability::Store as u8) else { + invariant_violation!("Unable to create balance abilities"); + }; + let (a, m, n) = RESOLVED_BALANCE_STRUCT; + let module = ModuleId::new(*a, m.to_owned()); + Ok(Type::Datatype(Rc::new(Datatype { + abilities, + module, + name: n.to_owned(), + type_arguments: vec![inner_type], + }))) + } + + pub fn withdrawal_type(&self, inner_type: Type) -> Result { + let Some(abilities) = AbilitySet::from_u8(Ability::Drop as u8) else { + invariant_violation!("Unable to create withdrawal abilities"); + }; + let (a, m, n) = RESOLVED_WITHDRAWAL_STRUCT; + let module = ModuleId::new(*a, m.to_owned()); + Ok(Type::Datatype(Rc::new(Datatype { + abilities, + module, + name: n.to_owned(), + type_arguments: vec![inner_type], + }))) + } + + pub fn vector_type(&self, element_type: Type) -> Result { + let abilities = AbilitySet::polymorphic_abilities( + AbilitySet::VECTOR, + [false], + [element_type.abilities()], + ) + .map_err(|e| { + ExecutionError::new_with_source(ExecutionErrorKind::VMInvariantViolation, e.to_string()) + })?; + Ok(Type::Vector(Rc::new(L::Vector { + abilities, + element_type, + }))) + } + + pub fn read_object(&self, id: &ObjectID) -> Result<&Object, ExecutionError> { + let Some(obj) = self.state_view.read_object(id) else { + // protected by transaction input checker + invariant_violation!("Object {:?} does not exist", id); + }; + Ok(obj) + } + + /// Takes an adapter Type and returns a VM runtime Type and the linkage for it. + pub fn load_vm_type_argument_from_adapter_type( + &self, + idx: usize, + ty: &Type, + ) -> Result { + self.load_vm_type_from_adapter_type(Some(idx), ty) + } + + fn load_vm_type_from_adapter_type( + &self, + type_arg_idx: Option, + ty: &Type, + ) -> Result { + let tag: TypeTag = ty.clone().try_into().map_err(|s| { + ExecutionError::new_with_source(ExecutionErrorKind::VMInvariantViolation, s) + })?; + self.load_vm_type_from_type_tag(type_arg_idx, &tag) + } + + /// Take a type tag and returns a VM runtime Type and the linkage for it. + fn load_vm_type_from_type_tag( + &self, + type_arg_idx: Option, + tag: &TypeTag, + ) -> Result { + use vm_runtime_type as VMR; + + fn load_type_tag( + env: &Env, + type_arg_idx: Option, + tag: &TypeTag, + ) -> Result { + Ok(match tag { + TypeTag::Bool => VMR::Type::Bool, + TypeTag::U8 => VMR::Type::U8, + TypeTag::U16 => VMR::Type::U16, + TypeTag::U32 => VMR::Type::U32, + TypeTag::U64 => VMR::Type::U64, + TypeTag::U128 => VMR::Type::U128, + TypeTag::U256 => VMR::Type::U256, + TypeTag::Address => VMR::Type::Address, + TypeTag::Signer => VMR::Type::Signer, + + TypeTag::Vector(inner) => { + VMR::Type::Vector(Box::new(load_type_tag(env, type_arg_idx, inner)?)) + } + TypeTag::Struct(tag) => load_struct_tag(env, type_arg_idx, tag)?, + }) + } + + fn load_struct_tag( + env: &Env, + type_arg_idx: Option, + struct_tag: &StructTag, + ) -> Result { + fn execution_error( + env: &Env, + type_arg_idx: Option, + e: VMError, + linkage: &RootedLinkage, + ) -> ExecutionError { + if let Some(idx) = type_arg_idx { + env.convert_type_argument_error(idx, e, linkage) + } else { + env.convert_linked_vm_error(e, linkage) + } + } + + fn verification_error(code: StatusCode) -> VMError { + PartialVMError::new(code).finish(Location::Undefined) + } + + let StructTag { + address, + module, + name, + type_params, + } = struct_tag; + + let tag_linkage = + ResolvedLinkage::type_linkage(&[(*address).into()], env.linkable_store)?; + let linkage = RootedLinkage::new(*address, tag_linkage); + let linked_store = LinkedDataStore::new(&linkage, env.linkable_store); + + let original_id = linkage + .resolved_linkage + .resolve_to_original_id(&(*address).into()) + .ok_or_else(|| { + make_invariant_violation!( + "StructTag {:?} is not found in linkage generated for that struct tag -- this shouldn't happen.", + struct_tag + ) + })?; + let runtime_id = ModuleId::new(*original_id, module.clone()); + + let (idx, struct_type) = env + .vm + .get_runtime() + .load_type(&runtime_id, name, &linked_store) + .map_err(|e| execution_error(env, type_arg_idx, e, &linkage))?; + + let type_param_constraints = struct_type.type_param_constraints(); + if type_param_constraints.len() != type_params.len() { + return Err(execution_error( + env, + type_arg_idx, + verification_error(StatusCode::NUMBER_OF_TYPE_ARGUMENTS_MISMATCH), + &linkage, + )); + } + + if type_params.is_empty() { + Ok(VMR::Type::Datatype(idx)) + } else { + let loaded_type_params = type_params + .iter() + .map(|type_param| load_type_tag(env, type_arg_idx, type_param)) + .collect::, _>>()?; + + // Verify that the type parameter constraints on the struct are met + for (constraint, param) in type_param_constraints.zip(&loaded_type_params) { + let abilities = env + .vm + .get_runtime() + .get_type_abilities(param) + .map_err(|e| execution_error(env, type_arg_idx, e, &linkage))?; + if !constraint.is_subset(abilities) { + return Err(execution_error( + env, + type_arg_idx, + verification_error(StatusCode::CONSTRAINT_NOT_SATISFIED), + &linkage, + )); + } + } + + Ok(VMR::Type::DatatypeInstantiation(Box::new(( + idx, + loaded_type_params, + )))) + } + } + + load_type_tag(self, type_arg_idx, tag) + } + + /// Converts a VM runtime Type to an adapter Type. + fn adapter_type_from_vm_type( + &self, + vm_type: &vm_runtime_type::Type, + ) -> Result { + use vm_runtime_type as VRT; + + Ok(match vm_type { + VRT::Type::Bool => Type::Bool, + VRT::Type::U8 => Type::U8, + VRT::Type::U16 => Type::U16, + VRT::Type::U32 => Type::U32, + VRT::Type::U64 => Type::U64, + VRT::Type::U128 => Type::U128, + VRT::Type::U256 => Type::U256, + VRT::Type::Address => Type::Address, + VRT::Type::Signer => Type::Signer, + + VRT::Type::Reference(ref_ty) => { + let inner_ty = self.adapter_type_from_vm_type(ref_ty)?; + Type::Reference(false, Rc::new(inner_ty)) + } + VRT::Type::MutableReference(ref_ty) => { + let inner_ty = self.adapter_type_from_vm_type(ref_ty)?; + Type::Reference(true, Rc::new(inner_ty)) + } + + VRT::Type::Vector(inner) => { + let element_type = self.adapter_type_from_vm_type(inner)?; + let abilities = self + .vm + .get_runtime() + .get_type_abilities(vm_type) + .map_err(|e| self.convert_vm_error(e))?; + let vector_ty = Vector { + abilities, + element_type, + }; + Type::Vector(Rc::new(vector_ty)) + } + VRT::Type::Datatype(cached_type_index) => { + let runtime = self.vm.get_runtime(); + let Some(cached_info) = runtime.get_type(*cached_type_index) else { + invariant_violation!( + "Unable to find cached type info for {:?}. This should not happen as we have \ + a loaded VM type in-hand.", + vm_type + ) + }; + let datatype = Datatype { + abilities: cached_info.abilities, + module: cached_info.defining_id.clone(), + name: cached_info.name.clone(), + type_arguments: vec![], + }; + Type::Datatype(Rc::new(datatype)) + } + ty @ VRT::Type::DatatypeInstantiation(inst) => { + let (cached_type_index, type_arguments) = &**inst; + let runtime = self.vm.get_runtime(); + let Some(cached_info) = runtime.get_type(*cached_type_index) else { + invariant_violation!( + "Unable to find cached type info for {:?}. This should not happen as we have \ + a loaded VM type in-hand.", + vm_type + ) + }; + + let abilities = runtime + .get_type_abilities(ty) + .map_err(|e| self.convert_vm_error(e))?; + let module = cached_info.defining_id.clone(); + let name = cached_info.name.clone(); + let type_arguments = type_arguments + .iter() + .map(|t| self.adapter_type_from_vm_type(t)) + .collect::, _>>()?; + + Type::Datatype(Rc::new(Datatype { + abilities, + module, + name, + type_arguments, + })) + } + + VRT::Type::TyParam(_) => { + invariant_violation!( + "Unexpected type parameter in VM type: {:?}. This should not happen as we should \ + have resolved all type parameters before this point.", + vm_type + ); + } + }) + } + + /// Load a `TypeInput` into a VM runtime `Type` and its `Linkage`. Loading into the VM ensures + /// that any adapter type or type tag that results from this is properly output with defining + /// IDs. + fn load_vm_type_from_type_input( + &self, + type_arg_idx: usize, + ty: TypeInput, + ) -> Result { + fn to_type_tag_internal( + env: &Env, + type_arg_idx: usize, + ty: TypeInput, + ) -> Result { + Ok(match ty { + TypeInput::Bool => TypeTag::Bool, + TypeInput::U8 => TypeTag::U8, + TypeInput::U16 => TypeTag::U16, + TypeInput::U32 => TypeTag::U32, + TypeInput::U64 => TypeTag::U64, + TypeInput::U128 => TypeTag::U128, + TypeInput::U256 => TypeTag::U256, + TypeInput::Address => TypeTag::Address, + TypeInput::Signer => TypeTag::Signer, + TypeInput::Vector(type_input) => { + let inner = to_type_tag_internal(env, type_arg_idx, *type_input)?; + TypeTag::Vector(Box::new(inner)) + } + TypeInput::Struct(struct_input) => { + let StructInput { + address, + module, + name, + type_params, + } = *struct_input; + + let pkg = env + .linkable_store + .get_package(&address.into()) + .ok() + .flatten() + .ok_or_else(|| { + ExecutionError::from_kind(ExecutionErrorKind::TypeArgumentError { + argument_idx: type_arg_idx as u16, + kind: TypeArgumentError::TypeNotFound, + }) + })?; + let Some(resolved_address) = pkg + .type_origin_map() + .get(&(module.clone(), name.clone())) + .cloned() + else { + return Err(ExecutionError::from_kind( + ExecutionErrorKind::TypeArgumentError { + argument_idx: type_arg_idx as u16, + kind: TypeArgumentError::TypeNotFound, + }, + )); + }; + + let module = to_identifier(module)?; + let name = to_identifier(name)?; + let tys = type_params + .into_iter() + .map(|tp| to_type_tag_internal(env, type_arg_idx, tp)) + .collect::, _>>()?; + TypeTag::Struct(Box::new(StructTag { + address: *resolved_address, + module, + name, + type_params: tys, + })) + } + }) + } + let tag = to_type_tag_internal(self, type_arg_idx, ty)?; + self.load_vm_type_from_type_tag(Some(type_arg_idx), &tag) + } +} + +fn to_identifier(name: String) -> Result { + Identifier::new(name).map_err(|e| { + ExecutionError::new_with_source(ExecutionErrorKind::VMInvariantViolation, e.to_string()) + }) +} + +fn convert_vm_error( + error: VMError, + vm: &MoveVM, + store: &dyn PackageStore, + linkage: Option<&RootedLinkage>, +) -> ExecutionError { + use crate::error::convert_vm_error_impl; + convert_vm_error_impl( + error, + &|id| { + debug_assert!( + linkage.is_some(), + "Linkage should be set anywhere where runtime errors may occur in order to resolve abort locations to package IDs" + ); + linkage + .and_then(|linkage| LinkedDataStore::new(linkage, store).relocate(id).ok()) + .unwrap_or_else(|| id.clone()) + }, + // NB: the `id` here is the original ID (and hence _not_ relocated). + &|id, function| { + debug_assert!( + linkage.is_some(), + "Linkage should be set anywhere where runtime errors may occur in order to resolve abort locations to package IDs" + ); + linkage.and_then(|linkage| { + let state_view = LinkedDataStore::new(linkage, store); + vm.load_module(id, state_view).ok().map(|module| { + let fdef = module.function_def_at(function); + let fhandle = module.function_handle_at(fdef.function); + module.identifier_at(fhandle.name).to_string() + }) + }) + }, + ) +} diff --git a/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/execution/context.rs b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/execution/context.rs new file mode 100644 index 0000000000000..337c17d8425d4 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/execution/context.rs @@ -0,0 +1,1455 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + adapter, + data_store::linked_data_store::LinkedDataStore, + execution_mode::ExecutionMode, + gas_charger::GasCharger, + gas_meter::SuiGasMeter, + programmable_transactions::{self as legacy_ptb}, + sp, + static_programmable_transactions::{ + env::Env, + execution::values::{Local, Locals, Value}, + linkage::resolved_linkage::{ResolvedLinkage, RootedLinkage}, + loading::ast::{Datatype, ObjectMutability}, + typing::ast::{self as T, Type}, + }, +}; +use indexmap::{IndexMap, IndexSet}; +use move_binary_format::{ + CompiledModule, + errors::{Location, PartialVMError, VMResult}, + file_format::FunctionDefinitionIndex, + file_format_common::VERSION_6, +}; +use move_core_types::{ + account_address::AccountAddress, + identifier::IdentStr, + language_storage::{ModuleId, StructTag}, +}; +use move_trace_format::format::MoveTraceBuilder; +use move_vm_runtime::native_extensions::NativeContextExtensions; +use move_vm_types::{ + gas::GasMeter, + values::{VMValueCast, Value as VMValue}, +}; +use std::{ + cell::RefCell, + collections::{BTreeMap, BTreeSet}, + rc::Rc, + sync::Arc, +}; +use sui_move_natives::object_runtime::{ + self, LoadedRuntimeObject, ObjectRuntime, RuntimeResults, get_all_uids, max_event_error, +}; +use sui_types::{ + TypeTag, + base_types::{MoveObjectType, ObjectID, SequenceNumber, TxContext}, + error::{ExecutionError, ExecutionErrorKind}, + execution::ExecutionResults, + metrics::LimitsMetrics, + move_package::{MovePackage, UpgradeCap, UpgradeReceipt, UpgradeTicket}, + object::{MoveObject, Object, Owner}, +}; +use sui_verifier::INIT_FN_NAME; +use tracing::instrument; + +macro_rules! unwrap { + ($e:expr, $($args:expr),* $(,)?) => { + match $e { + Some(v) => v, + None => { + invariant_violation!("Unexpected none: {}", format!($($args),*)) + } + } + + }; +} + +macro_rules! object_runtime_mut { + ($context:ident) => {{ + $context + .native_extensions + .get_mut::() + .map_err(|e| $context.env.convert_vm_error(e.finish(Location::Undefined))) + }}; +} + +macro_rules! charge_gas_ { + ($gas_charger:expr, $env:expr, $case:ident, $value_view:expr) => {{ + SuiGasMeter($gas_charger.move_gas_status_mut()) + .$case($value_view) + .map_err(|e| $env.convert_vm_error(e.finish(Location::Undefined))) + }}; +} + +macro_rules! charge_gas { + ($context:ident, $case:ident, $value_view:expr) => {{ charge_gas_!($context.gas_charger, $context.env, $case, $value_view) }}; +} + +/// Type wrapper around Value to ensure safe usage +#[derive(Debug)] +pub struct CtxValue(Value); + +#[derive(Clone, Debug)] +pub struct InputObjectMetadata { + pub id: ObjectID, + pub mutability: ObjectMutability, + pub owner: Owner, + pub version: SequenceNumber, + pub type_: Type, +} + +#[derive(Copy, Clone)] +enum UsageKind { + Move, + Copy, + Borrow, +} + +// Locals and metadata for all `Location`s. Separated from `Context` for lifetime reasons. +struct Locations { + // A single local for holding the TxContext + tx_context_value: Locals, + /// The runtime value for the Gas coin, None if no gas coin is provided + gas: Option<(InputObjectMetadata, Locals)>, + /// The runtime value for the input objects args + input_object_metadata: Vec<(T::InputIndex, InputObjectMetadata)>, + object_inputs: Locals, + input_withdrawal_metadata: Vec, + withdrawal_inputs: Locals, + pure_input_bytes: IndexSet>, + pure_input_metadata: Vec, + pure_inputs: Locals, + receiving_input_metadata: Vec, + receiving_inputs: Locals, + /// The results of a given command. For most commands, the inner vector will have length 1. + /// It will only not be 1 for Move calls with multiple return values. + /// Inner values are None if taken/moved by-value + results: Vec, +} + +enum ResolvedLocation<'a> { + Local(Local<'a>), + Pure { + bytes: &'a [u8], + metadata: &'a T::PureInput, + local: Local<'a>, + }, + Receiving { + metadata: &'a T::ReceivingInput, + local: Local<'a>, + }, +} + +/// Maintains all runtime state specific to programmable transactions +pub struct Context<'env, 'pc, 'vm, 'state, 'linkage, 'gas> { + pub env: &'env Env<'pc, 'vm, 'state, 'linkage>, + /// Metrics for reporting exceeded limits + pub metrics: Arc, + pub native_extensions: NativeContextExtensions<'env>, + /// A shared transaction context, contains transaction digest information and manages the + /// creation of new object IDs + pub tx_context: Rc>, + /// The gas charger used for metering + pub gas_charger: &'gas mut GasCharger, + /// User events are claimed after each Move call + user_events: Vec<(ModuleId, StructTag, Vec)>, + // runtime data + locations: Locations, +} + +impl Locations { + /// NOTE! This does not charge gas and should not be used directly. It is exposed for + /// dev-inspect + fn resolve(&mut self, location: T::Location) -> Result, ExecutionError> { + Ok(match location { + T::Location::TxContext => ResolvedLocation::Local(self.tx_context_value.local(0)?), + T::Location::GasCoin => { + let (_, gas_locals) = unwrap!(self.gas.as_mut(), "Gas coin not provided"); + ResolvedLocation::Local(gas_locals.local(0)?) + } + T::Location::ObjectInput(i) => ResolvedLocation::Local(self.object_inputs.local(i)?), + T::Location::WithdrawalInput(i) => { + ResolvedLocation::Local(self.withdrawal_inputs.local(i)?) + } + T::Location::Result(i, j) => { + let result = unwrap!(self.results.get_mut(i as usize), "bounds already verified"); + ResolvedLocation::Local(result.local(j)?) + } + T::Location::PureInput(i) => { + let local = self.pure_inputs.local(i)?; + let metadata = &self.pure_input_metadata[i as usize]; + let bytes = self + .pure_input_bytes + .get_index(metadata.byte_index) + .ok_or_else(|| { + make_invariant_violation!( + "Pure input {} bytes out of bounds at index {}", + metadata.original_input_index.0, + metadata.byte_index, + ) + })?; + ResolvedLocation::Pure { + bytes, + metadata, + local, + } + } + T::Location::ReceivingInput(i) => ResolvedLocation::Receiving { + metadata: &self.receiving_input_metadata[i as usize], + local: self.receiving_inputs.local(i)?, + }, + }) + } +} + +impl<'env, 'pc, 'vm, 'state, 'linkage, 'gas> Context<'env, 'pc, 'vm, 'state, 'linkage, 'gas> { + #[instrument(name = "Context::new", level = "trace", skip_all)] + pub fn new( + env: &'env Env<'pc, 'vm, 'state, 'linkage>, + metrics: Arc, + tx_context: Rc>, + gas_charger: &'gas mut GasCharger, + pure_input_bytes: IndexSet>, + object_inputs: Vec, + input_withdrawal_metadata: Vec, + pure_input_metadata: Vec, + receiving_input_metadata: Vec, + ) -> Result + where + 'pc: 'state, + { + let mut input_object_map = BTreeMap::new(); + let mut input_object_metadata = Vec::with_capacity(object_inputs.len()); + let mut object_values = Vec::with_capacity(object_inputs.len()); + for object_input in object_inputs { + let (i, m, v) = load_object_arg(gas_charger, env, &mut input_object_map, object_input)?; + input_object_metadata.push((i, m)); + object_values.push(Some(v)); + } + let object_inputs = Locals::new(object_values)?; + let mut withdrawal_values = Vec::with_capacity(input_withdrawal_metadata.len()); + for withdrawal_input in &input_withdrawal_metadata { + let v = load_withdrawal_arg(gas_charger, env, withdrawal_input)?; + withdrawal_values.push(Some(v)); + } + let withdrawal_inputs = Locals::new(withdrawal_values)?; + let pure_inputs = Locals::new_invalid(pure_input_metadata.len())?; + let receiving_inputs = Locals::new_invalid(receiving_input_metadata.len())?; + let gas = match gas_charger.gas_coin() { + Some(gas_coin) => { + let ty = env.gas_coin_type()?; + let (gas_metadata, gas_value) = load_object_arg_impl( + gas_charger, + env, + &mut input_object_map, + gas_coin, + ObjectMutability::Mutable, + ty, + )?; + let mut gas_locals = Locals::new([Some(gas_value)])?; + let gas_local = gas_locals.local(0)?; + let gas_ref = gas_local.borrow()?; + // We have already checked that the gas balance is enough to cover the gas budget + let max_gas_in_balance = gas_charger.gas_budget(); + gas_ref.coin_ref_subtract_balance(max_gas_in_balance)?; + Some((gas_metadata, gas_locals)) + } + None => None, + }; + let native_extensions = adapter::new_native_extensions( + env.state_view.as_child_resolver(), + input_object_map, + !gas_charger.is_unmetered(), + env.protocol_config, + metrics.clone(), + tx_context.clone(), + ); + + let tx_context_value = Locals::new(vec![Some(Value::new_tx_context( + tx_context.borrow().digest(), + )?)])?; + Ok(Self { + env, + metrics, + native_extensions, + tx_context, + gas_charger, + user_events: vec![], + locations: Locations { + tx_context_value, + gas, + input_object_metadata, + object_inputs, + input_withdrawal_metadata, + withdrawal_inputs, + pure_input_bytes, + pure_input_metadata, + pure_inputs, + receiving_input_metadata, + receiving_inputs, + results: vec![], + }, + }) + } + + pub fn finish(mut self) -> Result { + assert_invariant!( + !self.locations.tx_context_value.local(0)?.is_invalid()?, + "tx context value should be present" + ); + let gas = std::mem::take(&mut self.locations.gas); + let object_input_metadata = std::mem::take(&mut self.locations.input_object_metadata); + let mut object_inputs = + std::mem::replace(&mut self.locations.object_inputs, Locals::new_invalid(0)?); + let mut loaded_runtime_objects = BTreeMap::new(); + let mut by_value_shared_objects = BTreeSet::new(); + let mut consensus_owner_objects = BTreeMap::new(); + let gas = gas + .map(|(m, mut g)| Result::<_, ExecutionError>::Ok((m, g.local(0)?.move_if_valid()?))) + .transpose()?; + let gas_id_opt = gas.as_ref().map(|(m, _)| m.id); + let object_inputs = object_input_metadata + .into_iter() + .enumerate() + .map(|(i, (_, m))| { + let v_opt = object_inputs.local(i as u16)?.move_if_valid()?; + Ok((m, v_opt)) + }) + .collect::, ExecutionError>>()?; + for (metadata, value_opt) in object_inputs.into_iter().chain(gas) { + let InputObjectMetadata { + id, + mutability, + owner, + version, + type_, + } = metadata; + match mutability { + ObjectMutability::Immutable => continue, + // It is illegal to mutate NonExclusiveWrites, but they are passed as &mut T, + // so we need to treat them as mutable here. After execution, we check if they + // have been mutated, and abort the tx if they have. + ObjectMutability::NonExclusiveWrite | ObjectMutability::Mutable => (), + } + loaded_runtime_objects.insert( + id, + LoadedRuntimeObject { + version, + is_modified: true, + }, + ); + if let Some(object) = value_opt { + self.transfer_object_( + owner, + type_, + CtxValue(object), + /* end of transaction */ true, + )?; + } else if owner.is_shared() { + by_value_shared_objects.insert(id); + } else if matches!(owner, Owner::ConsensusAddressOwner { .. }) { + consensus_owner_objects.insert(id, owner.clone()); + } + } + + let Self { + env, + mut native_extensions, + tx_context, + gas_charger, + user_events, + .. + } = self; + let ref_context: &RefCell = &tx_context; + let tx_context: &TxContext = &ref_context.borrow(); + let tx_digest = ref_context.borrow().digest(); + + let object_runtime: ObjectRuntime = native_extensions + .remove() + .map_err(|e| env.convert_vm_error(e.finish(Location::Undefined)))?; + + let RuntimeResults { + mut writes, + user_events: remaining_events, + loaded_child_objects, + mut created_object_ids, + deleted_object_ids, + accumulator_events, + settlement_input_sui, + settlement_output_sui, + } = object_runtime.finish()?; + assert_invariant!( + remaining_events.is_empty(), + "Events should be taken after every Move call" + ); + // Refund unused gas + if let Some(gas_id) = gas_id_opt { + refund_max_gas_budget(&mut writes, gas_charger, gas_id)?; + } + + loaded_runtime_objects.extend(loaded_child_objects); + + let mut written_objects = BTreeMap::new(); + + for (id, (recipient, ty, value)) in writes { + let (ty, layout) = env.load_type_and_layout_from_struct(&ty.clone().into())?; + let abilities = ty.abilities(); + let has_public_transfer = abilities.has_store(); + let Some(bytes) = value.typed_serialize(&layout) else { + invariant_violation!("Failed to serialize already deserialized Move value"); + }; + // safe because has_public_transfer has been determined by the abilities + let move_object = unsafe { + create_written_object::( + env, + &loaded_runtime_objects, + id, + ty, + has_public_transfer, + bytes, + )? + }; + let object = Object::new_move(move_object, recipient, tx_digest); + written_objects.insert(id, object); + } + + for package in self.env.linkable_store.to_new_packages().into_iter() { + let package_obj = Object::new_from_package(package, tx_digest); + let id = package_obj.id(); + created_object_ids.insert(id); + written_objects.insert(id, package_obj); + } + + legacy_ptb::context::finish( + env.protocol_config, + env.state_view, + gas_charger, + tx_context, + &by_value_shared_objects, + &consensus_owner_objects, + loaded_runtime_objects, + written_objects, + created_object_ids, + deleted_object_ids, + user_events, + accumulator_events, + settlement_input_sui, + settlement_output_sui, + ) + } + + pub fn object_runtime(&self) -> Result<&ObjectRuntime<'_>, ExecutionError> { + self.native_extensions + .get::() + .map_err(|e| self.env.convert_vm_error(e.finish(Location::Undefined))) + } + + pub fn take_user_events( + &mut self, + storage_id: ModuleId, + function_def_idx: FunctionDefinitionIndex, + instr_length: u16, + linkage: &RootedLinkage, + ) -> Result<(), ExecutionError> { + let events = object_runtime_mut!(self)?.take_user_events(); + let Some(num_events) = self.user_events.len().checked_add(events.len()) else { + invariant_violation!("usize overflow, too many events emitted") + }; + let max_events = self.env.protocol_config.max_num_event_emit(); + if num_events as u64 > max_events { + let err = max_event_error(max_events) + .at_code_offset(function_def_idx, instr_length) + .finish(Location::Module(storage_id.clone())); + return Err(self.env.convert_linked_vm_error(err, linkage)); + } + let new_events = events + .into_iter() + .map(|(tag, value)| { + let layout = self.env.type_layout_for_struct(&tag)?; + let Some(bytes) = value.typed_serialize(&layout) else { + invariant_violation!("Failed to serialize Move event"); + }; + Ok((storage_id.clone(), tag, bytes)) + }) + .collect::, ExecutionError>>()?; + self.user_events.extend(new_events); + Ok(()) + } + + // + // Arguments and Values + // + + fn location( + &mut self, + usage: UsageKind, + location: T::Location, + ) -> Result { + let resolved = self.locations.resolve(location)?; + let mut local = match resolved { + ResolvedLocation::Local(l) => l, + ResolvedLocation::Pure { + bytes, + metadata, + mut local, + } => { + if local.is_invalid()? { + let v = load_pure_value(self.gas_charger, self.env, bytes, metadata)?; + local.store(v)?; + } + local + } + ResolvedLocation::Receiving { + metadata, + mut local, + } => { + if local.is_invalid()? { + let v = load_receiving_value(self.gas_charger, self.env, metadata)?; + local.store(v)?; + } + local + } + }; + Ok(match usage { + UsageKind::Move => local.move_()?, + UsageKind::Copy => { + let value = local.copy()?; + charge_gas_!(self.gas_charger, self.env, charge_copy_loc, &value)?; + value + } + UsageKind::Borrow => local.borrow()?, + }) + } + + fn location_usage(&mut self, usage: T::Usage) -> Result { + match usage { + T::Usage::Move(location) => self.location(UsageKind::Move, location), + T::Usage::Copy { location, .. } => self.location(UsageKind::Copy, location), + } + } + + fn argument_value(&mut self, sp!(_, (arg_, _)): T::Argument) -> Result { + match arg_ { + T::Argument__::Use(usage) => self.location_usage(usage), + // freeze is a no-op for references since the value does not track mutability + T::Argument__::Freeze(usage) => self.location_usage(usage), + T::Argument__::Borrow(_, location) => self.location(UsageKind::Borrow, location), + T::Argument__::Read(usage) => { + let reference = self.location_usage(usage)?; + charge_gas!(self, charge_read_ref, &reference)?; + reference.read_ref() + } + } + } + + pub fn argument(&mut self, arg: T::Argument) -> Result + where + VMValue: VMValueCast, + { + let value = self.argument_value(arg)?; + let value: V = value.cast()?; + Ok(value) + } + + pub fn arguments(&mut self, args: Vec) -> Result, ExecutionError> + where + VMValue: VMValueCast, + { + args.into_iter().map(|arg| self.argument(arg)).collect() + } + + pub fn result(&mut self, result: Vec>) -> Result<(), ExecutionError> { + self.locations + .results + .push(Locals::new(result.into_iter().map(|v| v.map(|v| v.0)))?); + Ok(()) + } + + pub fn copy_value(&mut self, value: &CtxValue) -> Result { + Ok(CtxValue(copy_value(self.gas_charger, self.env, &value.0)?)) + } + + pub fn new_coin(&mut self, amount: u64) -> Result { + let id = self.tx_context.borrow_mut().fresh_id(); + object_runtime_mut!(self)? + .new_id(id) + .map_err(|e| self.env.convert_vm_error(e.finish(Location::Undefined)))?; + Ok(CtxValue(Value::coin(id, amount))) + } + + pub fn destroy_coin(&mut self, coin: CtxValue) -> Result { + let (id, amount) = coin.0.unpack_coin()?; + object_runtime_mut!(self)? + .delete_id(id) + .map_err(|e| self.env.convert_vm_error(e.finish(Location::Undefined)))?; + Ok(amount) + } + + pub fn new_upgrade_cap(&mut self, storage_id: ObjectID) -> Result { + let id = self.tx_context.borrow_mut().fresh_id(); + object_runtime_mut!(self)? + .new_id(id) + .map_err(|e| self.env.convert_vm_error(e.finish(Location::Undefined)))?; + let cap = UpgradeCap::new(id, storage_id); + Ok(CtxValue(Value::upgrade_cap(cap))) + } + + pub fn upgrade_receipt( + &self, + upgrade_ticket: UpgradeTicket, + upgraded_package_id: ObjectID, + ) -> CtxValue { + let receipt = UpgradeReceipt::new(upgrade_ticket, upgraded_package_id); + CtxValue(Value::upgrade_receipt(receipt)) + } + + // + // Move calls + // + + pub fn vm_move_call( + &mut self, + function: T::LoadedFunction, + args: Vec, + trace_builder_opt: Option<&mut MoveTraceBuilder>, + ) -> Result, ExecutionError> { + let result = self.execute_function_bypass_visibility( + &function.runtime_id, + &function.name, + &function.type_arguments, + args, + &function.linkage, + trace_builder_opt, + )?; + self.take_user_events( + function.storage_id, + function.definition_index, + function.instruction_length, + &function.linkage, + )?; + Ok(result) + } + + pub fn execute_function_bypass_visibility( + &mut self, + runtime_id: &ModuleId, + function_name: &IdentStr, + ty_args: &[Type], + args: Vec, + linkage: &RootedLinkage, + tracer: Option<&mut MoveTraceBuilder>, + ) -> Result, ExecutionError> { + let ty_args = ty_args + .iter() + .enumerate() + .map(|(idx, ty)| self.env.load_vm_type_argument_from_adapter_type(idx, ty)) + .collect::>()?; + let gas_status = self.gas_charger.move_gas_status_mut(); + let mut data_store = LinkedDataStore::new(linkage, self.env.linkable_store); + let values = self + .env + .vm + .get_runtime() + .execute_function_with_values_bypass_visibility( + runtime_id, + function_name, + ty_args, + args.into_iter().map(|v| v.0.into()).collect(), + &mut data_store, + &mut SuiGasMeter(gas_status), + &mut self.native_extensions, + tracer, + ) + .map_err(|e| self.env.convert_linked_vm_error(e, linkage))?; + Ok(values.into_iter().map(|v| CtxValue(v.into())).collect()) + } + + // + // Publish and Upgrade + // + + // is_upgrade is used for gas charging. Assumed to be a new publish if false. + pub fn deserialize_modules( + &mut self, + module_bytes: &[Vec], + is_upgrade: bool, + ) -> Result, ExecutionError> { + assert_invariant!( + !module_bytes.is_empty(), + "empty package is checked in transaction input checker" + ); + let total_bytes = module_bytes.iter().map(|v| v.len()).sum(); + if is_upgrade { + self.gas_charger.charge_upgrade_package(total_bytes)? + } else { + self.gas_charger.charge_publish_package(total_bytes)? + } + + let binary_config = self.env.protocol_config.binary_config(None); + let modules = module_bytes + .iter() + .map(|b| { + CompiledModule::deserialize_with_config(b, &binary_config) + .map_err(|e| e.finish(Location::Undefined)) + }) + .collect::>>() + .map_err(|e| self.env.convert_vm_error(e))?; + Ok(modules) + } + + fn fetch_package( + &mut self, + dependency_id: &ObjectID, + ) -> Result, ExecutionError> { + let [fetched_package] = self.fetch_packages(&[*dependency_id])?.try_into().map_err( + |_| { + make_invariant_violation!( + "We should always fetch a single package for each object or return a dependency error." + ) + }, + )?; + Ok(fetched_package) + } + + fn fetch_packages( + &mut self, + dependency_ids: &[ObjectID], + ) -> Result>, ExecutionError> { + let mut fetched = vec![]; + let mut missing = vec![]; + + // Collect into a set to avoid duplicate fetches and preserve existing behavior + let dependency_ids: BTreeSet<_> = dependency_ids.iter().collect(); + + for id in &dependency_ids { + match self.env.linkable_store.get_package(id) { + Err(e) => { + return Err(ExecutionError::new_with_source( + ExecutionErrorKind::PublishUpgradeMissingDependency, + e, + )); + } + Ok(Some(inner)) => { + fetched.push(inner); + } + Ok(None) => { + missing.push(*id); + } + } + } + + if missing.is_empty() { + assert_invariant!( + fetched.len() == dependency_ids.len(), + "all dependencies should be fetched" + ); + Ok(fetched) + } else { + let msg = format!( + "Missing dependencies: {}", + missing + .into_iter() + .map(|dep| format!("{}", dep)) + .collect::>() + .join(", ") + ); + Err(ExecutionError::new_with_source( + ExecutionErrorKind::PublishUpgradeMissingDependency, + msg, + )) + } + } + + fn publish_and_verify_modules( + &mut self, + package_id: ObjectID, + modules: &[CompiledModule], + linkage: &RootedLinkage, + ) -> Result<(), ExecutionError> { + // TODO(https://github.com/MystenLabs/sui/issues/69): avoid this redundant serialization by exposing VM API that allows us to run the linker directly on `Vec` + let binary_version = self.env.protocol_config.move_binary_format_version(); + let new_module_bytes: Vec<_> = modules + .iter() + .map(|m| { + let mut bytes = Vec::new(); + let version = if binary_version > VERSION_6 { + m.version + } else { + VERSION_6 + }; + m.serialize_with_version(version, &mut bytes).unwrap(); + bytes + }) + .collect(); + let mut data_store = LinkedDataStore::new(linkage, self.env.linkable_store); + self.env + .vm + .get_runtime() + .publish_module_bundle( + new_module_bytes, + AccountAddress::from(package_id), + &mut data_store, + &mut SuiGasMeter(self.gas_charger.move_gas_status_mut()), + ) + .map_err(|e| self.env.convert_linked_vm_error(e, linkage))?; + + // run the Sui verifier + for module in modules { + // Run Sui bytecode verifier, which runs some additional checks that assume the Move + // bytecode verifier has passed. + sui_verifier::verifier::sui_verify_module_unmetered( + module, + &BTreeMap::new(), + &self + .env + .protocol_config + .verifier_config(/* signing_limits */ None), + )?; + } + + Ok(()) + } + + fn init_modules( + &mut self, + package_id: ObjectID, + modules: &[CompiledModule], + linkage: &RootedLinkage, + mut trace_builder_opt: Option<&mut MoveTraceBuilder>, + ) -> Result<(), ExecutionError> { + for module in modules { + let Some((fdef_idx, fdef)) = module.find_function_def_by_name(INIT_FN_NAME.as_str()) + else { + continue; + }; + let fhandle = module.function_handle_at(fdef.function); + let fparameters = module.signature_at(fhandle.parameters); + assert_invariant!( + fparameters.0.len() <= 2, + "init function should have at most 2 parameters" + ); + let has_otw = fparameters.0.len() == 2; + let tx_context = self + .location(UsageKind::Borrow, T::Location::TxContext) + .map_err(|e| { + make_invariant_violation!("Failed to get tx context for init function: {}", e) + })?; + let args = if has_otw { + vec![CtxValue(Value::one_time_witness()?), CtxValue(tx_context)] + } else { + vec![CtxValue(tx_context)] + }; + let return_values = self.execute_function_bypass_visibility( + &module.self_id(), + INIT_FN_NAME, + &[], + args, + linkage, + trace_builder_opt.as_deref_mut(), + )?; + + let storage_id = ModuleId::new(package_id.into(), module.self_id().name().to_owned()); + self.take_user_events( + storage_id, + fdef_idx, + fdef.code.as_ref().map(|c| c.code.len() as u16).unwrap_or(0), + linkage, + )?; + assert_invariant!( + return_values.is_empty(), + "init should not have return values" + ) + } + + Ok(()) + } + + pub fn publish_and_init_package( + &mut self, + mut modules: Vec, + dep_ids: &[ObjectID], + linkage: ResolvedLinkage, + trace_builder_opt: Option<&mut MoveTraceBuilder>, + ) -> Result { + let runtime_id = if ::packages_are_predefined() { + // do not calculate or substitute id for predefined packages + (*modules[0].self_id().address()).into() + } else { + // It should be fine that this does not go through the object runtime since it does not + // need to know about new packages created, since Move objects and Move packages + // cannot interact + let id = self.tx_context.borrow_mut().fresh_id(); + adapter::substitute_package_id(&mut modules, id)?; + id + }; + + let dependencies = self.fetch_packages(dep_ids)?; + let package = Rc::new(MovePackage::new_initial( + &modules, + self.env.protocol_config, + dependencies.iter().map(|p| p.as_ref()), + )?); + let package_id = package.id(); + + // Here we optimistically push the package that is being published/upgraded + // and if there is an error of any kind (verification or module init) we + // remove it. + // The call to `pop_last_package` later is fine because we cannot re-enter and + // the last package we pushed is the one we are verifying and running the init from + let linkage = RootedLinkage::new_for_publication(package_id, runtime_id, linkage); + + self.env.linkable_store.push_package(package_id, package)?; + let res = self + .publish_and_verify_modules(runtime_id, &modules, &linkage) + .and_then(|_| self.init_modules(package_id, &modules, &linkage, trace_builder_opt)); + match res { + Ok(()) => Ok(runtime_id), + Err(e) => { + self.env.linkable_store.pop_package(package_id)?; + Err(e) + } + } + } + + pub fn upgrade( + &mut self, + mut modules: Vec, + dep_ids: &[ObjectID], + current_package_id: ObjectID, + upgrade_ticket_policy: u8, + linkage: ResolvedLinkage, + ) -> Result { + // Check that this package ID points to a package and get the package we're upgrading. + let current_move_package = self.fetch_package(¤t_package_id)?; + + let runtime_id = current_move_package.original_package_id(); + adapter::substitute_package_id(&mut modules, runtime_id)?; + + // Upgraded packages share their predecessor's runtime ID but get a new storage ID. + // It should be fine that this does not go through the object runtime since it does not + // need to know about new packages created, since Move objects and Move packages + // cannot interact + let storage_id = self.tx_context.borrow_mut().fresh_id(); + + let dependencies = self.fetch_packages(dep_ids)?; + let package = current_move_package.new_upgraded( + storage_id, + &modules, + self.env.protocol_config, + dependencies.iter().map(|p| p.as_ref()), + )?; + + let linkage = RootedLinkage::new_for_publication(storage_id, runtime_id, linkage); + self.publish_and_verify_modules(runtime_id, &modules, &linkage)?; + + legacy_ptb::execution::check_compatibility( + self.env.protocol_config, + current_move_package.as_ref(), + &modules, + upgrade_ticket_policy, + )?; + + if self.env.protocol_config.check_for_init_during_upgrade() { + // find newly added modules to the package, + // and error if they have init functions + let current_module_names: BTreeSet<&str> = current_move_package + .serialized_module_map() + .keys() + .map(|s| s.as_str()) + .collect(); + let upgrade_module_names: BTreeSet<&str> = package + .serialized_module_map() + .keys() + .map(|s| s.as_str()) + .collect(); + let new_module_names = upgrade_module_names + .difference(¤t_module_names) + .copied() + .collect::>(); + let new_modules = modules + .iter() + .filter(|m| { + let name = m.identifier_at(m.self_handle().name).as_str(); + new_module_names.contains(name) + }) + .collect::>(); + let new_module_has_init = new_modules.iter().any(|module| { + module.function_defs.iter().any(|fdef| { + let fhandle = module.function_handle_at(fdef.function); + let fname = module.identifier_at(fhandle.name); + fname == INIT_FN_NAME + }) + }); + if new_module_has_init { + // TODO we cannot run 'init' on upgrade yet due to global type cache limitations + return Err(ExecutionError::new_with_source( + ExecutionErrorKind::FeatureNotYetSupported, + "`init` in new modules on upgrade is not yet supported", + )); + } + } + + self.env + .linkable_store + .push_package(storage_id, Rc::new(package))?; + Ok(storage_id) + } + + // + // Commands + // + + pub fn transfer_object( + &mut self, + recipient: Owner, + ty: Type, + object: CtxValue, + ) -> Result<(), ExecutionError> { + self.transfer_object_(recipient, ty, object, /* end of transaction */ false) + } + + fn transfer_object_( + &mut self, + recipient: Owner, + ty: Type, + object: CtxValue, + end_of_transaction: bool, + ) -> Result<(), ExecutionError> { + let tag = TypeTag::try_from(ty) + .map_err(|_| make_invariant_violation!("Unable to convert Type to TypeTag"))?; + let TypeTag::Struct(tag) = tag else { + invariant_violation!("Expected struct type tag"); + }; + let ty = MoveObjectType::from(*tag); + object_runtime_mut!(self)? + .transfer(recipient, ty, object.0.into(), end_of_transaction) + .map_err(|e| self.env.convert_vm_error(e.finish(Location::Undefined)))?; + Ok(()) + } + + // + // Dev Inspect tracking + // + + pub fn argument_updates( + &mut self, + args: Vec, + ) -> Result, TypeTag)>, ExecutionError> { + args.into_iter() + .filter_map(|arg| self.argument_update(arg).transpose()) + .collect() + } + + fn argument_update( + &mut self, + sp!(_, (arg, ty)): T::Argument, + ) -> Result, TypeTag)>, ExecutionError> { + use sui_types::transaction::Argument as TxArgument; + let ty = match ty { + Type::Reference(true, inner) => (*inner).clone(), + ty => { + debug_assert!( + false, + "Unexpected non reference type in location update: {ty:?}" + ); + return Ok(None); + } + }; + let Ok(tag): Result = ty.clone().try_into() else { + invariant_violation!("unable to generate type tag from type") + }; + let location = arg.location(); + let resolved = self.locations.resolve(location)?; + let local = match resolved { + ResolvedLocation::Local(local) + | ResolvedLocation::Pure { local, .. } + | ResolvedLocation::Receiving { local, .. } => local, + }; + if local.is_invalid()? { + return Ok(None); + } + // copy the value from the local + let value = local.copy()?; + let value = match arg { + T::Argument__::Use(_) => { + // dereference the reference + value.read_ref()? + } + T::Argument__::Borrow(_, _) => { + // value is not a reference, nothing to do + value + } + T::Argument__::Freeze(_) => { + invariant_violation!("freeze should not be used for a mutable reference") + } + T::Argument__::Read(_) => { + invariant_violation!("read should not return a reference") + } + }; + let layout = self.env.runtime_layout(&ty)?; + let Some(bytes) = value.typed_serialize(&layout) else { + invariant_violation!("Failed to serialize Move value"); + }; + let arg = match location { + T::Location::TxContext => return Ok(None), + T::Location::GasCoin => TxArgument::GasCoin, + T::Location::Result(i, j) => TxArgument::NestedResult(i, j), + T::Location::ObjectInput(i) => { + TxArgument::Input(self.locations.input_object_metadata[i as usize].0.0) + } + T::Location::WithdrawalInput(i) => TxArgument::Input( + self.locations.input_withdrawal_metadata[i as usize] + .original_input_index + .0, + ), + T::Location::PureInput(i) => TxArgument::Input( + self.locations.pure_input_metadata[i as usize] + .original_input_index + .0, + ), + T::Location::ReceivingInput(i) => TxArgument::Input( + self.locations.receiving_input_metadata[i as usize] + .original_input_index + .0, + ), + }; + Ok(Some((arg, bytes, tag))) + } + + pub fn tracked_results( + &self, + results: &[CtxValue], + result_tys: &T::ResultType, + ) -> Result, TypeTag)>, ExecutionError> { + assert_invariant!( + results.len() == result_tys.len(), + "results and result types should match" + ); + results + .iter() + .zip(result_tys) + .map(|(v, ty)| self.tracked_result(&v.0, ty.clone())) + .collect() + } + + fn tracked_result( + &self, + result: &Value, + ty: Type, + ) -> Result<(Vec, TypeTag), ExecutionError> { + let inner_value; + let (v, ty) = match ty { + Type::Reference(_, inner) => { + inner_value = result.copy()?.read_ref()?; + (&inner_value, (*inner).clone()) + } + _ => (result, ty), + }; + let layout = self.env.runtime_layout(&ty)?; + let Some(bytes) = v.typed_serialize(&layout) else { + invariant_violation!("Failed to serialize Move value"); + }; + let Ok(tag): Result = ty.try_into() else { + invariant_violation!("unable to generate type tag from type") + }; + Ok((bytes, tag)) + } +} + +impl VMValueCast for VMValue { + fn cast(self) -> Result { + Ok(CtxValue(self.into())) + } +} + +impl CtxValue { + pub fn vec_pack(ty: Type, values: Vec) -> Result { + Ok(CtxValue(Value::vec_pack( + ty, + values.into_iter().map(|v| v.0).collect(), + )?)) + } + + pub fn coin_ref_value(self) -> Result { + self.0.coin_ref_value() + } + + pub fn coin_ref_subtract_balance(self, amount: u64) -> Result<(), ExecutionError> { + self.0.coin_ref_subtract_balance(amount) + } + + pub fn coin_ref_add_balance(self, amount: u64) -> Result<(), ExecutionError> { + self.0.coin_ref_add_balance(amount) + } + + pub fn into_upgrade_ticket(self) -> Result { + self.0.into_upgrade_ticket() + } +} + +fn load_object_arg( + meter: &mut GasCharger, + env: &Env, + input_object_map: &mut BTreeMap, + input: T::ObjectInput, +) -> Result<(T::InputIndex, InputObjectMetadata, Value), ExecutionError> { + let id = input.arg.id(); + let mutability = input.arg.mutability(); + let (metadata, value) = + load_object_arg_impl(meter, env, input_object_map, id, mutability, input.ty)?; + Ok((input.original_input_index, metadata, value)) +} + +fn load_object_arg_impl( + meter: &mut GasCharger, + env: &Env, + input_object_map: &mut BTreeMap, + id: ObjectID, + mutability: ObjectMutability, + ty: T::Type, +) -> Result<(InputObjectMetadata, Value), ExecutionError> { + let obj = env.read_object(&id)?; + let owner = obj.owner.clone(); + let version = obj.version(); + let object_metadata = InputObjectMetadata { + id, + mutability, + owner: owner.clone(), + version, + type_: ty.clone(), + }; + let sui_types::object::ObjectInner { + data: sui_types::object::Data::Move(move_obj), + .. + } = obj.as_inner() + else { + invariant_violation!("Expected a Move object"); + }; + assert_expected_move_object_type(&object_metadata.type_, move_obj.type_())?; + let contained_uids = { + let fully_annotated_layout = env.fully_annotated_layout(&ty)?; + get_all_uids(&fully_annotated_layout, move_obj.contents()).map_err(|e| { + make_invariant_violation!("Unable to retrieve UIDs for object. Got error: {e}") + })? + }; + input_object_map.insert( + id, + object_runtime::InputObject { + contained_uids, + version, + owner, + }, + ); + + let v = Value::deserialize(env, move_obj.contents(), ty)?; + charge_gas_!(meter, env, charge_copy_loc, &v)?; + Ok((object_metadata, v)) +} + +fn load_withdrawal_arg( + meter: &mut GasCharger, + env: &Env, + withdrawal: &T::WithdrawalInput, +) -> Result { + let T::WithdrawalInput { + original_input_index: _, + ty: _, + owner, + amount, + } = withdrawal; + let loaded = Value::funds_accumulator_withdrawal(*owner, *amount); + charge_gas_!(meter, env, charge_copy_loc, &loaded)?; + Ok(loaded) +} + +fn load_pure_value( + meter: &mut GasCharger, + env: &Env, + bytes: &[u8], + metadata: &T::PureInput, +) -> Result { + let loaded = Value::deserialize(env, bytes, metadata.ty.clone())?; + // ByteValue::Receiving { id, version } => Value::receiving(*id, *version), + charge_gas_!(meter, env, charge_copy_loc, &loaded)?; + Ok(loaded) +} + +fn load_receiving_value( + meter: &mut GasCharger, + env: &Env, + metadata: &T::ReceivingInput, +) -> Result { + let (id, version, _) = metadata.object_ref; + let loaded = Value::receiving(id, version); + charge_gas_!(meter, env, charge_copy_loc, &loaded)?; + Ok(loaded) +} + +fn copy_value(meter: &mut GasCharger, env: &Env, value: &Value) -> Result { + charge_gas_!(meter, env, charge_copy_loc, value)?; + value.copy() +} + +/// The max budget was deducted from the gas coin at the beginning of the transaction, +/// now we return exactly that amount. Gas will be charged by the execution engine +fn refund_max_gas_budget( + writes: &mut IndexMap, + gas_charger: &mut GasCharger, + gas_id: ObjectID, +) -> Result<(), ExecutionError> { + let Some((_, _, value_ref)) = writes.get_mut(&gas_id) else { + invariant_violation!("Gas object cannot be wrapped or destroyed") + }; + // replace with dummy value + let value = std::mem::replace(value_ref, VMValue::u8(0)); + let mut locals = Locals::new([Some(value.into())])?; + let mut local = locals.local(0)?; + let coin_value = local.borrow()?.coin_ref_value()?; + let additional = gas_charger.gas_budget(); + if coin_value.checked_add(additional).is_none() { + return Err(ExecutionError::new_with_source( + ExecutionErrorKind::CoinBalanceOverflow, + "Gas coin too large after returning the max gas budget", + )); + }; + local.borrow()?.coin_ref_add_balance(additional)?; + // put the value back + *value_ref = local.move_()?.into(); + Ok(()) +} + +/// Generate an MoveObject given an updated/written object +/// # Safety +/// +/// This function assumes proper generation of has_public_transfer, either from the abilities of +/// the StructTag, or from the runtime correctly propagating from the inputs +unsafe fn create_written_object( + env: &Env, + objects_modified_at: &BTreeMap, + id: ObjectID, + type_: Type, + has_public_transfer: bool, + contents: Vec, +) -> Result { + debug_assert_eq!( + id, + MoveObject::id_opt(&contents).expect("object contents should start with an id") + ); + let old_obj_ver = objects_modified_at + .get(&id) + .map(|obj: &LoadedRuntimeObject| obj.version); + + let Ok(type_tag): Result = type_.try_into() else { + invariant_violation!("unable to generate type tag from type") + }; + + let struct_tag = match type_tag { + TypeTag::Struct(inner) => *inner, + _ => invariant_violation!("Non struct type for object"), + }; + unsafe { + MoveObject::new_from_execution( + struct_tag.into(), + has_public_transfer, + old_obj_ver.unwrap_or_default(), + contents, + env.protocol_config, + Mode::packages_are_predefined(), + ) + } +} + +/// Assert the type inferred matches the object's type. This has already been done during loading, +/// but is checked again as an invariant. This may be removed safely at a later time if needed. +fn assert_expected_move_object_type( + actual: &Type, + expected: &MoveObjectType, +) -> Result<(), ExecutionError> { + let Type::Datatype(actual) = actual else { + invariant_violation!("Expected a datatype for a Move object"); + }; + let (a, m, n) = actual.qualified_ident(); + assert_invariant!( + a == &expected.address(), + "Actual address does not match expected. actual: {actual:?} vs expected: {expected:?}" + ); + assert_invariant!( + m == expected.module(), + "Actual module does not match expected. actual: {actual:?} vs expected: {expected:?}" + ); + assert_invariant!( + n == expected.name(), + "Actual struct does not match expected. actual: {actual:?} vs expected: {expected:?}" + ); + let actual_type_arguments = &actual.type_arguments; + let expected_type_arguments = expected.type_params(); + assert_invariant!( + actual_type_arguments.len() == expected_type_arguments.len(), + "Actual type arg length does not match expected. \ + actual: {actual:?} vs expected: {expected:?}", + ); + for (actual_ty, expected_ty) in actual_type_arguments.iter().zip(&expected_type_arguments) { + assert_expected_type(actual_ty, expected_ty)?; + } + Ok(()) +} + +/// Assert the type inferred matches the expected type. This has already been done during typing, +/// but is checked again as an invariant. This may be removed safely at a later time if needed. +fn assert_expected_type(actual: &Type, expected: &TypeTag) -> Result<(), ExecutionError> { + match (actual, expected) { + (Type::Bool, TypeTag::Bool) + | (Type::U8, TypeTag::U8) + | (Type::U16, TypeTag::U16) + | (Type::U32, TypeTag::U32) + | (Type::U64, TypeTag::U64) + | (Type::U128, TypeTag::U128) + | (Type::U256, TypeTag::U256) + | (Type::Address, TypeTag::Address) + | (Type::Signer, TypeTag::Signer) => Ok(()), + (Type::Vector(inner_actual), TypeTag::Vector(inner_expected)) => { + assert_expected_type(&inner_actual.element_type, inner_expected) + } + (Type::Datatype(actual_dt), TypeTag::Struct(expected_st)) => { + assert_expected_data_type(actual_dt, expected_st) + } + _ => invariant_violation!( + "Type mismatch between actual: {actual:?} and expected: {expected:?}" + ), + } +} +/// Assert the type inferred matches the expected type. This has already been done during typing, +/// but is checked again as an invariant. This may be removed safely at a later time if needed. +fn assert_expected_data_type( + actual: &Datatype, + expected: &StructTag, +) -> Result<(), ExecutionError> { + let (a, m, n) = actual.qualified_ident(); + assert_invariant!( + a == &expected.address, + "Actual address does not match expected. actual: {actual:?} vs expected: {expected:?}" + ); + assert_invariant!( + m == expected.module.as_ident_str(), + "Actual module does not match expected. actual: {actual:?} vs expected: {expected:?}" + ); + assert_invariant!( + n == expected.name.as_ident_str(), + "Actual struct does not match expected. actual: {actual:?} vs expected: {expected:?}" + ); + let actual_type_arguments = &actual.type_arguments; + let expected_type_arguments = &expected.type_params; + assert_invariant!( + actual_type_arguments.len() == expected_type_arguments.len(), + "Actual type arg length does not match expected. \ + actual: {actual:?} vs expected: {expected:?}", + ); + for (actual_ty, expected_ty) in actual_type_arguments.iter().zip(expected_type_arguments) { + assert_expected_type(actual_ty, expected_ty)?; + } + Ok(()) +} diff --git a/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/execution/interpreter.rs b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/execution/interpreter.rs new file mode 100644 index 0000000000000..f1ff6f31b57c9 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/execution/interpreter.rs @@ -0,0 +1,333 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + execution_mode::ExecutionMode, + gas_charger::GasCharger, + sp, + static_programmable_transactions::{ + env::Env, + execution::context::{Context, CtxValue}, + typing::ast as T, + }, +}; +use move_core_types::account_address::AccountAddress; +use move_trace_format::format::MoveTraceBuilder; +use std::{cell::RefCell, rc::Rc, sync::Arc, time::Instant}; +use sui_types::{ + base_types::TxContext, + error::{ExecutionError, ExecutionErrorKind}, + execution::{ExecutionTiming, ResultWithTimings}, + execution_status::PackageUpgradeError, + metrics::LimitsMetrics, + move_package::MovePackage, + object::Owner, +}; +use tracing::instrument; + +pub fn execute<'env, 'pc, 'vm, 'state, 'linkage, Mode: ExecutionMode>( + env: &'env mut Env<'pc, 'vm, 'state, 'linkage>, + metrics: Arc, + tx_context: Rc>, + gas_charger: &mut GasCharger, + ast: T::Transaction, + trace_builder_opt: &mut Option, +) -> ResultWithTimings +where + 'pc: 'state, + 'env: 'state, +{ + let mut timings = vec![]; + let result = execute_inner::( + &mut timings, + env, + metrics, + tx_context, + gas_charger, + ast, + trace_builder_opt, + ); + + match result { + Ok(result) => Ok((result, timings)), + Err(e) => Err((e, timings)), + } +} + +pub fn execute_inner<'env, 'pc, 'vm, 'state, 'linkage, Mode: ExecutionMode>( + timings: &mut Vec, + env: &'env mut Env<'pc, 'vm, 'state, 'linkage>, + metrics: Arc, + tx_context: Rc>, + gas_charger: &mut GasCharger, + ast: T::Transaction, + trace_builder_opt: &mut Option, +) -> Result +where + 'pc: 'state, +{ + let T::Transaction { + bytes, + objects, + withdrawals, + pure, + receiving, + commands, + } = ast; + let mut context = Context::new( + env, + metrics, + tx_context, + gas_charger, + bytes, + objects, + withdrawals, + pure, + receiving, + )?; + let mut mode_results = Mode::empty_results(); + for sp!(idx, c) in commands { + let start = Instant::now(); + if let Err(err) = execute_command::( + &mut context, + &mut mode_results, + c, + trace_builder_opt.as_mut(), + ) { + let object_runtime = context.object_runtime()?; + // We still need to record the loaded child objects for replay + let loaded_runtime_objects = object_runtime.loaded_runtime_objects(); + // we do not save the wrapped objects since on error, they should not be modified + drop(context); + // TODO wtf is going on with the borrow checker here. 'state is bound into the object + // runtime, but its since been dropped. what gives with this error? + env.state_view + .save_loaded_runtime_objects(loaded_runtime_objects); + timings.push(ExecutionTiming::Abort(start.elapsed())); + return Err(err.with_command_index(idx as usize)); + }; + timings.push(ExecutionTiming::Success(start.elapsed())); + } + // Save loaded objects table in case we fail in post execution + let object_runtime = context.object_runtime()?; + // We still need to record the loaded child objects for replay + // Record the objects loaded at runtime (dynamic fields + received) for + // storage rebate calculation. + let loaded_runtime_objects = object_runtime.loaded_runtime_objects(); + // We record what objects were contained in at the start of the transaction + // for expensive invariant checks + let wrapped_object_containers = object_runtime.wrapped_object_containers(); + // We record the generated object IDs for expensive invariant checks + let generated_object_ids = object_runtime.generated_object_ids(); + + // apply changes + let finished = context.finish::(); + // Save loaded objects for debug. We dont want to lose the info + env.state_view + .save_loaded_runtime_objects(loaded_runtime_objects); + env.state_view + .save_wrapped_object_containers(wrapped_object_containers); + env.state_view.record_execution_results(finished?)?; + env.state_view + .record_generated_object_ids(generated_object_ids); + Ok(mode_results) +} + +/// Execute a single command +#[instrument(level = "trace", skip_all)] +fn execute_command( + context: &mut Context, + mode_results: &mut Mode::ExecutionResults, + c: T::Command_, + trace_builder_opt: Option<&mut MoveTraceBuilder>, +) -> Result<(), ExecutionError> { + let T::Command_ { + command, + result_type, + drop_values, + consumed_shared_objects: _, + } = c; + let mut args_to_update = vec![]; + let result = match command { + T::Command__::MoveCall(move_call) => { + let T::MoveCall { + function, + arguments, + } = *move_call; + if Mode::TRACK_EXECUTION { + args_to_update.extend( + arguments + .iter() + .filter(|arg| matches!(&arg.value.1, T::Type::Reference(/* mut */ true, _))) + .cloned(), + ) + } + let arguments = context.arguments(arguments)?; + context.vm_move_call(function, arguments, trace_builder_opt)? + } + T::Command__::TransferObjects(objects, recipient) => { + let object_tys = objects + .iter() + .map(|sp!(_, (_, ty))| ty.clone()) + .collect::>(); + let object_values: Vec = context.arguments(objects)?; + let recipient: AccountAddress = context.argument(recipient)?; + assert_invariant!( + object_values.len() == object_tys.len(), + "object values and types mismatch" + ); + for (object_value, ty) in object_values.into_iter().zip(object_tys) { + // TODO should we just call a Move function? + let recipient = Owner::AddressOwner(recipient.into()); + context.transfer_object(recipient, ty, object_value)?; + } + vec![] + } + T::Command__::SplitCoins(_, coin, amounts) => { + // TODO should we just call a Move function? + if Mode::TRACK_EXECUTION { + args_to_update.push(coin.clone()); + } + let coin_ref: CtxValue = context.argument(coin)?; + let amount_values: Vec = context.arguments(amounts)?; + let mut total: u64 = 0; + for amount in &amount_values { + let Some(new_total) = total.checked_add(*amount) else { + return Err(ExecutionError::from_kind( + ExecutionErrorKind::CoinBalanceOverflow, + )); + }; + total = new_total; + } + let coin_value = context.copy_value(&coin_ref)?.coin_ref_value()?; + fp_ensure!( + coin_value >= total, + ExecutionError::new_with_source( + ExecutionErrorKind::InsufficientCoinBalance, + format!("balance: {coin_value} required: {total}") + ) + ); + coin_ref.coin_ref_subtract_balance(total)?; + amount_values + .into_iter() + .map(|a| context.new_coin(a)) + .collect::>()? + } + T::Command__::MergeCoins(_, target, coins) => { + // TODO should we just call a Move function? + if Mode::TRACK_EXECUTION { + args_to_update.push(target.clone()); + } + let target_ref: CtxValue = context.argument(target)?; + let coins = context.arguments(coins)?; + let amounts = coins + .into_iter() + .map(|coin| context.destroy_coin(coin)) + .collect::, _>>()?; + let mut additional: u64 = 0; + for amount in amounts { + let Some(new_additional) = additional.checked_add(amount) else { + return Err(ExecutionError::from_kind( + ExecutionErrorKind::CoinBalanceOverflow, + )); + }; + additional = new_additional; + } + let target_value = context.copy_value(&target_ref)?.coin_ref_value()?; + fp_ensure!( + target_value.checked_add(additional).is_some(), + ExecutionError::from_kind(ExecutionErrorKind::CoinBalanceOverflow,) + ); + target_ref.coin_ref_add_balance(additional)?; + vec![] + } + T::Command__::MakeMoveVec(ty, items) => { + let items: Vec = context.arguments(items)?; + vec![CtxValue::vec_pack(ty, items)?] + } + T::Command__::Publish(module_bytes, dep_ids, linkage) => { + let modules = + context.deserialize_modules(&module_bytes, /* is upgrade */ false)?; + + let runtime_id = context.publish_and_init_package::( + modules, + &dep_ids, + linkage, + trace_builder_opt, + )?; + + if ::packages_are_predefined() { + // no upgrade cap for genesis modules + std::vec![] + } else { + std::vec![context.new_upgrade_cap(runtime_id)?] + } + } + T::Command__::Upgrade( + module_bytes, + dep_ids, + current_package_id, + upgrade_ticket, + linkage, + ) => { + let upgrade_ticket = context + .argument::(upgrade_ticket)? + .into_upgrade_ticket()?; + // Make sure the passed-in package ID matches the package ID in the `upgrade_ticket`. + if current_package_id != upgrade_ticket.package.bytes { + return Err(ExecutionError::from_kind( + ExecutionErrorKind::PackageUpgradeError { + upgrade_error: PackageUpgradeError::PackageIDDoesNotMatch { + package_id: current_package_id, + ticket_id: upgrade_ticket.package.bytes, + }, + }, + )); + } + // deserialize modules and charge gas + let modules = context.deserialize_modules(&module_bytes, /* is upgrade */ true)?; + + let computed_digest = MovePackage::compute_digest_for_modules_and_deps( + &module_bytes, + &dep_ids, + /* hash_modules */ true, + ) + .to_vec(); + if computed_digest != upgrade_ticket.digest { + return Err(ExecutionError::from_kind( + ExecutionErrorKind::PackageUpgradeError { + upgrade_error: PackageUpgradeError::DigestDoesNotMatch { + digest: computed_digest, + }, + }, + )); + } + + let upgraded_package_id = context.upgrade( + modules, + &dep_ids, + current_package_id, + upgrade_ticket.policy, + linkage, + )?; + + vec![context.upgrade_receipt(upgrade_ticket, upgraded_package_id)] + } + }; + if Mode::TRACK_EXECUTION { + let argument_updates = context.argument_updates(args_to_update)?; + let command_result = context.tracked_results(&result, &result_type)?; + Mode::finish_command_v2(mode_results, argument_updates, command_result)?; + } + assert_invariant!( + result.len() == drop_values.len(), + "result values and drop values mismatch" + ); + let result = result + .into_iter() + .zip(drop_values) + .map(|(value, drop)| if !drop { Some(value) } else { None }) + .collect::>(); + context.result(result)?; + Ok(()) +} diff --git a/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/execution/mod.rs b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/execution/mod.rs new file mode 100644 index 0000000000000..39e103ab4a36b --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/execution/mod.rs @@ -0,0 +1,6 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +pub mod context; +pub mod interpreter; +pub mod values; diff --git a/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/execution/values.rs b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/execution/values.rs new file mode 100644 index 0000000000000..4b161317e6efb --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/execution/values.rs @@ -0,0 +1,421 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::static_programmable_transactions::{env::Env, typing::ast::Type}; +use move_binary_format::errors::PartialVMError; +use move_core_types::{account_address::AccountAddress, runtime_value::MoveTypeLayout, u256::U256}; +use move_vm_types::{ + values::{ + self, Locals as VMLocals, Struct, VMValueCast, Value as VMValue, VectorSpecialization, + }, + views::ValueView, +}; +use sui_types::{ + base_types::{ObjectID, SequenceNumber}, + digests::TransactionDigest, + error::ExecutionError, + move_package::{UpgradeCap, UpgradeReceipt, UpgradeTicket}, +}; +pub enum InputValue<'a> { + Bytes(&'a ByteValue), + Loaded(Local<'a>), +} + +pub enum ByteValue { + Pure(Vec), + Receiving { + id: ObjectID, + version: SequenceNumber, + }, +} + +/// A memory location that can be borrowed or moved from +pub struct Local<'a>(&'a mut Locals, u16); + +/// A set of memory locations that can be borrowed or moved from. Used for inputs and results +pub struct Locals(VMLocals); + +#[derive(Debug)] +pub struct Value(VMValue); + +impl Locals { + pub fn new(values: Items) -> Result + where + Items: IntoIterator>, + Items::IntoIter: ExactSizeIterator, + { + let values = values.into_iter(); + let n = values.len(); + assert_invariant!(n <= u16::MAX as usize, "Locals size exceeds u16::MAX"); + let mut locals = VMLocals::new(n); + for (i, value_opt) in values.enumerate() { + let Some(value) = value_opt else { + // If the value is None, we leave the local invalid + continue; + }; + locals + .store_loc(i, value.0, /* violation check */ true) + .map_err(iv("store loc"))?; + } + Ok(Self(locals)) + } + + pub fn new_invalid(n: usize) -> Result { + assert_invariant!(n <= u16::MAX as usize, "Locals size exceeds u16::MAX"); + Ok(Self(VMLocals::new(n))) + } + + pub fn local(&mut self, index: u16) -> Result, ExecutionError> { + Ok(Local(self, index)) + } +} + +impl Local<'_> { + /// Does the local contain a value? + pub fn is_invalid(&self) -> Result { + self.0 + .0 + .is_invalid(self.1 as usize) + .map_err(iv("out of bounds")) + } + + pub fn store(&mut self, value: Value) -> Result<(), ExecutionError> { + self.0 + .0 + .store_loc(self.1 as usize, value.0, /* violation check */ true) + .map_err(iv("store loc")) + } + + /// Move the value out of the local + pub fn move_(&mut self) -> Result { + assert_invariant!(!self.is_invalid()?, "cannot move invalid local"); + Ok(Value( + self.0 + .0 + .move_loc(self.1 as usize, /* violation check */ true) + .map_err(iv("move loc"))?, + )) + } + + /// Copy the value out in the local + pub fn copy(&self) -> Result { + assert_invariant!(!self.is_invalid()?, "cannot copy invalid local"); + Ok(Value( + self.0.0.copy_loc(self.1 as usize).map_err(iv("copy loc"))?, + )) + } + + /// Borrow the local, creating a reference to the value + pub fn borrow(&self) -> Result { + assert_invariant!(!self.is_invalid()?, "cannot borrow invalid local"); + Ok(Value( + self.0 + .0 + .borrow_loc(self.1 as usize) + .map_err(iv("borrow loc"))?, + )) + } + + pub fn move_if_valid(&mut self) -> Result, ExecutionError> { + if self.is_invalid()? { + Ok(None) + } else { + Ok(Some(self.move_()?)) + } + } +} + +impl Value { + pub fn copy(&self) -> Result { + Ok(Value(self.0.copy_value().map_err(iv("copy"))?)) + } + + /// Read the value, giving an invariant violation if the value is not a reference + pub fn read_ref(self) -> Result { + let value: values::Reference = self.0.cast().map_err(iv("cast"))?; + Ok(Self(value.read_ref().map_err(iv("read ref"))?)) + } + + /// This function will invariant violation on an invalid cast + pub fn cast(self) -> Result + where + VMValue: VMValueCast, + { + self.0.cast().map_err(iv("cast")) + } + + pub fn deserialize(env: &Env, bytes: &[u8], ty: Type) -> Result { + let layout = env.runtime_layout(&ty)?; + let Some(value) = VMValue::simple_deserialize(bytes, &layout) else { + // we already checked the layout of pure bytes during typing + // and objects should already be valid + invariant_violation!("unable to deserialize value to type {ty:?}") + }; + Ok(Value(value)) + } + + pub fn typed_serialize(&self, layout: &MoveTypeLayout) -> Option> { + self.0.typed_serialize(layout) + } +} + +impl From for Value { + fn from(value: VMValue) -> Self { + Value(value) + } +} + +impl From for VMValue { + fn from(value: Value) -> Self { + value.0 + } +} + +impl VMValueCast for VMValue { + fn cast(self) -> Result { + Ok(self.into()) + } +} + +impl ValueView for Value { + fn visit(&self, visitor: &mut impl move_vm_types::views::ValueVisitor) { + self.0.visit(visitor) + } +} + +//************************************************************************************************** +// Value Construction +//************************************************************************************************** + +impl Value { + pub fn id(address: AccountAddress) -> Self { + // ID { address } + Self(VMValue::struct_(Struct::pack([VMValue::address(address)]))) + } + + pub fn uid(address: AccountAddress) -> Self { + // UID { ID { address } } + Self(VMValue::struct_(Struct::pack([Self::id(address).0]))) + } + + pub fn receiving(id: ObjectID, version: SequenceNumber) -> Self { + Self(VMValue::struct_(Struct::pack([ + Self::id(id.into()).0, + VMValue::u64(version.into()), + ]))) + } + + pub fn balance(amount: u64) -> Self { + // Balance { amount } + Self(VMValue::struct_(Struct::pack([VMValue::u64(amount)]))) + } + + /// The uid _must_ be registered by the object runtime before being called + pub fn coin(id: ObjectID, amount: u64) -> Self { + Self(VMValue::struct_(Struct::pack([ + Self::uid(id.into()).0, + Self::balance(amount).0, + ]))) + } + + /// Constructs a `sui::funds_accumulator::Withdrawal` value + pub fn funds_accumulator_withdrawal(owner: AccountAddress, limit: U256) -> Self { + // public struct Withdrawal has drop { + // owner: address, + // limit: u256, + // } + Self(VMValue::struct_(Struct::pack([ + VMValue::address(owner), + VMValue::u256(limit), + ]))) + } + + pub fn vec_pack(ty: Type, values: Vec) -> Result { + let specialization: VectorSpecialization = ty + .try_into() + .map_err(|e| make_invariant_violation!("Unable to specialize vector: {e}"))?; + let vec = values::Vector::pack(specialization, values.into_iter().map(|v| v.0)) + .map_err(iv("pack"))?; + Ok(Self(vec)) + } + + /// Should be called once at the start of a transaction to populate the location with the + /// transaction context. + pub fn new_tx_context(digest: TransactionDigest) -> Result { + // public struct TxContext has drop { + // sender: address, + // tx_hash: vector, + // epoch: u64, + // epoch_timestamp_ms: u64, + // ids_created: u64, + // } + Ok(Self(VMValue::struct_(Struct::pack([ + VMValue::address(AccountAddress::ZERO), + VMValue::vector_u8(digest.inner().iter().copied()), + VMValue::u64(0), + VMValue::u64(0), + VMValue::u64(0), + ])))) + } + + pub fn one_time_witness() -> Result { + // public struct has drop{ + // _dummy: bool, + // } + Ok(Self(VMValue::struct_(Struct::pack([VMValue::bool(true)])))) + } +} + +//************************************************************************************************** +// Coin Functions +//************************************************************************************************** + +impl Value { + pub fn unpack_coin(self) -> Result<(ObjectID, u64), ExecutionError> { + let [id, balance] = unpack(self.0)?; + // unpack UID + let [id] = unpack(id)?; + // unpack ID + let [id] = unpack(id)?; + let id: AccountAddress = id.cast().map_err(iv("cast"))?; + // unpack Balance + let [balance] = unpack(balance)?; + let balance: u64 = balance.cast().map_err(iv("cast"))?; + Ok((ObjectID::from(id), balance)) + } + + pub fn coin_ref_value(self) -> Result { + let balance_value_ref = borrow_coin_ref_balance_value(self.0)?; + let balance_value_ref: values::Reference = balance_value_ref.cast().map_err(iv("cast"))?; + let balance_value = balance_value_ref.read_ref().map_err(iv("read ref"))?; + balance_value.cast().map_err(iv("cast")) + } + + /// The coin value MUST be checked before calling this function, if `amount` is greater than + /// the value of the coin, it will return an invariant violation. + pub fn coin_ref_subtract_balance(self, amount: u64) -> Result<(), ExecutionError> { + coin_ref_modify_balance(self.0, |balance| { + let Some(new_balance) = balance.checked_sub(amount) else { + invariant_violation!("coin balance {balance} is less than {amount}") + }; + Ok(new_balance) + }) + } + + /// The coin max value MUST be checked before calling this function, if `amount` plus the current + /// balance is greater than `u64::MAX`, it will return an invariant violation. + pub fn coin_ref_add_balance(self, amount: u64) -> Result<(), ExecutionError> { + coin_ref_modify_balance(self.0, |balance| { + let Some(new_balance) = balance.checked_add(amount) else { + invariant_violation!("coin balance {balance} + {amount} is greater than u64::MAX") + }; + Ok(new_balance) + }) + } +} + +fn coin_ref_modify_balance( + coin_ref: VMValue, + modify: impl FnOnce(u64) -> Result, +) -> Result<(), ExecutionError> { + let balance_value_ref = borrow_coin_ref_balance_value(coin_ref)?; + let reference: values::Reference = balance_value_ref + .copy_value() + .map_err(iv("copy"))? + .cast() + .map_err(iv("cast"))?; + let balance: u64 = reference + .read_ref() + .map_err(iv("read ref"))? + .cast() + .map_err(iv("cast"))?; + let new_balance = modify(balance)?; + let reference: values::Reference = balance_value_ref.cast().map_err(iv("cast"))?; + reference + .write_ref(VMValue::u64(new_balance)) + .map_err(iv("write ref")) +} + +fn borrow_coin_ref_balance_value(coin_ref: VMValue) -> Result { + let coin_ref: values::StructRef = coin_ref.cast().map_err(iv("cast"))?; + let balance = coin_ref.borrow_field(1).map_err(iv("borrow field"))?; + let balance: values::StructRef = balance.cast().map_err(iv("cast"))?; + balance.borrow_field(0).map_err(iv("borrow field")) +} + +//************************************************************************************************** +// Upgrades +//************************************************************************************************** + +impl Value { + pub fn upgrade_cap(cap: UpgradeCap) -> Self { + // public struct UpgradeCap has key, store { + // id: UID, + // package: ID, + // version: u64, + // policy: u8, + // } + let UpgradeCap { + id, + package, + version, + policy, + } = cap; + Self(VMValue::struct_(Struct::pack([ + Self::uid(id.id.bytes.into()).0, + Self::id(package.bytes.into()).0, + VMValue::u64(version), + VMValue::u8(policy), + ]))) + } + + pub fn upgrade_receipt(receipt: UpgradeReceipt) -> Self { + // public struct UpgradeReceipt { + // cap: ID, + // package: ID, + // } + let UpgradeReceipt { cap, package } = receipt; + Self(VMValue::struct_(Struct::pack([ + Self::id(cap.bytes.into()).0, + Self::id(package.bytes.into()).0, + ]))) + } + + pub fn into_upgrade_ticket(self) -> Result { + // public struct UpgradeTicket { + // cap: ID, + // package: ID, + // policy: u8, + // digest: vector, + // } + // unpack UpgradeTicket + let [cap, package, policy, digest] = unpack(self.0)?; + // unpack cap ID + let [cap] = unpack(cap)?; + let cap: AccountAddress = cap.cast().map_err(iv("cast"))?; + // unpack package ID + let [package] = unpack(package)?; + let package: AccountAddress = package.cast().map_err(iv("cast"))?; + // unpack policy + let policy: u8 = policy.cast().map_err(iv("cast"))?; + // unpack digest + let digest: Vec = digest.cast().map_err(iv("cast"))?; + Ok(UpgradeTicket { + cap: sui_types::id::ID::new(cap.into()), + package: sui_types::id::ID::new(package.into()), + policy, + digest, + }) + } +} + +fn unpack(value: VMValue) -> Result<[VMValue; N], ExecutionError> { + let value: values::Struct = value.cast().map_err(iv("cast"))?; + let unpacked = value.unpack().map_err(iv("unpack"))?.collect::>(); + assert_invariant!(unpacked.len() == N, "Expected {N} fields, got {unpacked:?}"); + Ok(unpacked.try_into().unwrap()) +} + +const fn iv(case: &str) -> impl FnOnce(PartialVMError) -> ExecutionError + use<'_> { + move |e| make_invariant_violation!("unexpected {case} failure {e:?}") +} diff --git a/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/linkage/analysis.rs b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/linkage/analysis.rs new file mode 100644 index 0000000000000..6322d013ba641 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/linkage/analysis.rs @@ -0,0 +1,107 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + data_store::PackageStore, + execution_mode::ExecutionMode, + static_programmable_transactions::linkage::{ + config::{LinkageConfig, ResolutionConfig}, + resolution::{ResolutionTable, VersionConstraint, add_and_unify, get_package}, + resolved_linkage::ResolvedLinkage, + }, +}; +use sui_protocol_config::ProtocolConfig; +use sui_types::{base_types::ObjectID, error::ExecutionError, transaction as P}; + +#[derive(Debug)] +pub struct LinkageAnalyzer { + internal: ResolutionConfig, +} + +impl LinkageAnalyzer { + pub fn new( + protocol_config: &ProtocolConfig, + ) -> Result { + let always_include_system_packages = !Mode::packages_are_predefined(); + let linkage_config = LinkageConfig::legacy_linkage_settings(always_include_system_packages); + let binary_config = protocol_config.binary_config(None); + Ok(Self { + internal: ResolutionConfig { + linkage_config, + binary_config, + }, + }) + } + + pub fn compute_call_linkage( + &self, + move_call: &P::ProgrammableMoveCall, + store: &dyn PackageStore, + ) -> Result { + Ok(ResolvedLinkage::from_resolution_table( + self.compute_call_linkage_(move_call, store)?, + )) + } + + pub fn compute_publication_linkage( + &self, + deps: &[ObjectID], + store: &dyn PackageStore, + ) -> Result { + Ok(ResolvedLinkage::from_resolution_table( + self.compute_publication_linkage_(deps, store)?, + )) + } + + pub fn config(&self) -> &ResolutionConfig { + &self.internal + } + + fn compute_call_linkage_( + &self, + move_call: &P::ProgrammableMoveCall, + store: &dyn PackageStore, + ) -> Result { + let mut resolution_table = self + .internal + .linkage_config + .resolution_table_with_native_packages(store)?; + let pkg = get_package(&move_call.package, store)?; + let transitive_deps = pkg + .linkage_table() + .values() + .map(|info| info.upgraded_id) + .collect::>(); + for object_id in transitive_deps.iter() { + add_and_unify( + object_id, + store, + &mut resolution_table, + VersionConstraint::exact, + )?; + } + add_and_unify( + &move_call.package, + store, + &mut resolution_table, + VersionConstraint::exact, + )?; + Ok(resolution_table) + } + + /// Compute the linkage for a publish or upgrade command. This is a special case because + fn compute_publication_linkage_( + &self, + deps: &[ObjectID], + store: &dyn PackageStore, + ) -> Result { + let mut resolution_table = self + .internal + .linkage_config + .resolution_table_with_native_packages(store)?; + for id in deps { + add_and_unify(id, store, &mut resolution_table, VersionConstraint::exact)?; + } + Ok(resolution_table) + } +} diff --git a/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/linkage/config.rs b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/linkage/config.rs new file mode 100644 index 0000000000000..af0d86a27b2b0 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/linkage/config.rs @@ -0,0 +1,79 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + data_store::PackageStore, + static_programmable_transactions::linkage::resolution::{ + ResolutionTable, VersionConstraint, add_and_unify, get_package, + }, +}; +use move_binary_format::binary_config::BinaryConfig; +use sui_types::{ + MOVE_STDLIB_PACKAGE_ID, SUI_FRAMEWORK_PACKAGE_ID, SUI_SYSTEM_PACKAGE_ID, base_types::ObjectID, + error::ExecutionError, +}; + +/// These are the set of native packages in Sui -- importantly they can be used implicitly by +/// different parts of the system and are not required to be explicitly imported always. +/// Additionally, there is no versioning concerns around these as they are "stable" for a given +/// epoch, and are the special packages that are always available, and updated in-place. +const NATIVE_PACKAGE_IDS: &[ObjectID] = &[ + SUI_FRAMEWORK_PACKAGE_ID, + SUI_SYSTEM_PACKAGE_ID, + MOVE_STDLIB_PACKAGE_ID, +]; + +/// Metadata and shared operations for the PTB linkage analysis. +#[derive(Debug)] +pub struct ResolutionConfig { + /// Config to use for the linkage analysis. + pub linkage_config: LinkageConfig, + /// Config to use for the binary analysis (needed for deserialization to determine if a + /// function is a non-public entry function). + pub binary_config: BinaryConfig, +} + +/// Configuration for the linkage analysis. +#[derive(Debug)] +pub struct LinkageConfig { + /// Whether system packages should always be included as a member in the generated linkage. + /// This is almost always true except for system transactions and genesis transactions. + pub always_include_system_packages: bool, +} + +impl ResolutionConfig { + pub fn new(linkage_config: LinkageConfig, binary_config: BinaryConfig) -> Self { + Self { + linkage_config, + binary_config, + } + } +} + +impl LinkageConfig { + pub fn legacy_linkage_settings(always_include_system_packages: bool) -> Self { + Self { + always_include_system_packages, + } + } + + pub(crate) fn resolution_table_with_native_packages( + &self, + store: &dyn PackageStore, + ) -> Result { + let mut resolution_table = ResolutionTable::empty(); + if self.always_include_system_packages { + for id in NATIVE_PACKAGE_IDS { + #[cfg(debug_assertions)] + { + let package = get_package(id, store)?; + debug_assert_eq!(package.id(), *id); + debug_assert_eq!(package.original_package_id(), *id); + } + add_and_unify(id, store, &mut resolution_table, VersionConstraint::exact)?; + } + } + + Ok(resolution_table) + } +} diff --git a/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/linkage/mod.rs b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/linkage/mod.rs new file mode 100644 index 0000000000000..e0866c72ad1a3 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/linkage/mod.rs @@ -0,0 +1,7 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +pub mod analysis; +pub mod config; +pub mod resolution; +pub mod resolved_linkage; diff --git a/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/linkage/resolution.rs b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/linkage/resolution.rs new file mode 100644 index 0000000000000..2df99c623db23 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/linkage/resolution.rs @@ -0,0 +1,166 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::data_store::PackageStore; +use std::{ + collections::{BTreeMap, btree_map::Entry}, + rc::Rc, +}; +use sui_types::{ + base_types::{ObjectID, SequenceNumber}, + error::{ExecutionError, ExecutionErrorKind}, + move_package::MovePackage, +}; + +/// Unifiers. These are used to determine how to unify two packages. +#[derive(Debug, Clone)] +pub enum VersionConstraint { + /// An exact constraint unifies as follows: + /// 1. Exact(a) ~ Exact(b) ==> Exact(a), iff a == b + /// 2. Exact(a) ~ AtLeast(b) ==> Exact(a), iff a >= b + Exact(SequenceNumber, ObjectID), + /// An at least constraint unifies as follows: + /// * AtLeast(a, a_version) ~ AtLeast(b, b_version) ==> AtLeast(x, max(a_version, b_version)), + /// where x is the package id of either a or b (the one with the greatest version). + AtLeast(SequenceNumber, ObjectID), +} + +#[derive(Debug, Clone)] +pub(crate) struct ResolutionTable { + pub(crate) resolution_table: BTreeMap, + /// For every version of every package that we have seen, a mapping of the ObjectID for that + /// package to its runtime ID. + pub(crate) all_versions_resolution_table: BTreeMap, +} + +impl ResolutionTable { + pub fn empty() -> Self { + Self { + resolution_table: BTreeMap::new(), + all_versions_resolution_table: BTreeMap::new(), + } + } +} + +impl VersionConstraint { + pub fn exact(pkg: &MovePackage) -> Option { + Some(VersionConstraint::Exact(pkg.version(), pkg.id())) + } + + pub fn at_least(pkg: &MovePackage) -> Option { + Some(VersionConstraint::AtLeast(pkg.version(), pkg.id())) + } + + pub fn unify(&self, other: &VersionConstraint) -> Result { + match (&self, other) { + // If we have two exact resolutions, they must be the same. + (VersionConstraint::Exact(sv, self_id), VersionConstraint::Exact(ov, other_id)) => { + if self_id != other_id || sv != ov { + Err(ExecutionError::new_with_source( + ExecutionErrorKind::InvalidLinkage, + format!( + "exact/exact conflicting resolutions for package: linkage requires the same package \ + at different versions. Linkage requires exactly {self_id} (version {sv}) and \ + {other_id} (version {ov}) to be used in the same transaction" + ), + )) + } else { + Ok(VersionConstraint::Exact(*sv, *self_id)) + } + } + // Take the max if you have two at least resolutions. + ( + VersionConstraint::AtLeast(self_version, sid), + VersionConstraint::AtLeast(other_version, oid), + ) => { + let id = if self_version > other_version { + *sid + } else { + *oid + }; + + Ok(VersionConstraint::AtLeast( + *self_version.max(other_version), + id, + )) + } + // If you unify an exact and an at least, the exact must be greater than or equal to + // the at least. It unifies to an exact. + ( + VersionConstraint::Exact(exact_version, exact_id), + VersionConstraint::AtLeast(at_least_version, at_least_id), + ) + | ( + VersionConstraint::AtLeast(at_least_version, at_least_id), + VersionConstraint::Exact(exact_version, exact_id), + ) => { + if exact_version < at_least_version { + return Err(ExecutionError::new_with_source( + ExecutionErrorKind::InvalidLinkage, + format!( + "Exact/AtLeast conflicting resolutions for package: linkage requires exactly this \ + package {exact_id} (version {exact_version}) and also at least the following \ + version of the package {at_least_id} at version {at_least_version}. However \ + {exact_id} is at version {exact_version} which is less than {at_least_version}." + ), + )); + } + + Ok(VersionConstraint::Exact(*exact_version, *exact_id)) + } + } + } +} + +/// Load a package from the store, and update the type origin map with the types in that +/// package. +pub(crate) fn get_package( + object_id: &ObjectID, + store: &dyn PackageStore, +) -> Result, ExecutionError> { + store + .get_package(object_id) + .map_err(|e| { + ExecutionError::new_with_source(ExecutionErrorKind::PublishUpgradeMissingDependency, e) + })? + .ok_or_else(|| ExecutionError::from_kind(ExecutionErrorKind::InvalidLinkage)) +} + +// Add a package to the unification table, unifying it with any existing package in the table. +// Errors if the packages cannot be unified (e.g., if one is exact and the other is not). +pub(crate) fn add_and_unify( + object_id: &ObjectID, + store: &dyn PackageStore, + resolution_table: &mut ResolutionTable, + resolution_fn: fn(&MovePackage) -> Option, +) -> Result<(), ExecutionError> { + let package = get_package(object_id, store)?; + + let Some(resolution) = resolution_fn(&package) else { + // If the resolution function returns None, we do not need to add this package to the + // resolution table, and this does not contribute to the linkage analysis. + return Ok(()); + }; + let original_pkg_id = package.original_package_id(); + + if let Entry::Vacant(e) = resolution_table.resolution_table.entry(original_pkg_id) { + e.insert(resolution); + } else { + let existing_unifier = resolution_table + .resolution_table + .get_mut(&original_pkg_id) + .expect("Guaranteed to exist"); + *existing_unifier = existing_unifier.unify(&resolution)?; + } + + if !resolution_table + .all_versions_resolution_table + .contains_key(object_id) + { + resolution_table + .all_versions_resolution_table + .insert(*object_id, original_pkg_id); + } + + Ok(()) +} diff --git a/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/linkage/resolved_linkage.rs b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/linkage/resolved_linkage.rs new file mode 100644 index 0000000000000..8e17fc75112ce --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/linkage/resolved_linkage.rs @@ -0,0 +1,117 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + data_store::PackageStore, + static_programmable_transactions::linkage::resolution::{ + ResolutionTable, VersionConstraint, add_and_unify, get_package, + }, +}; +use move_core_types::account_address::AccountAddress; +use std::{collections::BTreeMap, rc::Rc}; +use sui_types::{base_types::ObjectID, error::ExecutionError}; + +#[derive(Clone, Debug)] +pub struct RootedLinkage { + pub link_context: AccountAddress, + pub resolved_linkage: Rc, +} + +impl RootedLinkage { + pub fn new(link_context: AccountAddress, resolved_linkage: ResolvedLinkage) -> RootedLinkage { + Self { + link_context, + resolved_linkage: Rc::new(resolved_linkage), + } + } + + /// We need to late-bind the "self" resolution since for publication and upgrade we don't know + /// this a priori when loading the PTB. + pub fn new_for_publication( + link_context: ObjectID, + original_package_id: ObjectID, + mut resolved_linkage: ResolvedLinkage, + ) -> RootedLinkage { + // original package ID maps to the link context (new package ID) in this context + resolved_linkage + .linkage + .insert(original_package_id, link_context); + // Add resolution from the new package ID to the original package ID. + resolved_linkage + .linkage_resolution + .insert(link_context, original_package_id); + let resolved_linkage = Rc::new(resolved_linkage); + Self { + link_context: *link_context, + resolved_linkage, + } + } +} + +#[derive(Debug)] +pub struct ResolvedLinkage { + pub linkage: BTreeMap, + // A mapping of every package ID to its runtime ID. + // Note: Multiple packages can have the same runtime ID in this mapping, and domain of this map + // is a superset of range of `linkage`. + pub linkage_resolution: BTreeMap, +} + +impl ResolvedLinkage { + /// In the current linkage resolve an object ID to its original package ID. + pub fn resolve_to_original_id(&self, object_id: &ObjectID) -> Option { + self.linkage_resolution.get(object_id).copied() + } + + /// Given a list of object IDs, generate a `ResolvedLinkage` for them. + /// Since this linkage analysis should only be used for types, all packages are resolved + /// "upwards" (i.e., later versions of the package are preferred). + pub fn type_linkage( + ids: &[ObjectID], + store: &dyn PackageStore, + ) -> Result { + let mut resolution_table = ResolutionTable::empty(); + for id in ids { + let pkg = get_package(id, store)?; + let transitive_deps = pkg + .linkage_table() + .values() + .map(|info| info.upgraded_id) + .collect::>(); + let package_id = pkg.id(); + add_and_unify( + &package_id, + store, + &mut resolution_table, + VersionConstraint::at_least, + )?; + for object_id in transitive_deps.iter() { + add_and_unify( + object_id, + store, + &mut resolution_table, + VersionConstraint::at_least, + )?; + } + } + + Ok(ResolvedLinkage::from_resolution_table(resolution_table)) + } + + /// Create a `ResolvedLinkage` from a `ResolutionTable`. + pub(crate) fn from_resolution_table(resolution_table: ResolutionTable) -> Self { + let mut linkage = BTreeMap::new(); + for (original_id, resolution) in resolution_table.resolution_table { + match resolution { + VersionConstraint::Exact(_version, object_id) + | VersionConstraint::AtLeast(_version, object_id) => { + linkage.insert(original_id, object_id); + } + } + } + Self { + linkage, + linkage_resolution: resolution_table.all_versions_resolution_table, + } + } +} diff --git a/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/loading/ast.rs b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/loading/ast.rs new file mode 100644 index 0000000000000..6b8a938276309 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/loading/ast.rs @@ -0,0 +1,349 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::static_programmable_transactions::linkage::resolved_linkage::{ + ResolvedLinkage, RootedLinkage, +}; +use indexmap::IndexSet; +use move_binary_format::file_format::{AbilitySet, CodeOffset, FunctionDefinitionIndex}; +use move_core_types::{ + account_address::AccountAddress, + identifier::IdentStr, + language_storage::{ModuleId, StructTag}, + u256::U256, +}; +use std::rc::Rc; +use sui_types::{ + Identifier, TypeTag, + base_types::{ObjectID, ObjectRef, RESOLVED_TX_CONTEXT, SequenceNumber, TxContextKind}, +}; + +//************************************************************************************************** +// AST Nodes +//************************************************************************************************** + +#[derive(Debug)] +pub struct Transaction { + pub inputs: Inputs, + pub commands: Commands, +} + +pub type Inputs = Vec<(InputArg, InputType)>; + +pub type Commands = Vec; + +#[derive(Debug)] +#[cfg_attr(debug_assertions, derive(Clone))] +pub enum InputArg { + Pure(Vec), + Receiving(ObjectRef), + Object(ObjectArg), + FundsWithdrawal(FundsWithdrawalArg), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SharedObjectKind { + Legacy, + Party, +} + +#[derive(Debug)] +#[cfg_attr(debug_assertions, derive(Clone))] +pub enum ObjectArg { + ImmObject(ObjectRef), + OwnedObject(ObjectRef), + SharedObject { + id: ObjectID, + initial_shared_version: SequenceNumber, + mutability: ObjectMutability, + kind: SharedObjectKind, + }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ObjectMutability { + Mutable, + Immutable, + NonExclusiveWrite, +} + +#[derive(Debug)] +#[cfg_attr(debug_assertions, derive(Clone))] +pub struct FundsWithdrawalArg { + /// The full type `sui::funds_accumulator::Withdrawal` + pub ty: Type, + pub owner: AccountAddress, + /// This amount is verified to be <= the max for the type described by the `T` in `ty` + pub amount: U256, +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Type { + Bool, + U8, + U16, + U32, + U64, + U128, + U256, + Address, + Signer, + Vector(Rc), + Datatype(Rc), + Reference(/* is mut */ bool, Rc), +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Vector { + pub abilities: AbilitySet, + pub element_type: Type, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Datatype { + pub abilities: AbilitySet, + pub module: ModuleId, + pub name: Identifier, + pub type_arguments: Vec, +} + +#[derive(Debug, Clone)] +pub enum InputType { + Bytes, + Fixed(Type), +} + +#[derive(Debug)] +pub enum Command { + MoveCall(Box), + TransferObjects(Vec, Argument), + SplitCoins(Argument, Vec), + MergeCoins(Argument, Vec), + MakeMoveVec(/* T for vector */ Option, Vec), + Publish(Vec>, Vec, ResolvedLinkage), + Upgrade( + Vec>, + Vec, + ObjectID, + Argument, + ResolvedLinkage, + ), +} + +#[derive(Debug)] +pub struct LoadedFunctionInstantiation { + pub parameters: Vec, + pub return_: Vec, +} + +#[derive(Debug)] +pub struct LoadedFunction { + pub storage_id: ModuleId, + pub runtime_id: ModuleId, + pub name: Identifier, + pub type_arguments: Vec, + pub signature: LoadedFunctionInstantiation, + pub linkage: RootedLinkage, + pub instruction_length: CodeOffset, + pub definition_index: FunctionDefinitionIndex, +} + +#[derive(Debug)] +pub struct MoveCall { + pub function: LoadedFunction, + pub arguments: Vec, +} + +pub use sui_types::transaction::Argument; + +//************************************************************************************************** +// impl +//************************************************************************************************** + +impl ObjectArg { + pub fn id(&self) -> ObjectID { + match self { + ObjectArg::ImmObject(oref) | ObjectArg::OwnedObject(oref) => oref.0, + ObjectArg::SharedObject { id, .. } => *id, + } + } + + pub fn mutability(&self) -> ObjectMutability { + match self { + ObjectArg::ImmObject(_) => ObjectMutability::Immutable, + ObjectArg::OwnedObject(_) => ObjectMutability::Mutable, + ObjectArg::SharedObject { mutability, .. } => *mutability, + } + } +} + +impl Type { + pub fn abilities(&self) -> AbilitySet { + match self { + Type::Bool + | Type::U8 + | Type::U16 + | Type::U32 + | Type::U64 + | Type::U128 + | Type::U256 + | Type::Address => AbilitySet::PRIMITIVES, + Type::Signer => AbilitySet::SIGNER, + Type::Reference(_, _) => AbilitySet::REFERENCES, + Type::Vector(v) => v.abilities, + Type::Datatype(dt) => dt.abilities, + } + } + + pub fn is_tx_context(&self) -> TxContextKind { + let (is_mut, inner) = match self { + Type::Reference(is_mut, inner) => (*is_mut, inner), + _ => return TxContextKind::None, + }; + let Type::Datatype(dt) = &**inner else { + return TxContextKind::None; + }; + if dt.qualified_ident() == RESOLVED_TX_CONTEXT { + if is_mut { + TxContextKind::Mutable + } else { + TxContextKind::Immutable + } + } else { + TxContextKind::None + } + } + pub fn all_addresses(&self) -> IndexSet { + match self { + Type::Bool + | Type::U8 + | Type::U16 + | Type::U32 + | Type::U64 + | Type::U128 + | Type::U256 + | Type::Address + | Type::Signer => IndexSet::new(), + Type::Vector(v) => v.element_type.all_addresses(), + Type::Reference(_, inner) => inner.all_addresses(), + Type::Datatype(dt) => dt.all_addresses(), + } + } + + pub fn node_count(&self) -> u64 { + use Type::*; + let mut total = 0u64; + let mut stack = vec![self]; + + while let Some(ty) = stack.pop() { + total = total.saturating_add(1); + match ty { + Bool | U8 | U16 | U32 | U64 | U128 | U256 | Address | Signer => {} + Vector(v) => stack.push(&v.element_type), + Reference(_, inner) => stack.push(inner), + Datatype(dt) => { + stack.extend(&dt.type_arguments); + } + } + } + + total + } + + pub fn is_reference(&self) -> bool { + match self { + Type::Bool + | Type::U8 + | Type::U16 + | Type::U32 + | Type::U64 + | Type::U128 + | Type::U256 + | Type::Address + | Type::Signer + | Type::Vector(_) + | Type::Datatype(_) => false, + Type::Reference(_, _) => true, + } + } +} + +impl Datatype { + pub fn qualified_ident(&self) -> (&AccountAddress, &IdentStr, &IdentStr) { + ( + self.module.address(), + self.module.name(), + self.name.as_ident_str(), + ) + } + + pub fn all_addresses(&self) -> IndexSet { + let mut addresses = IndexSet::new(); + addresses.insert(*self.module.address()); + for arg in &self.type_arguments { + addresses.extend(arg.all_addresses()); + } + addresses + } +} + +//************************************************************************************************** +// Traits +//************************************************************************************************** + +impl TryFrom for TypeTag { + type Error = &'static str; + fn try_from(ty: Type) -> Result { + Ok(match ty { + Type::Bool => TypeTag::Bool, + Type::U8 => TypeTag::U8, + Type::U16 => TypeTag::U16, + Type::U32 => TypeTag::U32, + Type::U64 => TypeTag::U64, + Type::U128 => TypeTag::U128, + Type::U256 => TypeTag::U256, + Type::Address => TypeTag::Address, + Type::Signer => TypeTag::Signer, + Type::Vector(inner) => { + let Vector { element_type, .. } = &*inner; + TypeTag::Vector(Box::new(element_type.clone().try_into()?)) + } + Type::Datatype(dt) => { + let dt: &Datatype = &dt; + TypeTag::Struct(Box::new(dt.try_into()?)) + } + Type::Reference(_, _) => return Err("unexpected reference type"), + }) + } +} + +impl TryFrom<&Datatype> for StructTag { + type Error = &'static str; + + fn try_from(dt: &Datatype) -> Result { + let Datatype { + module, + name, + type_arguments, + .. + } = dt; + Ok(StructTag { + address: *module.address(), + module: module.name().to_owned(), + name: name.to_owned(), + type_params: type_arguments + .iter() + .map(|t| t.clone().try_into()) + .collect::, _>>()?, + }) + } +} + +//************************************************************************************************** +// Tests +//************************************************************************************************** + +#[test] +fn enum_size() { + assert_eq!(std::mem::size_of::(), 16); +} diff --git a/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/loading/mod.rs b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/loading/mod.rs new file mode 100644 index 0000000000000..5945b5e1919b5 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/loading/mod.rs @@ -0,0 +1,5 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +pub mod ast; +pub mod translate; diff --git a/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/loading/translate.rs b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/loading/translate.rs new file mode 100644 index 0000000000000..c3750b59943d9 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/loading/translate.rs @@ -0,0 +1,199 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::static_programmable_transactions::{ + env::Env, + linkage::resolved_linkage::RootedLinkage, + loading::ast as L, + metering::{self, translation_meter::TranslationMeter}, +}; +use move_core_types::{account_address::AccountAddress, language_storage::StructTag, u256::U256}; +use sui_types::{ + base_types::TxContext, + error::ExecutionError, + object::Owner, + transaction::{self as P, CallArg, FundsWithdrawalArg, ObjectArg, SharedObjectMutability}, +}; + +pub fn transaction( + meter: &mut TranslationMeter<'_, '_>, + env: &Env, + tx_context: &TxContext, + pt: P::ProgrammableTransaction, +) -> Result { + metering::pre_translation::meter(meter, &pt)?; + let P::ProgrammableTransaction { inputs, commands } = pt; + let inputs = inputs + .into_iter() + .map(|arg| input(env, tx_context, arg)) + .collect::, _>>()?; + let commands = commands + .into_iter() + .enumerate() + .map(|(idx, cmd)| command(env, cmd).map_err(|e| e.with_command_index(idx))) + .collect::, _>>()?; + let loaded_tx = L::Transaction { inputs, commands }; + metering::loading::meter(meter, &loaded_tx)?; + Ok(loaded_tx) +} + +fn input( + env: &Env, + tx_context: &TxContext, + arg: CallArg, +) -> Result<(L::InputArg, L::InputType), ExecutionError> { + Ok(match arg { + CallArg::Pure(bytes) => (L::InputArg::Pure(bytes), L::InputType::Bytes), + CallArg::Object(ObjectArg::Receiving(oref)) => { + (L::InputArg::Receiving(oref), L::InputType::Bytes) + } + CallArg::Object(ObjectArg::ImmOrOwnedObject(oref)) => { + let id = &oref.0; + let obj = env.read_object(id)?; + let Some(ty) = obj.type_() else { + invariant_violation!("Object {:?} has does not have a Move type", id); + }; + let tag: StructTag = ty.clone().into(); + let ty = env.load_type_from_struct(&tag)?; + let arg = match obj.owner { + Owner::AddressOwner(_) => L::ObjectArg::OwnedObject(oref), + Owner::Immutable => L::ObjectArg::ImmObject(oref), + Owner::ObjectOwner(_) + | Owner::Shared { .. } + | Owner::ConsensusAddressOwner { .. } => { + invariant_violation!("Unexpected owner for ImmOrOwnedObject: {:?}", obj.owner); + } + }; + (L::InputArg::Object(arg), L::InputType::Fixed(ty)) + } + CallArg::Object(ObjectArg::SharedObject { + id, + initial_shared_version, + mutability, + }) => { + let obj = env.read_object(&id)?; + let Some(ty) = obj.type_() else { + invariant_violation!("Object {:?} has does not have a Move type", id); + }; + let tag: StructTag = ty.clone().into(); + let ty = env.load_type_from_struct(&tag)?; + let kind = match obj.owner { + Owner::AddressOwner(_) | Owner::ObjectOwner(_) | Owner::Immutable => { + invariant_violation!("Unexpected owner for SharedObject: {:?}", obj.owner) + } + Owner::Shared { .. } => L::SharedObjectKind::Legacy, + Owner::ConsensusAddressOwner { .. } => L::SharedObjectKind::Party, + }; + ( + L::InputArg::Object(L::ObjectArg::SharedObject { + id, + initial_shared_version, + mutability: object_mutability(mutability), + kind, + }), + L::InputType::Fixed(ty), + ) + } + CallArg::FundsWithdrawal(f) => { + let FundsWithdrawalArg { + reservation, + type_arg, + withdraw_from, + } = f; + let amount = match reservation { + P::Reservation::EntireBalance => { + invariant_violation!("Entire balance reservation amount is not yet supported") + } + P::Reservation::MaxAmountU64(u) => U256::from(u), + // TODO when types other than u64 are supported, we must check that this is a + // valid amount for the type + }; + let funds_ty = match type_arg { + P::WithdrawalTypeArg::Balance(inner) => { + let inner = env.load_type_input(0, inner)?; + env.balance_type(inner)? + } + }; + let ty = env.withdrawal_type(funds_ty.clone())?; + let owner: AccountAddress = match withdraw_from { + P::WithdrawFrom::Sender => tx_context.sender().into(), + P::WithdrawFrom::Sponsor => tx_context + .sponsor() + .ok_or_else(|| { + make_invariant_violation!( + "A sponsor withdrawal requires a sponsor and should have been \ + checked at signing" + ) + })? + .into(), + }; + ( + L::InputArg::FundsWithdrawal(L::FundsWithdrawalArg { + amount, + ty: ty.clone(), + owner, + }), + L::InputType::Fixed(ty), + ) + } + }) +} + +fn object_mutability(mutability: SharedObjectMutability) -> L::ObjectMutability { + match mutability { + SharedObjectMutability::Mutable => L::ObjectMutability::Mutable, + SharedObjectMutability::NonExclusiveWrite => L::ObjectMutability::NonExclusiveWrite, + SharedObjectMutability::Immutable => L::ObjectMutability::Immutable, + } +} + +fn command(env: &Env, command: P::Command) -> Result { + Ok(match command { + P::Command::MoveCall(pmc) => { + let resolved_linkage = env + .linkage_analysis + .compute_call_linkage(&pmc, env.linkable_store)?; + let P::ProgrammableMoveCall { + package, + module, + function: name, + type_arguments: ptype_arguments, + arguments, + } = *pmc; + let linkage = RootedLinkage::new(*package, resolved_linkage); + let type_arguments = ptype_arguments + .into_iter() + .enumerate() + .map(|(idx, ty)| env.load_type_input(idx, ty)) + .collect::, _>>()?; + let function = env.load_function(package, module, name, type_arguments, linkage)?; + L::Command::MoveCall(Box::new(L::MoveCall { + function, + arguments, + })) + } + P::Command::MakeMoveVec(ptype_argument, arguments) => { + let type_argument = ptype_argument + .map(|ty| env.load_type_input(0, ty)) + .transpose()?; + L::Command::MakeMoveVec(type_argument, arguments) + } + P::Command::TransferObjects(objects, address) => { + L::Command::TransferObjects(objects, address) + } + P::Command::SplitCoins(coin, amounts) => L::Command::SplitCoins(coin, amounts), + P::Command::MergeCoins(target, coins) => L::Command::MergeCoins(target, coins), + P::Command::Publish(items, object_ids) => { + let resolved_linkage = env + .linkage_analysis + .compute_publication_linkage(&object_ids, env.linkable_store)?; + L::Command::Publish(items, object_ids, resolved_linkage) + } + P::Command::Upgrade(items, object_ids, object_id, argument) => { + let resolved_linkage = env + .linkage_analysis + .compute_publication_linkage(&object_ids, env.linkable_store)?; + L::Command::Upgrade(items, object_ids, object_id, argument, resolved_linkage) + } + }) +} diff --git a/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/metering/loading.rs b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/metering/loading.rs new file mode 100644 index 0000000000000..e856bb205cc22 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/metering/loading.rs @@ -0,0 +1,64 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::static_programmable_transactions::{ + linkage::resolved_linkage::ResolvedLinkage, loading::ast as L, + metering::translation_meter::TranslationMeter, +}; +use sui_types::error::ExecutionError; + +/// After loading and before type checking we do a pass over the loaded transaction to charge for +/// types that occured in the transaction and were loaded. We simply charge for the number of type +/// nodes that were loaded. +pub fn meter( + meter: &mut TranslationMeter, + transaction: &L::Transaction, +) -> Result<(), ExecutionError> { + let inputs = transaction.inputs.iter().filter_map(|i| match &i.1 { + L::InputType::Bytes => None, + L::InputType::Fixed(ty) => Some(ty), + }); + let commands = transaction.commands.iter().flat_map(command_types); + for ty in inputs.chain(commands) { + meter.charge_num_type_nodes(ty.node_count())?; + } + + for linkage in transaction.commands.iter().filter_map(command_linkage) { + meter.charge_num_linkage_entries(linkage.linkage_resolution.len())?; + } + + Ok(()) +} + +fn command_linkage(cmd: &L::Command) -> Option<&ResolvedLinkage> { + match cmd { + L::Command::Publish(_, _, linkage) | L::Command::Upgrade(_, _, _, _, linkage) => { + Some(linkage) + } + L::Command::MoveCall(call) => Some(&call.function.linkage.resolved_linkage), + L::Command::MakeMoveVec(_, _) + | L::Command::TransferObjects(_, _) + | L::Command::SplitCoins(_, _) + | L::Command::MergeCoins(_, _) => None, + } +} + +fn command_types(cmd: &L::Command) -> Box + '_> { + match cmd { + L::Command::MoveCall(move_call) => Box::new( + move_call + .function + .type_arguments + .iter() + .chain(move_call.function.signature.parameters.iter()) + .chain(move_call.function.signature.return_.iter()), + ), + L::Command::MakeMoveVec(Some(ty), _) => Box::new(std::iter::once(ty)), + L::Command::TransferObjects(_, _) + | L::Command::SplitCoins(_, _) + | L::Command::MergeCoins(_, _) + | L::Command::MakeMoveVec(None, _) + | L::Command::Publish(_, _, _) + | L::Command::Upgrade(_, _, _, _, _) => Box::new(std::iter::empty()), + } +} diff --git a/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/metering/mod.rs b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/metering/mod.rs new file mode 100644 index 0000000000000..6bc24a114ec49 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/metering/mod.rs @@ -0,0 +1,7 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +pub mod loading; +pub mod pre_translation; +pub mod translation_meter; +pub mod typing; diff --git a/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/metering/pre_translation.rs b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/metering/pre_translation.rs new file mode 100644 index 0000000000000..3da5aaa99e745 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/metering/pre_translation.rs @@ -0,0 +1,53 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::static_programmable_transactions::metering::translation_meter::TranslationMeter; +use sui_types::{ + error::ExecutionError, + transaction::{CallArg, Command, ProgrammableTransaction}, +}; + +/// Before loading and type checking, we do a first pass over the transaction to charge for basic +/// properties: +/// - number of inputs and pure input bytes +/// - number of commands and their arguments +/// - for Move calls, count arguments to the function (both value and type arguments) as the +/// "arguments" to charge for for the command. +pub fn meter( + meter: &mut TranslationMeter, + transaction: &ProgrammableTransaction, +) -> Result<(), ExecutionError> { + meter.charge_base_inputs(transaction.inputs.len())?; + + for input in &transaction.inputs { + match input { + CallArg::Pure(bytes) => { + meter.charge_pure_input_bytes(bytes.len())?; + } + CallArg::FundsWithdrawal(_) | CallArg::Object(_) => (), + } + } + + for command in &transaction.commands { + meter.charge_base_command(arguments_len(command))?; + } + + Ok(()) +} + +fn arguments_len(cmd: &Command) -> usize { + match cmd { + Command::MoveCall(call) => call + .type_arguments + .len() + .saturating_add(call.arguments.len()), + Command::TransferObjects(args, _) + | Command::SplitCoins(_, args) + | Command::MergeCoins(_, args) => args.len().saturating_add(1), + Command::Publish(modules, deps) => modules.len().saturating_add(deps.len()), + Command::MakeMoveVec(_, args) => args.len().saturating_add(1), + Command::Upgrade(modules, deps, _, _) => { + modules.len().saturating_add(deps.len()).saturating_add(2) + } + } +} diff --git a/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/metering/translation_meter.rs b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/metering/translation_meter.rs new file mode 100644 index 0000000000000..910b71eca8040 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/metering/translation_meter.rs @@ -0,0 +1,110 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::gas_charger::GasCharger; +use sui_protocol_config::ProtocolConfig; +use sui_types::error::{ExecutionError, ExecutionErrorKind}; + +/// The [`TranslationMeter`] is responsible for metering gas usage for various operations +/// during the translation of a transaction. It interacts with and exposes interfaces to the +/// [`GasCharger`] it holds in order to deduct gas based on the operations performed. +/// +/// It holds a reference to the `ProtocolConfig` to access protocol-specific configuration +/// parameters that may influence gas costs and limits. +pub struct TranslationMeter<'pc, 'gas> { + protocol_config: &'pc ProtocolConfig, + charger: &'gas mut GasCharger, +} + +impl<'pc, 'gas> TranslationMeter<'pc, 'gas> { + pub fn new( + protocol_config: &'pc ProtocolConfig, + gas_charger: &'gas mut GasCharger, + ) -> TranslationMeter<'pc, 'gas> { + TranslationMeter { + protocol_config, + charger: gas_charger, + } + } + + pub fn charge_base_inputs(&mut self, num_inputs: usize) -> Result<(), ExecutionError> { + let amount = (num_inputs as u64) + .max(1) + .saturating_mul(self.protocol_config.translation_per_input_base_charge()); + self.charge(amount) + } + + pub fn charge_pure_input_bytes(&mut self, num_bytes: usize) -> Result<(), ExecutionError> { + let amount = (num_bytes as u64).max(1).saturating_mul( + self.protocol_config + .translation_pure_input_per_byte_charge(), + ); + self.charge(amount) + } + + pub fn charge_base_command(&mut self, num_args: usize) -> Result<(), ExecutionError> { + let amount = (num_args as u64) + .max(1) + .saturating_mul(self.protocol_config.translation_per_command_base_charge()); + self.charge(amount) + } + + /// Charge gas for loading types based on the number of type nodes loaded. + /// The cost is calculated as `num_type_nodes * TYPE_LOAD_PER_NODE_MULTIPLIER`. + /// This function assumes that `num_type_nodes` is non-zero. + pub fn charge_num_type_nodes(&mut self, num_type_nodes: u64) -> Result<(), ExecutionError> { + let amount = num_type_nodes + .max(1) + .saturating_mul(self.protocol_config.translation_per_type_node_charge()); + self.charge(amount) + } + + pub fn charge_num_type_references( + &mut self, + num_type_references: u64, + ) -> Result<(), ExecutionError> { + let amount = self.reference_cost_formula(num_type_references.max(1))?; + let amount = + amount.saturating_mul(self.protocol_config.translation_per_reference_node_charge()); + self.charge(amount) + } + + pub fn charge_num_linkage_entries( + &mut self, + num_linkage_entries: usize, + ) -> Result<(), ExecutionError> { + let amount = (num_linkage_entries as u64) + .saturating_mul(self.protocol_config.translation_per_linkage_entry_charge()) + .max(1); + self.charge(amount) + } + + // We use a non-linear cost function for type references to account for the increased + // complexity they introduce. The cost is calculated as: + // cost = (num_type_references * (num_type_references + 1)) / 2 + // + // Take &self to access protocol config if needed in the future. + fn reference_cost_formula(&self, n: u64) -> Result { + let Some(n_succ) = n.checked_add(1) else { + invariant_violation!("u64 overflow when calculating type reference cost") + }; + Ok(n.saturating_mul(n_succ) / 2) + } + + // Charge gas using a point charge mechanism based on the cumulative number of units charged so + // far. + fn charge(&mut self, amount: u64) -> Result<(), ExecutionError> { + debug_assert!(amount > 0); + self.charger + .move_gas_status_mut() + .deduct_gas(amount.into()) + .map_err(Self::gas_error) + } + + fn gas_error(e: E) -> ExecutionError + where + E: Into>, + { + ExecutionError::new_with_source(ExecutionErrorKind::InsufficientGas, e) + } +} diff --git a/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/metering/typing.rs b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/metering/typing.rs new file mode 100644 index 0000000000000..1bd1ec6cce82b --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/metering/typing.rs @@ -0,0 +1,30 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::static_programmable_transactions::{ + metering::translation_meter::TranslationMeter, typing::ast as T, +}; +use sui_types::{base_types::TxContextKind, error::ExecutionError}; + +/// After loading and type checking, we do a second pass over the typed transaction to charge for +/// type-related properties (before further analysis is done): +/// - number of type nodes (including nested) +/// - number of type references. These are charged non-linearly +pub fn meter( + meter: &mut TranslationMeter, + transaction: &T::Transaction, +) -> Result<(), ExecutionError> { + let mut num_refs: u64 = 0; + let mut num_nodes: u64 = 0; + + for ty in transaction.types() { + if ty.is_reference() && ty.is_tx_context() == TxContextKind::None { + num_refs = num_refs.saturating_add(1); + } + num_nodes = num_nodes.saturating_add(ty.node_count()); + } + + meter.charge_num_type_nodes(num_nodes)?; + meter.charge_num_type_references(num_refs)?; + Ok(()) +} diff --git a/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/mod.rs b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/mod.rs new file mode 100644 index 0000000000000..d373206c67032 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/mod.rs @@ -0,0 +1,75 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#![deny(clippy::arithmetic_side_effects)] + +use crate::{ + data_store::cached_package_store::CachedPackageStore, + execution_mode::ExecutionMode, + execution_value::ExecutionState, + gas_charger::GasCharger, + static_programmable_transactions::{ + env::Env, linkage::analysis::LinkageAnalyzer, metering::translation_meter, + }, +}; +use move_trace_format::format::MoveTraceBuilder; +use move_vm_runtime::move_vm::MoveVM; +use std::{cell::RefCell, rc::Rc, sync::Arc}; +use sui_protocol_config::ProtocolConfig; +use sui_types::{ + base_types::TxContext, error::ExecutionError, execution::ResultWithTimings, + metrics::LimitsMetrics, storage::BackingPackageStore, transaction::ProgrammableTransaction, +}; + +// TODO we might replace this with a new one +pub use crate::data_store::legacy::linkage_view::LinkageView; + +pub mod env; +pub mod execution; +pub mod linkage; +pub mod loading; +pub mod metering; +pub mod spanned; +pub mod typing; + +pub fn execute( + protocol_config: &ProtocolConfig, + metrics: Arc, + vm: &MoveVM, + state_view: &mut dyn ExecutionState, + package_store: &dyn BackingPackageStore, + tx_context: Rc>, + gas_charger: &mut GasCharger, + txn: ProgrammableTransaction, + trace_builder_opt: &mut Option, +) -> ResultWithTimings { + let package_store = CachedPackageStore::new(Box::new(package_store)); + let linkage_analysis = + LinkageAnalyzer::new::(protocol_config).map_err(|e| (e, vec![]))?; + + let mut env = Env::new( + protocol_config, + vm, + state_view, + &package_store, + &linkage_analysis, + ); + let mut translation_meter = + translation_meter::TranslationMeter::new(protocol_config, gas_charger); + + let txn = { + let tx_context_ref = tx_context.borrow(); + loading::translate::transaction(&mut translation_meter, &env, &tx_context_ref, txn) + .map_err(|e| (e, vec![]))? + }; + let txn = typing::translate_and_verify::(&mut translation_meter, &env, txn) + .map_err(|e| (e, vec![]))?; + execution::interpreter::execute::( + &mut env, + metrics, + tx_context, + gas_charger, + txn, + trace_builder_opt, + ) +} diff --git a/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/spanned.rs b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/spanned.rs new file mode 100644 index 0000000000000..400062dd1c35f --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/spanned.rs @@ -0,0 +1,78 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//! This module provides a shared API amongst the ASTs for original command and argument locations. +//! Some translations might reorder or move commands/arguments, and as such we need to annotate +//! the values with the original location + +use std::{ + cmp::Ordering, + fmt, + hash::{Hash, Hasher}, +}; + +#[derive(Copy, Clone)] +pub struct Spanned { + pub idx: u16, + pub value: T, +} + +impl PartialEq for Spanned { + fn eq(&self, other: &Spanned) -> bool { + self.value == other.value + } +} + +impl Eq for Spanned {} + +impl Hash for Spanned { + fn hash(&self, state: &mut H) { + self.value.hash(state); + } +} + +impl PartialOrd for Spanned { + fn partial_cmp(&self, other: &Spanned) -> Option { + self.value.partial_cmp(&other.value) + } +} + +impl Ord for Spanned { + fn cmp(&self, other: &Spanned) -> Ordering { + self.value.cmp(&other.value) + } +} + +impl fmt::Display for Spanned { + fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", &self.value) + } +} + +impl fmt::Debug for Spanned { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", &self.value) + } +} + +/// Function used to have nearly tuple-like syntax for creating a Spanned +pub const fn sp(idx: u16, value: T) -> Spanned { + Spanned { idx, value } +} + +/// Macro used to create a tuple-like pattern match for Spanned +#[macro_export] +macro_rules! sp { + (_, $value:pat) => { + $crate::static_programmable_transactions::spanned::Spanned { value: $value, .. } + }; + ($idx:pat, _) => { + $crate::static_programmable_transactions::spanned::Spanned { idx: $idx, .. } + }; + ($idx:pat, $value:pat) => { + $crate::static_programmable_transactions::spanned::Spanned { + idx: $idx, + value: $value, + } + }; +} diff --git a/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/ast.rs b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/ast.rs new file mode 100644 index 0000000000000..7cb3a9c32f838 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/ast.rs @@ -0,0 +1,317 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::static_programmable_transactions::{ + linkage::resolved_linkage::ResolvedLinkage, loading::ast as L, spanned::Spanned, +}; +use indexmap::IndexSet; +use move_core_types::{account_address::AccountAddress, u256::U256}; +use move_vm_types::values::VectorSpecialization; +use std::{cell::OnceCell, vec}; +use sui_types::base_types::{ObjectID, ObjectRef}; + +//************************************************************************************************** +// AST Nodes +//************************************************************************************************** + +#[derive(Debug)] +pub struct Transaction { + /// Gathered BCS bytes from Pure inputs + pub bytes: IndexSet>, + // All input objects + pub objects: Vec, + /// All Withdrawal inputs + pub withdrawals: Vec, + /// All pure inputs + pub pure: Vec, + /// All receiving inputs + pub receiving: Vec, + pub commands: Commands, +} + +/// The original index into the `input` vector of the transaction, before the inputs were split +/// into their respective categories (objects, pure, or receiving). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct InputIndex(pub u16); + +#[derive(Debug)] +pub struct ObjectInput { + pub original_input_index: InputIndex, + pub arg: ObjectArg, + pub ty: Type, +} + +pub type ByteIndex = usize; + +#[derive(Debug)] +pub struct PureInput { + pub original_input_index: InputIndex, + // A index into `byte` table of BCS bytes + pub byte_index: ByteIndex, + // the type that the BCS bytes will be deserialized into + pub ty: Type, + // Information about where this constraint came from + pub constraint: BytesConstraint, +} + +#[derive(Debug)] +pub struct ReceivingInput { + pub original_input_index: InputIndex, + pub object_ref: ObjectRef, + pub ty: Type, + // Information about where this constraint came from + pub constraint: BytesConstraint, +} + +#[derive(Debug)] +pub struct WithdrawalInput { + pub original_input_index: InputIndex, + /// The full type `sui::funds_accumulator::Withdrawal` + pub ty: Type, + pub owner: AccountAddress, + /// This amount is verified to be <= the max for the type described by the `T` in `ty` + pub amount: U256, +} + +pub type Commands = Vec; + +pub type ObjectArg = L::ObjectArg; + +pub type Type = L::Type; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// Information for a given constraint for input bytes +pub struct BytesConstraint { + /// The command that first added this constraint + pub command: u16, + /// The argument in that command + pub argument: u16, +} + +pub type ResultType = Vec; + +pub type Command = Spanned; + +#[derive(Debug)] +pub struct Command_ { + /// The command + pub command: Command__, + /// The type of the return values of the command + pub result_type: ResultType, + /// Markers to drop unused results from the command. These are inferred based on any usage + /// of the given result `Result(i,j)` after this command. This is leveraged by the borrow + /// checker to remove unused references to allow potentially reuse of parent references. + /// The value at result `j` is unused and can be dropped if `drop_value[j]` is true. + pub drop_values: Vec, + /// The set of object shared object IDs that are consumed by this command. + /// After this command is executed, these objects must be either reshared or deleted. + pub consumed_shared_objects: Vec, +} + +#[derive(Debug)] +pub enum Command__ { + MoveCall(Box), + TransferObjects(Vec, Argument), + SplitCoins(/* Coin */ Type, Argument, Vec), + MergeCoins(/* Coin */ Type, Argument, Vec), + MakeMoveVec(/* T for vector */ Type, Vec), + Publish(Vec>, Vec, ResolvedLinkage), + Upgrade( + Vec>, + Vec, + ObjectID, + Argument, + ResolvedLinkage, + ), +} + +pub type LoadedFunctionInstantiation = L::LoadedFunctionInstantiation; + +pub type LoadedFunction = L::LoadedFunction; + +#[derive(Debug)] +pub struct MoveCall { + pub function: LoadedFunction, + pub arguments: Vec, +} + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] +pub enum Location { + TxContext, + GasCoin, + ObjectInput(u16), + WithdrawalInput(u16), + PureInput(u16), + ReceivingInput(u16), + Result(u16, u16), +} + +// Non borrowing usage of locations, moving or copying +#[derive(Clone, Debug)] +pub enum Usage { + Move(Location), + Copy { + location: Location, + /// Was this location borrowed at the time of copying? + /// Initially empty and populated by `memory_safety` + borrowed: OnceCell, + }, +} + +pub type Argument = Spanned; +pub type Argument_ = (Argument__, Type); + +#[derive(Clone, Debug)] +pub enum Argument__ { + /// Move or copy a value + Use(Usage), + /// Borrow a value, i.e. `&x` or `&mut x` + Borrow(/* mut */ bool, Location), + /// Read a value from a reference, i.e. `*&x` + Read(Usage), + /// Freeze a mutable reference, making an `&t` from `&mut t` + Freeze(Usage), +} + +//************************************************************************************************** +// impl +//************************************************************************************************** + +impl Transaction { + pub fn types(&self) -> impl Iterator { + let pure_types = self.pure.iter().map(|p| &p.ty); + let object_types = self.objects.iter().map(|o| &o.ty); + let receiving_types = self.receiving.iter().map(|r| &r.ty); + let command_types = self.commands.iter().flat_map(command_types); + pure_types + .chain(object_types) + .chain(receiving_types) + .chain(command_types) + } +} + +impl Usage { + pub fn new_move(location: Location) -> Usage { + Usage::Move(location) + } + + pub fn new_copy(location: Location) -> Usage { + Usage::Copy { + location, + borrowed: OnceCell::new(), + } + } + + pub fn location(&self) -> Location { + match self { + Usage::Move(location) => *location, + Usage::Copy { location, .. } => *location, + } + } +} + +impl Argument__ { + pub fn new_move(location: Location) -> Self { + Self::Use(Usage::new_move(location)) + } + + pub fn new_copy(location: Location) -> Self { + Self::Use(Usage::new_copy(location)) + } + + pub fn location(&self) -> Location { + match self { + Self::Use(usage) | Self::Read(usage) => usage.location(), + Self::Borrow(_, location) => *location, + Self::Freeze(usage) => usage.location(), + } + } +} + +impl Command__ { + pub fn arguments(&self) -> Vec<&Argument> { + match self { + Command__::MoveCall(mc) => mc.arguments.iter().collect(), + Command__::TransferObjects(objs, addr) => { + objs.iter().chain(std::iter::once(addr)).collect() + } + Command__::SplitCoins(_, coin, amounts) => { + std::iter::once(coin).chain(amounts).collect() + } + Command__::MergeCoins(_, target, sources) => { + std::iter::once(target).chain(sources).collect() + } + Command__::MakeMoveVec(_, elems) => elems.iter().collect(), + Command__::Publish(_, _, _) => vec![], + Command__::Upgrade(_, _, _, arg, _) => vec![arg], + } + } + + pub fn types(&self) -> Box + '_> { + match self { + Command__::TransferObjects(args, arg) => { + Box::new(std::iter::once(arg).chain(args.iter()).map(argument_type)) + } + Command__::SplitCoins(ty, arg, args) | Command__::MergeCoins(ty, arg, args) => { + Box::new( + std::iter::once(arg) + .chain(args.iter()) + .map(argument_type) + .chain(std::iter::once(ty)), + ) + } + Command__::MakeMoveVec(ty, args) => { + Box::new(args.iter().map(argument_type).chain(std::iter::once(ty))) + } + Command__::MoveCall(call) => Box::new( + call.arguments + .iter() + .map(argument_type) + .chain(call.function.type_arguments.iter()) + .chain(call.function.signature.parameters.iter()) + .chain(call.function.signature.return_.iter()), + ), + Command__::Upgrade(_, _, _, arg, _) => { + Box::new(std::iter::once(arg).map(argument_type)) + } + Command__::Publish(_, _, _) => Box::new(std::iter::empty()), + } + } +} + +//************************************************************************************************** +// Standalone functions +//************************************************************************************************** + +pub fn command_types(cmd: &Command) -> impl Iterator { + let result_types = cmd.value.result_type.iter(); + let command_types = cmd.value.command.types(); + result_types.chain(command_types) +} + +pub fn argument_type(arg: &Argument) -> &Type { + &arg.value.1 +} + +//************************************************************************************************** +// traits +//************************************************************************************************** + +impl TryFrom for VectorSpecialization { + type Error = &'static str; + + fn try_from(value: Type) -> Result { + Ok(match value { + Type::U8 => VectorSpecialization::U8, + Type::U16 => VectorSpecialization::U16, + Type::U32 => VectorSpecialization::U32, + Type::U64 => VectorSpecialization::U64, + Type::U128 => VectorSpecialization::U128, + Type::U256 => VectorSpecialization::U256, + Type::Address => VectorSpecialization::Address, + Type::Bool => VectorSpecialization::Bool, + Type::Signer | Type::Vector(_) | Type::Datatype(_) => VectorSpecialization::Container, + Type::Reference(_, _) => return Err("unexpected reference in vector specialization"), + }) + } +} diff --git a/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/invariant_checks/defining_ids_in_types.rs b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/invariant_checks/defining_ids_in_types.rs new file mode 100644 index 0000000000000..9f510ba067ec3 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/invariant_checks/defining_ids_in_types.rs @@ -0,0 +1,103 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + data_store::PackageStore, + sp, + static_programmable_transactions::{env, spanned::Spanned, typing::ast as T}, +}; +use sui_types::error::ExecutionError; + +pub fn verify(env: &env::Env, tt: &T::Transaction) -> Result<(), ExecutionError> { + let check_type = |ty| ensure_type_defining_id_based(env, ty); + let check_arg = |sp!(_, (_, ty)): &Spanned<_>| ensure_type_defining_id_based(env, ty); + + // Verify all types in inputs are defining-id based. + tt.objects + .iter() + .try_for_each(|object_input| check_type(&object_input.ty))?; + tt.pure + .iter() + .try_for_each(|pure_input| check_type(&pure_input.ty))?; + tt.receiving + .iter() + .try_for_each(|receiving_input| check_type(&receiving_input.ty))?; + + // Verify all types in commands are defining-id based. + tt.commands.iter().try_for_each(|sp!(_, c)| { + c.result_type.iter().try_for_each(check_type)?; + match &c.command { + T::Command__::Publish(_, _, _) => Ok(()), + T::Command__::Upgrade(_, _, _, sp!(_, (_, ty)), _) => check_type(ty), + T::Command__::SplitCoins(ty, sp!(_, (_, coin_ty)), amounts) => { + check_type(ty)?; + check_type(coin_ty)?; + amounts.iter().try_for_each(check_arg) + } + T::Command__::MergeCoins(ty, sp!(_, (_, target_ty)), coins) => { + check_type(ty)?; + check_type(target_ty)?; + coins.iter().try_for_each(check_arg) + } + T::Command__::MakeMoveVec(ty, args) => { + check_type(ty)?; + args.iter().try_for_each(check_arg) + } + T::Command__::TransferObjects(objs, sp!(_, (_, recipient_ty))) => { + objs.iter().try_for_each(check_arg)?; + check_type(recipient_ty) + } + T::Command__::MoveCall(move_call) => { + move_call + .function + .type_arguments + .iter() + .try_for_each(check_type)?; + move_call.arguments.iter().try_for_each(check_arg) + } + } + }) +} + +fn ensure_type_defining_id_based(env: &env::Env, ty: &T::Type) -> Result<(), ExecutionError> { + match ty { + T::Type::Bool + | T::Type::U8 + | T::Type::U16 + | T::Type::U32 + | T::Type::U64 + | T::Type::U128 + | T::Type::U256 + | T::Type::Address + | T::Type::Signer => Ok(()), + T::Type::Reference(_, ty) => ensure_type_defining_id_based(env, ty), + T::Type::Vector(vector) => ensure_type_defining_id_based(env, &vector.element_type), + T::Type::Datatype(datatype) => { + // Resolve the type to its defining ID and ensure it matches the module address that is + // already written down as the defining ID. + // + // If we fail to resolve the type that's an invariant violation as we should be able to + // load the package that defined this type otherwise we should have failed at + // load/typing time. + let Ok(Some(resolved_id)) = env.linkable_store.resolve_type_to_defining_id( + (*datatype.module.address()).into(), + datatype.module.name(), + &datatype.name, + ) else { + invariant_violation!("[defining_ids_in_types] Unable to resolve Type {ty:?}",); + }; + + if *resolved_id != *datatype.module.address() { + invariant_violation!( + "[defining_ids_in_types] Type {ty:?} has a different defining ID {} than expected: {resolved_id}", + datatype.module.address() + ); + } + + datatype + .type_arguments + .iter() + .try_for_each(|arg| ensure_type_defining_id_based(env, arg)) + } + } +} diff --git a/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/invariant_checks/memory_safety.rs b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/invariant_checks/memory_safety.rs new file mode 100644 index 0000000000000..9910207e23b22 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/invariant_checks/memory_safety.rs @@ -0,0 +1,808 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + sp, + static_programmable_transactions::{env::Env, typing::ast as T}, +}; +use indexmap::IndexSet; +use std::rc::Rc; +use sui_types::error::ExecutionError; + +/// A dot-star like extension, but with a unique identifier. Deltas can be compared between +/// different Deltas of the same command, otherwise they behave like .* in the regex based +/// implementation. This means that it represents an arbitrary field extension of the reference +/// in question. However, due to invariants within reference safety, for mutable references these +/// extensions cannot with other references from the same command. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct Delta { + command: u16, + result: u16, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum RootLocation { + /// The result of a command, specifically a `MoveCall`, without any input references. + /// These calls will always abort, but we still must track them. + Unknown { + command: u16, + }, + Known(T::Location), +} + +/// A path points to an abstract memory location, rooted in an input or command result. Any +/// extension is the result from a reference being returned from a command. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct Path { + root: RootLocation, + extensions: Vec, +} + +#[derive(Debug)] +struct PathSet(IndexSet); + +#[derive(Debug)] +enum Value { + NonRef, + Ref { is_mut: bool, paths: Rc }, +} + +#[derive(Debug)] +struct Location { + // A singleton set pointing to the location itself + self_path: Rc, + value: Option, +} + +#[derive(Debug)] +struct Context { + tx_context: Location, + gas: Location, + object_inputs: Vec, + withdrawal_inputs: Vec, + pure_inputs: Vec, + receiving_inputs: Vec, + results: Vec>, + // Temporary set of locations borrowed by arguments seen thus far for the current command. + // Used exclusively for checking the validity copy/move. + arg_roots: IndexSet, +} + +enum PathComparison { + /// `self` is a strict prefix of `other` + Prefix, + /// `self` and `other` are the same path + Aliases, + /// `self` extends `other` + Extends, + /// `self` and `other` point to distinct regions of memory. They might however be rooted + /// in the same parent region, i.e. the same `root` location or a prefix of the same + /// `extensions` + Disjoint, +} + +impl Path { + fn initial(location: T::Location) -> Self { + Self { + root: RootLocation::Known(location), + extensions: vec![], + } + } + + /// See `PathComparison` for the meaning of the return value + fn compare(&self, other: &Self) -> PathComparison { + if self.root != other.root { + return PathComparison::Disjoint; + }; + let mut self_extensions = self.extensions.iter(); + let mut other_extensions = other.extensions.iter(); + loop { + match (self_extensions.next(), other_extensions.next()) { + (Some(self_ext), Some(other_ext)) => { + if self_ext.command != other_ext.command { + // Cannot compare `Delta` from distinct commands, as such we do assume + // the possibility that `self` extends `other` + return PathComparison::Extends; + } + if self_ext.result != other_ext.result { + // If the command is the same, but the result is different, we know that + // they must be disjoint. Or they are immutable references, in which case + // we do not care. + return PathComparison::Disjoint; + } + } + (None, Some(_)) => return PathComparison::Prefix, + (Some(_), None) => return PathComparison::Extends, + (None, None) => return PathComparison::Aliases, + } + } + } + + /// Create a new `Path` that extends the current path with the given `extension`. + fn extend(&self, extension: Delta) -> Self { + let mut new_extensions = self.extensions.clone(); + new_extensions.push(extension); + Self { + root: self.root, + extensions: new_extensions, + } + } +} + +impl PathSet { + /// Should be used only for `call` for creating initial path sets. + fn empty() -> Self { + Self(IndexSet::new()) + } + + fn initial(location: T::Location) -> Self { + Self(IndexSet::from([Path::initial(location)])) + } + + fn unknown_root(command: u16) -> Self { + Self(IndexSet::from([Path { + root: RootLocation::Unknown { command }, + extensions: vec![], + }])) + } + + fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Returns true if any path in `self` `Extends` with any path in `other`. + /// Excludes `Aliases` if `ignore_aliases` is true. + fn extends(&self, other: &Self, ignore_aliases: bool) -> bool { + self.0.iter().any(|self_path| { + other + .0 + .iter() + .any(|other_path| match self_path.compare(other_path) { + PathComparison::Prefix | PathComparison::Disjoint => false, + PathComparison::Aliases => !ignore_aliases, + PathComparison::Extends => true, + }) + }) + } + + /// Returns true if all paths in `self` are `Disjoint` with all paths in `other`. + fn is_disjoint(&self, other: &Self) -> bool { + self.0.iter().all(|self_path| { + other + .0 + .iter() + .all(|other_path| match self_path.compare(other_path) { + PathComparison::Disjoint => true, + PathComparison::Prefix | PathComparison::Aliases | PathComparison::Extends => { + false + } + }) + }) + } + + /// Insert all paths from `other` into `self`. + fn union(&mut self, other: &PathSet) { + // We might be able to optimize this slightly by not including paths that are extensions + // of existing paths + self.0.extend(other.0.iter().cloned()); + } + + /// Create a new `PathSet` where all paths in `self` are extended with the given `extension`. + fn extend(&self, extension: Delta) -> Self { + let mut new_paths = IndexSet::with_capacity(self.0.len()); + for path in &self.0 { + new_paths.insert(path.extend(extension)); + } + Self(new_paths) + } +} + +impl Value { + /// Create a new reference value + fn ref_(is_mut: bool, paths: PathSet) -> anyhow::Result { + anyhow::ensure!( + !paths.is_empty(), + "Cannot create a reference with an empty path set" + ); + Ok(Value::Ref { + is_mut, + paths: Rc::new(paths), + }) + } + + fn copy(&self) -> Value { + match self { + Value::NonRef => Value::NonRef, + Value::Ref { is_mut, paths } => Value::Ref { + is_mut: *is_mut, + paths: paths.clone(), + }, + } + } + + fn freeze(&mut self) -> anyhow::Result { + let copied = self.copy(); + match copied { + Value::NonRef => { + anyhow::bail!("Cannot freeze a non-reference value") + } + Value::Ref { is_mut, paths } => { + anyhow::ensure!(is_mut, "Cannot freeze an immutable reference"); + Ok(Value::Ref { + is_mut: false, + paths, + }) + } + } + } +} + +impl Location { + fn non_ref(location: T::Location) -> Self { + Self { + self_path: Rc::new(PathSet::initial(location)), + value: Some(Value::NonRef), + } + } + + fn copy_value(&self) -> anyhow::Result { + let Some(value) = self.value.as_ref() else { + anyhow::bail!("Use of invalid memory location") + }; + Ok(value.copy()) + } + + fn move_value(&mut self) -> anyhow::Result { + let Some(value) = self.value.take() else { + anyhow::bail!("Use of invalid memory location") + }; + Ok(value) + } + + fn use_(&mut self, usage: &T::Usage) -> anyhow::Result { + match usage { + T::Usage::Move(_) => self.move_value(), + T::Usage::Copy { .. } => self.copy_value(), + } + } + + fn borrow(&mut self, is_mut: bool) -> anyhow::Result { + let Some(value) = self.value.as_ref() else { + anyhow::bail!("Borrow of invalid memory location") + }; + match value { + Value::Ref { .. } => { + anyhow::bail!("Cannot borrow a reference") + } + Value::NonRef => { + anyhow::ensure!( + !self.self_path.is_empty(), + "Cannot have an empty location to borrow from" + ); + // a new reference that borrows from this location + Ok(Value::Ref { + is_mut, + paths: self.self_path.clone(), + }) + } + } + } +} + +impl Context { + fn new(txn: &T::Transaction) -> Self { + let T::Transaction { + bytes: _, + objects, + withdrawals, + pure, + receiving, + commands: _, + } = txn; + let tx_context = Location::non_ref(T::Location::TxContext); + let gas = Location::non_ref(T::Location::GasCoin); + let object_inputs = (0..objects.len()) + .map(|i| Location::non_ref(T::Location::ObjectInput(i as u16))) + .collect(); + let withdrawal_inputs = (0..withdrawals.len()) + .map(|i| Location::non_ref(T::Location::WithdrawalInput(i as u16))) + .collect(); + let pure_inputs = (0..pure.len()) + .map(|i| Location::non_ref(T::Location::PureInput(i as u16))) + .collect(); + let receiving_inputs = (0..receiving.len()) + .map(|i| Location::non_ref(T::Location::ReceivingInput(i as u16))) + .collect(); + Self { + tx_context, + gas, + object_inputs, + withdrawal_inputs, + pure_inputs, + receiving_inputs, + results: vec![], + arg_roots: IndexSet::new(), + } + } + + fn current_command(&self) -> u16 { + self.results.len() as u16 + } + + fn add_result_values(&mut self, results: impl IntoIterator>) { + let command = self.current_command(); + self.results.push( + results + .into_iter() + .enumerate() + .map(|(i, v)| Location { + self_path: Rc::new(PathSet::initial(T::Location::Result(command, i as u16))), + value: v, + }) + .collect(), + ); + } + + fn location(&self, loc: T::Location) -> anyhow::Result<&Location> { + Ok(match loc { + T::Location::TxContext => &self.tx_context, + T::Location::GasCoin => &self.gas, + T::Location::ObjectInput(i) => self + .object_inputs + .get(i as usize) + .ok_or_else(|| anyhow::anyhow!("Object input index out of bounds {i}"))?, + T::Location::WithdrawalInput(i) => self + .withdrawal_inputs + .get(i as usize) + .ok_or_else(|| anyhow::anyhow!("Withdrawal input index out of bounds {i}"))?, + T::Location::PureInput(i) => self + .pure_inputs + .get(i as usize) + .ok_or_else(|| anyhow::anyhow!("Pure input index out of bounds {i}"))?, + T::Location::ReceivingInput(i) => self + .receiving_inputs + .get(i as usize) + .ok_or_else(|| anyhow::anyhow!("Receiving input index out of bounds {i}"))?, + T::Location::Result(i, j) => self + .results + .get(i as usize) + .and_then(|r| r.get(j as usize)) + .ok_or_else(|| anyhow::anyhow!("Result index out of bounds ({i},{j})"))?, + }) + } + + fn location_mut(&mut self, loc: T::Location) -> anyhow::Result<&mut Location> { + Ok(match loc { + T::Location::TxContext => &mut self.tx_context, + T::Location::GasCoin => &mut self.gas, + T::Location::ObjectInput(i) => self + .object_inputs + .get_mut(i as usize) + .ok_or_else(|| anyhow::anyhow!("Object input index out of bounds {i}"))?, + T::Location::WithdrawalInput(i) => self + .withdrawal_inputs + .get_mut(i as usize) + .ok_or_else(|| anyhow::anyhow!("Withdrawal input index out of bounds {i}"))?, + T::Location::PureInput(i) => self + .pure_inputs + .get_mut(i as usize) + .ok_or_else(|| anyhow::anyhow!("Pure input index out of bounds {i}"))?, + T::Location::ReceivingInput(i) => self + .receiving_inputs + .get_mut(i as usize) + .ok_or_else(|| anyhow::anyhow!("Receiving input index out of bounds {i}"))?, + T::Location::Result(i, j) => self + .results + .get_mut(i as usize) + .and_then(|r| r.get_mut(j as usize)) + .ok_or_else(|| anyhow::anyhow!("Result index out of bounds ({i},{j})"))?, + }) + } + + fn check_usage(&self, usage: &T::Usage, location: &Location) -> anyhow::Result<()> { + // by marking "ignore alias" as `false`, we will also check for `Alias` paths, i.e. paths + // that point to the location itself without any extensions. + let is_borrowed = self.any_extends(&location.self_path, /* ignore alias */ false) + || self.arg_roots.contains(&usage.location()); + match usage { + T::Usage::Move(_) => { + anyhow::ensure!(!is_borrowed, "Cannot move a value that is borrowed"); + } + T::Usage::Copy { borrowed, .. } => { + let Some(borrowed) = borrowed.get().copied() else { + anyhow::bail!("Borrowed flag not set for copy usage"); + }; + anyhow::ensure!( + borrowed == is_borrowed, + "Borrowed flag mismatch for copy usage: expected {borrowed}, got {is_borrowed} \ + location {:?} for in command {}", + location.self_path, + self.current_command() + ); + } + } + Ok(()) + } + + fn argument(&mut self, sp!(_, (arg, _)): &T::Argument) -> anyhow::Result { + let location = self.location(arg.location())?; + match arg { + T::Argument__::Use(usage) + | T::Argument__::Freeze(usage) + | T::Argument__::Read(usage) => self.check_usage(usage, location)?, + T::Argument__::Borrow(_, _) => (), + }; + let location = self.location_mut(arg.location())?; + let value = match arg { + T::Argument__::Use(usage) => location.use_(usage)?, + T::Argument__::Freeze(usage) => location.use_(usage)?.freeze()?, + T::Argument__::Borrow(is_mut, _) => location.borrow(*is_mut)?, + T::Argument__::Read(usage) => { + location.use_(usage)?; + Value::NonRef + } + }; + if let Value::Ref { paths, .. } = &value { + for p in &paths.0 { + match p.root { + RootLocation::Unknown { .. } => (), + RootLocation::Known(location) => { + self.arg_roots.insert(location); + } + } + } + } + Ok(value) + } + + fn arguments(&mut self, args: &[T::Argument]) -> anyhow::Result> { + args.iter() + .map(|arg| self.argument(arg)) + .collect::>>() + } + + fn all_references(&self) -> impl Iterator> { + let Self { + tx_context, + gas, + object_inputs, + withdrawal_inputs, + pure_inputs, + receiving_inputs, + results, + arg_roots: _, + } = self; + std::iter::once(tx_context) + .chain(std::iter::once(gas)) + .chain(object_inputs) + .chain(withdrawal_inputs) + .chain(pure_inputs) + .chain(receiving_inputs) + .chain(results.iter().flatten()) + .filter_map(|v| -> Option> { + match v.value.as_ref() { + Some(Value::Ref { paths, .. }) => Some(paths.clone()), + Some(Value::NonRef) | None => None, + } + }) + } + + /// Returns true if any of the references in a given `T::Location` extends a path in `paths`. + /// Excludes `Aliases` if `ignore_aliases` is true. + fn any_extends(&self, paths: &PathSet, ignore_aliases: bool) -> bool { + self.all_references() + .any(|other| other.extends(paths, ignore_aliases)) + } +} + +/// Verifies memory safety of a transaction. This is a re-implementation of `verify::memory_safety` +/// using an alternative approach given the newness of the Regex based borrow graph in that +/// implementation. +/// This is a set based approach were each reference is represent as a set of paths. A path +/// is has a root (basically a `T::Location` plus some edge case massaging) and a list of extensions +/// resulting from Move function calls. Each one of those Move calls gets a `Delta` extension for +/// each return value. The `Delta` is like the ".*" in the regex based implementation but where it +/// carries a sense of identity. This identity allows for invariants from the return values of the +/// Move call to be leveraged. For example, mutable references returned from a call cannot overlap. +/// If we just used ".*", we would not be able to express this invariant without some sense of +/// identity for the reference itself (which is what is going on in the Regex based implementation). +/// This implementation stems from research work for the Move borrow checker, but would normally +/// not be expressive enough in the presence of control flow. Luckily, PTBs do not have control flow +/// so we can use this approach as a safety net for the Regex based implementation until that +/// code is sufficiently. tested and hardened. +/// Checks the following +/// - Values are not used after being moved +/// - Reference safety is upheld (no dangling references) +pub fn verify(_env: &Env, txn: &T::Transaction) -> Result<(), ExecutionError> { + verify_(txn).map_err(|e| make_invariant_violation!("{}. Transaction {:?}", e, txn)) +} + +fn verify_(txn: &T::Transaction) -> anyhow::Result<()> { + let mut context = Context::new(txn); + let T::Transaction { + bytes: _, + objects: _, + withdrawals: _, + pure: _, + receiving: _, + commands, + } = txn; + for c in commands { + command(&mut context, c)?; + } + Ok(()) +} + +fn command(context: &mut Context, c: &T::Command) -> anyhow::Result<()> { + // process the command + debug_assert!(context.arg_roots.is_empty()); + let results = command_(context, c)?; + // drop unused result values by marking them as `None` + assert_invariant!( + results.len() == c.value.drop_values.len(), + "result length mismatch. expected {}, got {}", + c.value.drop_values.len(), + results.len() + ); + context.add_result_values( + results + .into_iter() + .zip(c.value.drop_values.iter().copied()) + .map(|(v, drop)| if drop { None } else { Some(v) }), + ); + context.arg_roots.clear(); + Ok(()) +} + +fn command_(context: &mut Context, sp!(_, c): &T::Command) -> anyhow::Result> { + let result_tys = &c.result_type; + let results = match &c.command { + T::Command__::MoveCall(move_call) => { + let T::MoveCall { + function, + arguments, + } = &**move_call; + let arg_values = context.arguments(arguments)?; + call(context, &function.signature, arg_values)? + } + T::Command__::TransferObjects(objs, recipient) => { + context.arguments(objs)?; + context.argument(recipient)?; + non_ref_results(result_tys)? + } + T::Command__::SplitCoins(_, coin, amounts) => { + context.arguments(amounts)?; + let coin_value = context.argument(coin)?; + write_ref(context, coin_value)?; + non_ref_results(result_tys)? + } + T::Command__::MergeCoins(_, target, coins) => { + context.arguments(coins)?; + let target_value = context.argument(target)?; + write_ref(context, target_value)?; + non_ref_results(result_tys)? + } + T::Command__::MakeMoveVec(_, arguments) => { + context.arguments(arguments)?; + non_ref_results(result_tys)? + } + T::Command__::Publish(_, _, _) => non_ref_results(result_tys)?, + T::Command__::Upgrade(_, _, _, ticket, _) => { + context.argument(ticket)?; + non_ref_results(result_tys)? + } + }; + assert_invariant!( + result_tys.len() == results.len(), + "result length mismatch. Expected {}, got {}", + result_tys.len(), + results.len() + ); + Ok(results) +} + +fn write_ref(context: &Context, value: Value) -> anyhow::Result<()> { + match value { + Value::NonRef => { + anyhow::bail!("Cannot write to a non-reference value"); + } + + Value::Ref { is_mut: false, .. } => { + anyhow::bail!("Cannot write to an immutable reference"); + } + Value::Ref { + is_mut: true, + paths, + } => { + anyhow::ensure!( + !context.any_extends(&paths, /* ignore alias */ true), + "Cannot write to a mutable reference that has extensions" + ); + Ok(()) + } + } +} + +fn call( + context: &mut Context, + signature: &T::LoadedFunctionInstantiation, + arguments: Vec, +) -> anyhow::Result> { + let return_ = &signature.return_; + let mut all_paths: PathSet = PathSet::empty(); + let mut imm_paths: PathSet = PathSet::empty(); + let mut mut_paths: PathSet = PathSet::empty(); + for arg in arguments { + match arg { + Value::NonRef => (), + Value::Ref { + is_mut: true, + paths, + } => { + // Allow alias conflicts with references not passed as arguments + anyhow::ensure!( + !context.any_extends(&paths, /* ignore alias */ true), + "Cannot transfer a mutable ref with extensions" + ); + // All mutable argument references must be disjoint from all other references + anyhow::ensure!(mut_paths.is_disjoint(&paths), "Double mutable borrow"); + all_paths.union(&paths); + mut_paths.union(&paths); + } + Value::Ref { + is_mut: false, + paths, + } => { + all_paths.union(&paths); + imm_paths.union(&paths); + } + } + } + // All mutable references must be disjoint from all immutable references + anyhow::ensure!( + imm_paths.is_disjoint(&mut_paths), + "Mutable and immutable borrows cannot overlap" + ); + let command = context.current_command(); + let mut_paths = if mut_paths.is_empty() { + PathSet::unknown_root(command) + } else { + mut_paths + }; + let all_paths = if all_paths.is_empty() { + PathSet::unknown_root(command) + } else { + all_paths + }; + return_ + .iter() + .enumerate() + .map(|(i, ty)| { + let delta = Delta { + command, + result: i as u16, + }; + match ty { + T::Type::Reference(/* is mut */ true, _) => { + Value::ref_(true, mut_paths.extend(delta)) + } + T::Type::Reference(/* is mut */ false, _) => { + Value::ref_(false, all_paths.extend(delta)) + } + _ => Ok(Value::NonRef), + } + }) + .collect::>>() +} + +fn non_ref_results(results: &[T::Type]) -> anyhow::Result> { + results + .iter() + .map(|t| { + anyhow::ensure!( + !matches!(t, T::Type::Reference(_, _)), + "attempted to create a non-reference result from a reference type", + ); + Ok(Value::NonRef) + }) + .collect() +} + +//************************************************************************************************** +// impl +//************************************************************************************************** + +impl Path { + #[cfg(debug_assertions)] + #[allow(unused)] + fn print(&self) { + print!("{:?}", self.root); + for ext in &self.extensions { + let Delta { command, result } = ext; + print!(".d{}_{}", command, result); + } + println!(","); + } +} + +impl PathSet { + #[cfg(debug_assertions)] + #[allow(unused)] + fn print(&self) { + println!("{{"); + for path in &self.0 { + path.print(); + } + println!("}}"); + } +} + +impl Value { + #[cfg(debug_assertions)] + #[allow(unused)] + fn print(&self) { + match self { + Value::NonRef => print!("NonRef"), + Value::Ref { is_mut, paths } => { + if *is_mut { + print!("mut "); + } else { + print!("imm "); + } + paths.print(); + } + } + } +} + +impl Location { + #[cfg(debug_assertions)] + #[allow(unused)] + fn print(&self) { + print!("{{ self_path: "); + self.self_path.print(); + print!(", value: "); + if let Some(value) = &self.value { + value.print(); + } else { + println!("_"); + } + println!("}}"); + } +} + +impl Context { + #[cfg(debug_assertions)] + #[allow(unused)] + fn print(&self) { + println!("Context {{"); + println!(" tx_context: "); + self.tx_context.print(); + println!(" gas: "); + self.gas.print(); + println!(" object_inputs: ["); + for input in &self.object_inputs { + input.print(); + } + println!(" ],"); + println!(" pure_inputs: ["); + for input in &self.pure_inputs { + input.print(); + } + println!(" ],"); + println!(" receiving_inputs: ["); + for input in &self.receiving_inputs { + input.print(); + } + println!(" ],"); + println!(" results: ["); + for result in &self.results { + for loc in result { + loc.print(); + } + println!(","); + } + println!(" ],"); + println!("}}"); + } +} diff --git a/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/invariant_checks/mod.rs b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/invariant_checks/mod.rs new file mode 100644 index 0000000000000..80205c0aa7bb1 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/invariant_checks/mod.rs @@ -0,0 +1,23 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + execution_mode::ExecutionMode, + static_programmable_transactions::{env, typing::ast as T}, +}; +use sui_types::error::ExecutionError; + +pub mod defining_ids_in_types; +pub mod memory_safety; +pub mod type_check; + +pub fn transaction( + env: &env::Env, + tt: &T::Transaction, +) -> Result<(), ExecutionError> { + defining_ids_in_types::verify(env, tt)?; + type_check::verify::(env, tt)?; + memory_safety::verify(env, tt)?; + // Add in other invariants checks here as needed/desired. + Ok(()) +} diff --git a/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/invariant_checks/type_check.rs b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/invariant_checks/type_check.rs new file mode 100644 index 0000000000000..2bde491e78ed6 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/invariant_checks/type_check.rs @@ -0,0 +1,402 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use std::rc::Rc; + +use crate::{ + execution_mode::ExecutionMode, + sp, + static_programmable_transactions::{ + env::Env, + loading::ast as L, + typing::{ast as T, verify::input_arguments}, + }, +}; +use sui_types::{ + coin::RESOLVED_COIN_STRUCT, error::ExecutionError, + funds_accumulator::RESOLVED_WITHDRAWAL_STRUCT, +}; + +struct Context<'txn> { + objects: Vec<&'txn T::Type>, + withdrawals: Vec<&'txn T::Type>, + pure: Vec<&'txn T::Type>, + receiving: Vec<&'txn T::Type>, + result_types: Vec<&'txn [T::Type]>, +} + +impl<'txn> Context<'txn> { + fn new(txn: &'txn T::Transaction) -> Self { + Self { + objects: txn.objects.iter().map(|o| &o.ty).collect(), + withdrawals: txn.withdrawals.iter().map(|w| &w.ty).collect(), + pure: txn.pure.iter().map(|p| &p.ty).collect(), + receiving: txn.receiving.iter().map(|r| &r.ty).collect(), + result_types: txn + .commands + .iter() + .map(|sp!(_, c)| c.result_type.as_slice()) + .collect(), + } + } +} + +/// Verifies the correctness of the typing on the AST +/// - All object inputs have key +/// - All pure inputs are valid types +/// - All receiving inputs types have key +/// - All commands are well formed with correct argument/result types +/// - All dropped result values have the `drop` ability +pub fn verify(env: &Env, txn: &T::Transaction) -> Result<(), ExecutionError> { + verify_::(env, txn).map_err(|e| make_invariant_violation!("{}. Transaction {:?}", e, txn)) +} + +fn verify_(env: &Env, txn: &T::Transaction) -> anyhow::Result<()> { + let context = Context::new(txn); + let T::Transaction { + bytes: _, + objects, + withdrawals, + pure, + receiving, + commands, + } = txn; + for obj in objects { + object_input(obj)?; + } + for w in withdrawals { + withdrawal_input(&w.ty)?; + } + for p in pure { + pure_input::(p)?; + } + for r in receiving { + receiving_input(r)?; + } + for c in commands { + command::(env, &context, c)?; + } + Ok(()) +} + +fn object_input(obj: &T::ObjectInput) -> anyhow::Result<()> { + anyhow::ensure!(obj.ty.abilities().has_key(), "object type must have key"); + Ok(()) +} + +fn withdrawal_input(ty: &T::Type) -> anyhow::Result<()> { + anyhow::ensure!(ty.abilities().has_drop(), "withdrawal type must have drop"); + let T::Type::Datatype(dt) = ty else { + anyhow::bail!("withdrawal input must be a datatype, got {ty:?}"); + }; + anyhow::ensure!( + dt.type_arguments.len() == 1, + "withdrawal input must have exactly one type argument, got {}", + dt.type_arguments.len() + ); + anyhow::ensure!( + dt.qualified_ident() == RESOLVED_WITHDRAWAL_STRUCT, + "withdrawal input must be sui::funds_accumulator::Withdrawal, got {:?}", + dt.qualified_ident() + ); + Ok(()) +} + +fn pure_input(p: &T::PureInput) -> anyhow::Result<()> { + if !Mode::allow_arbitrary_values() { + anyhow::ensure!( + input_arguments::is_valid_pure_type(&p.ty)?, + "pure type must be valid" + ); + } + Ok(()) +} + +fn receiving_input(r: &T::ReceivingInput) -> anyhow::Result<()> { + anyhow::ensure!( + input_arguments::is_valid_receiving(&r.ty), + "receiving type must be valid" + ); + Ok(()) +} + +fn command( + env: &Env, + context: &Context, + sp!(_, c): &T::Command, +) -> anyhow::Result<()> { + let result_tys = &c.result_type; + match &c.command { + T::Command__::MoveCall(move_call) => { + let T::MoveCall { + function, + arguments, + } = &**move_call; + let L::LoadedFunction { signature, .. } = function; + let L::LoadedFunctionInstantiation { + parameters, + return_, + } = signature; + anyhow::ensure!( + arguments.len() == parameters.len(), + "arity mismatch. Expected {}, got {}", + parameters.len(), + arguments.len() + ); + for (arg, param) in arguments.iter().zip(parameters) { + argument(env, context, arg, param)?; + } + anyhow::ensure!( + return_.len() == result_tys.len(), + "result arity mismatch. Expected {}, got {}", + return_.len(), + result_tys.len() + ); + for (actual, expected) in return_.iter().zip(result_tys) { + anyhow::ensure!( + actual == expected, + "return type mismatch. Expected {expected:?}, got {actual:?}" + ); + } + } + T::Command__::TransferObjects(objs, recipient) => { + for obj in objs { + let ty = &obj.value.1; + anyhow::ensure!( + ty.abilities().has_key(), + "transfer object type must have key, got {ty:?}" + ); + argument(env, context, obj, ty)?; + } + argument(env, context, recipient, &T::Type::Address)?; + anyhow::ensure!( + result_tys.is_empty(), + "transfer objects should not return any value, got {result_tys:?}" + ); + } + T::Command__::SplitCoins(ty_coin, coin, amounts) => { + let T::Type::Datatype(dt) = ty_coin else { + anyhow::bail!("split coins should have a coin type, got {ty_coin:?}"); + }; + let resolved = dt.qualified_ident(); + anyhow::ensure!( + resolved == RESOLVED_COIN_STRUCT, + "split coins should have a coin type, got {resolved:?}" + ); + argument( + env, + context, + coin, + &T::Type::Reference(true, Rc::new(ty_coin.clone())), + )?; + for amount in amounts { + argument(env, context, amount, &T::Type::U64)?; + } + anyhow::ensure!( + amounts.len() == result_tys.len(), + "split coins should return as many values as amounts, expected {} got {}", + amounts.len(), + result_tys.len() + ); + anyhow::ensure!( + result_tys.iter().all(|t| t == ty_coin), + "split coins should return coin<{ty_coin:?}>, got {result_tys:?}" + ); + } + T::Command__::MergeCoins(ty_coin, target, coins) => { + let T::Type::Datatype(dt) = ty_coin else { + anyhow::bail!("split coins should have a coin type, got {ty_coin:?}"); + }; + let resolved = dt.qualified_ident(); + anyhow::ensure!( + resolved == RESOLVED_COIN_STRUCT, + "split coins should have a coin type, got {resolved:?}" + ); + argument( + env, + context, + target, + &T::Type::Reference(true, Rc::new(ty_coin.clone())), + )?; + for coin in coins { + argument(env, context, coin, ty_coin)?; + } + anyhow::ensure!( + result_tys.is_empty(), + "merge coins should not return any value, got {result_tys:?}" + ); + } + T::Command__::MakeMoveVec(t, args) => { + for arg in args { + argument(env, context, arg, t)?; + } + anyhow::ensure!( + result_tys.len() == 1, + "make move vec should return exactly one vector" + ); + let T::Type::Vector(inner) = &result_tys[0] else { + anyhow::bail!("make move vec should return a vector type, got {result_tys:?}"); + }; + anyhow::ensure!( + t == &inner.element_type, + "make move vec should return vector<{t:?}>, got {result_tys:?}" + ); + } + T::Command__::Publish(_, _, _) => { + if Mode::packages_are_predefined() { + anyhow::ensure!( + result_tys.is_empty(), + "publish should not return upgrade cap for predefined packages" + ); + } else { + anyhow::ensure!( + result_tys.len() == 1, + "publish should return exactly one upgrade cap" + ); + let cap = &env.upgrade_cap_type()?; + anyhow::ensure!( + cap == &result_tys[0], + "publish should return {cap:?}, got {result_tys:?}", + ); + } + } + T::Command__::Upgrade(_, _, _, arg, _) => { + argument(env, context, arg, &env.upgrade_ticket_type()?)?; + let receipt = &env.upgrade_receipt_type()?; + anyhow::ensure!( + result_tys.len() == 1, + "upgrade should return exactly one receipt" + ); + anyhow::ensure!( + receipt == &result_tys[0], + "upgrade should return {receipt:?}, got {result_tys:?}" + ); + } + } + assert_invariant!( + c.drop_values.len() == result_tys.len(), + "drop values should match result types, expected {} got {}", + c.drop_values.len(), + result_tys.len() + ); + for (drop_value, result_ty) in c.drop_values.iter().copied().zip(result_tys) { + // drop value ==> `ty: drop` + assert_invariant!( + !drop_value || result_ty.abilities().has_drop(), + "result was marked for drop but does not have the `drop` ability" + ); + } + Ok(()) +} + +fn argument( + env: &Env, + context: &Context, + sp!(_, (arg__, ty)): &T::Argument, + param: &T::Type, +) -> anyhow::Result<()> { + anyhow::ensure!( + ty == param, + "argument type mismatch. Expected {param:?}, got {ty:?}" + ); + let (actual, expected) = match arg__ { + T::Argument__::Use(u) => (usage(env, context, u)?, param), + T::Argument__::Read(u) => { + let actual = match usage(env, context, u)? { + T::Type::Reference(_, inner) => (*inner).clone(), + _ => { + anyhow::bail!("should never ReadRef a non-reference type, got {ty:?}"); + } + }; + (actual, param) + } + T::Argument__::Freeze(u) => { + let actual = match usage(env, context, u)? { + T::Type::Reference(true, inner) => T::Type::Reference(false, inner.clone()), + T::Type::Reference(false, _) => { + anyhow::bail!("should never FreezeRef an immutable reference") + } + ty => { + anyhow::bail!("should never Freeze a non-reference type, got {ty:?}"); + } + }; + (actual, param) + } + T::Argument__::Borrow(is_mut, l) => { + let T::Type::Reference(param_mut, expected) = param else { + anyhow::bail!("expected a reference type for borrowed location, got {param:?}"); + }; + anyhow::ensure!( + *param_mut == *is_mut, + "borrowed location mutability mismatch. Expected {param_mut}, got {is_mut}" + ); + let actual = location(env, context, *l)?; + (actual, &**expected) + } + }; + // check actual == expected + anyhow::ensure!( + &actual == expected, + "argument type mismatch. Expected {expected:?}, got {actual:?}" + ); + // check copy usage + match arg__ { + T::Argument__::Use(T::Usage::Copy { .. }) | T::Argument__::Read(_) => { + anyhow::ensure!( + param.abilities().has_copy(), + "expected type does not have copy, {expected:?}" + ); + } + T::Argument__::Use(T::Usage::Move(_)) + | T::Argument__::Freeze(_) + | T::Argument__::Borrow(_, _) => (), + } + Ok(()) +} + +fn usage(env: &Env, context: &Context, u: &T::Usage) -> anyhow::Result { + match u { + T::Usage::Move(l) + | T::Usage::Copy { + location: l, + borrowed: _, + } => location(env, context, *l), + } +} + +fn location(env: &Env, context: &Context, l: T::Location) -> anyhow::Result { + Ok(match l { + T::Location::TxContext => env.tx_context_type()?, + T::Location::GasCoin => env.gas_coin_type()?, + T::Location::ObjectInput(i) => context + .objects + .get(i as usize) + .copied() + .ok_or_else(|| anyhow::anyhow!("object input {i} out of bounds"))? + .clone(), + T::Location::WithdrawalInput(i) => context + .withdrawals + .get(i as usize) + .copied() + .ok_or_else(|| anyhow::anyhow!("withdrawal input {i} out of bounds"))? + .clone(), + T::Location::PureInput(i) => context + .pure + .get(i as usize) + .copied() + .ok_or_else(|| anyhow::anyhow!("pure input {i} out of bounds"))? + .clone(), + T::Location::ReceivingInput(i) => context + .receiving + .get(i as usize) + .copied() + .ok_or_else(|| anyhow::anyhow!("receiving input {i} out of bounds"))? + .clone(), + T::Location::Result(i, j) => context + .result_types + .get(i as usize) + .and_then(|v| v.get(j as usize)) + .ok_or_else(|| anyhow::anyhow!("result ({i}, {j}) out of bounds",))? + .clone(), + }) +} diff --git a/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/mod.rs b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/mod.rs new file mode 100644 index 0000000000000..31f739d086fb3 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/mod.rs @@ -0,0 +1,29 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + execution_mode::ExecutionMode, + static_programmable_transactions::{ + env, + loading::ast as L, + metering::{self, translation_meter::TranslationMeter}, + }, +}; +use sui_types::error::ExecutionError; + +pub mod ast; +pub mod invariant_checks; +pub mod translate; +pub mod verify; + +pub fn translate_and_verify( + meter: &mut TranslationMeter<'_, '_>, + env: &env::Env, + lt: L::Transaction, +) -> Result { + let mut ast = translate::transaction::(env, lt)?; + metering::typing::meter(meter, &ast)?; + verify::transaction::(env, &mut ast)?; + invariant_checks::transaction::(env, &ast)?; + Ok(ast) +} diff --git a/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/translate.rs b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/translate.rs new file mode 100644 index 0000000000000..ba95912d40da5 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/translate.rs @@ -0,0 +1,1097 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use super::{ast as T, env::Env}; +use crate::{ + execution_mode::ExecutionMode, + programmable_transactions::context::EitherError, + static_programmable_transactions::{ + loading::ast::{self as L, Type}, + spanned::sp, + typing::ast::BytesConstraint, + }, +}; +use indexmap::{IndexMap, IndexSet}; +use std::rc::Rc; +use sui_types::{ + base_types::{ObjectRef, TxContextKind}, + coin::RESOLVED_COIN_STRUCT, + error::{ExecutionError, ExecutionErrorKind, command_argument_error}, + execution_status::CommandArgumentError, +}; + +#[derive(Debug, Clone, Copy)] +enum SplatLocation { + GasCoin, + Input(T::InputIndex), + Result(u16, u16), +} + +#[derive(Debug, Clone, Copy)] +enum InputKind { + Object, + Withdrawal, + Pure, + Receiving, +} + +struct Context { + current_command: u16, + /// What kind of input is at each original index + input_resolution: Vec, + bytes: IndexSet>, + // Mapping from original index to `bytes` + bytes_idx_remapping: IndexMap, + receiving_refs: IndexMap, + objects: IndexMap, + withdrawals: IndexMap, + pure: IndexMap<(T::InputIndex, Type), T::PureInput>, + receiving: IndexMap<(T::InputIndex, Type), T::ReceivingInput>, + results: Vec, +} + +impl Context { + fn new(linputs: L::Inputs) -> Result { + let mut context = Context { + current_command: 0, + input_resolution: vec![], + bytes: IndexSet::new(), + bytes_idx_remapping: IndexMap::new(), + receiving_refs: IndexMap::new(), + objects: IndexMap::new(), + withdrawals: IndexMap::new(), + pure: IndexMap::new(), + receiving: IndexMap::new(), + results: vec![], + }; + // clone inputs for debug assertions + #[cfg(debug_assertions)] + let cloned_inputs = linputs + .iter() + .map(|(arg, _)| arg.clone()) + .collect::>(); + // - intern the bytes + // - build maps for object, pure, and receiving inputs + for (i, (arg, ty)) in linputs.into_iter().enumerate() { + let idx = T::InputIndex(i as u16); + let kind = match (arg, ty) { + (L::InputArg::Pure(bytes), L::InputType::Bytes) => { + let (byte_index, _) = context.bytes.insert_full(bytes); + context.bytes_idx_remapping.insert(idx, byte_index); + InputKind::Pure + } + (L::InputArg::Receiving(oref), L::InputType::Bytes) => { + context.receiving_refs.insert(idx, oref); + InputKind::Receiving + } + (L::InputArg::Object(arg), L::InputType::Fixed(ty)) => { + let o = T::ObjectInput { + original_input_index: idx, + arg, + ty, + }; + context.objects.insert(idx, o); + InputKind::Object + } + (L::InputArg::FundsWithdrawal(withdrawal), L::InputType::Fixed(input_ty)) => { + let L::FundsWithdrawalArg { ty, owner, amount } = withdrawal; + debug_assert!(ty == input_ty); + let withdrawal = T::WithdrawalInput { + original_input_index: idx, + ty, + owner, + amount, + }; + context.withdrawals.insert(idx, withdrawal); + InputKind::Withdrawal + } + (arg, ty) => invariant_violation!( + "Input arg, type mismatch. Unexpected {arg:?} with type {ty:?}" + ), + }; + context.input_resolution.push(kind); + } + #[cfg(debug_assertions)] + { + // iterate to check the correctness of bytes interning + for (i, arg) in cloned_inputs.iter().enumerate() { + if let L::InputArg::Pure(bytes) = &arg { + let idx = T::InputIndex(i as u16); + let Some(byte_index) = context.bytes_idx_remapping.get(&idx) else { + invariant_violation!("Unbound pure input {}", idx.0); + }; + let Some(interned_bytes) = context.bytes.get_index(*byte_index) else { + invariant_violation!("Interned bytes not found for index {}", byte_index); + }; + if interned_bytes != bytes { + assert_invariant!( + interned_bytes == bytes, + "Interned bytes mismatch for input {i}", + ); + } + } + } + } + Ok(context) + } + + fn finish(self, commands: T::Commands) -> T::Transaction { + let Self { + bytes, + objects, + withdrawals, + pure, + receiving, + .. + } = self; + let objects = objects.into_iter().map(|(_, o)| o).collect(); + let withdrawals = withdrawals.into_iter().map(|(_, w)| w).collect(); + let pure = pure.into_iter().map(|(_, p)| p).collect(); + let receiving = receiving.into_iter().map(|(_, r)| r).collect(); + T::Transaction { + bytes, + objects, + withdrawals, + pure, + receiving, + commands, + } + } + + // Get the fixed type of a location. Returns `None` for Pure and Receiving inputs, + fn fixed_type( + &mut self, + env: &Env, + location: SplatLocation, + ) -> Result, ExecutionError> { + Ok(Some(match location { + SplatLocation::GasCoin => (T::Location::GasCoin, env.gas_coin_type()?), + SplatLocation::Result(i, j) => ( + T::Location::Result(i, j), + self.results[i as usize][j as usize].clone(), + ), + SplatLocation::Input(i) => match &self.input_resolution[i.0 as usize] { + InputKind::Object => { + let Some((object_index, _, object_input)) = self.objects.get_full(&i) else { + invariant_violation!("Unbound object input {}", i.0) + }; + ( + T::Location::ObjectInput(object_index as u16), + object_input.ty.clone(), + ) + } + InputKind::Withdrawal => { + let Some((withdrawal_index, _, withdrawal_input)) = + self.withdrawals.get_full(&i) + else { + invariant_violation!("Unbound withdrawal input {}", i.0) + }; + ( + T::Location::WithdrawalInput(withdrawal_index as u16), + withdrawal_input.ty.clone(), + ) + } + InputKind::Pure | InputKind::Receiving => return Ok(None), + }, + })) + } + + fn resolve_location( + &mut self, + env: &Env, + location: SplatLocation, + expected_ty: &Type, + bytes_constraint: BytesConstraint, + ) -> Result<(T::Location, Type), ExecutionError> { + Ok(match location { + SplatLocation::GasCoin | SplatLocation::Result(_, _) => self + .fixed_type(env, location)? + .ok_or_else(|| make_invariant_violation!("Expected fixed type for {location:?}"))?, + SplatLocation::Input(i) => match &self.input_resolution[i.0 as usize] { + InputKind::Object | InputKind::Withdrawal => { + self.fixed_type(env, location)?.ok_or_else(|| { + make_invariant_violation!("Expected fixed type for {location:?}") + })? + } + InputKind::Pure => { + let ty = match expected_ty { + Type::Reference(_, inner) => (**inner).clone(), + ty => ty.clone(), + }; + let k = (i, ty.clone()); + if !self.pure.contains_key(&k) { + let Some(byte_index) = self.bytes_idx_remapping.get(&i).copied() else { + invariant_violation!("Unbound pure input {}", i.0); + }; + let pure = T::PureInput { + original_input_index: i, + byte_index, + ty: ty.clone(), + constraint: bytes_constraint, + }; + self.pure.insert(k.clone(), pure); + } + let byte_index = self.pure.get_index_of(&k).unwrap(); + (T::Location::PureInput(byte_index as u16), ty) + } + InputKind::Receiving => { + let ty = match expected_ty { + Type::Reference(_, inner) => (**inner).clone(), + ty => ty.clone(), + }; + let k = (i, ty.clone()); + if !self.receiving.contains_key(&k) { + let Some(object_ref) = self.receiving_refs.get(&i).copied() else { + invariant_violation!("Unbound receiving input {}", i.0); + }; + let receiving = T::ReceivingInput { + original_input_index: i, + object_ref, + ty: ty.clone(), + constraint: bytes_constraint, + }; + self.receiving.insert(k.clone(), receiving); + } + let byte_index = self.receiving.get_index_of(&k).unwrap(); + (T::Location::ReceivingInput(byte_index as u16), ty) + } + }, + }) + } +} + +pub fn transaction( + env: &Env, + lt: L::Transaction, +) -> Result { + let L::Transaction { inputs, commands } = lt; + let mut context = Context::new(inputs)?; + let commands = commands + .into_iter() + .enumerate() + .map(|(i, c)| { + let idx = i as u16; + context.current_command = idx; + let (c_, tys) = + command::(env, &mut context, c).map_err(|e| e.with_command_index(i))?; + context.results.push(tys.clone()); + let c = T::Command_ { + command: c_, + result_type: tys, + // computed later + drop_values: vec![], + // computed later + consumed_shared_objects: vec![], + }; + Ok(sp(idx, c)) + }) + .collect::, ExecutionError>>()?; + let mut ast = context.finish(commands); + // mark the last usage of references as Move instead of Copy + scope_references::transaction(&mut ast); + // mark unused results to be dropped + unused_results::transaction(&mut ast); + // track shared object IDs + consumed_shared_objects::transaction(&mut ast)?; + Ok(ast) +} + +fn command( + env: &Env, + context: &mut Context, + command: L::Command, +) -> Result<(T::Command__, T::ResultType), ExecutionError> { + Ok(match command { + L::Command::MoveCall(lmc) => { + let L::MoveCall { + function, + arguments: largs, + } = *lmc; + let arg_locs = locations(context, 0, largs)?; + let tx_context_kind = tx_context_kind(&function); + let parameter_tys = match tx_context_kind { + TxContextKind::None => &function.signature.parameters, + TxContextKind::Mutable | TxContextKind::Immutable => { + let Some(n_) = function.signature.parameters.len().checked_sub(1) else { + invariant_violation!( + "A function with a TxContext should have at least one parameter" + ) + }; + &function.signature.parameters[0..n_] + } + }; + let num_args = arg_locs.len(); + let num_parameters = parameter_tys.len(); + if num_args != num_parameters { + return Err(ExecutionError::new_with_source( + ExecutionErrorKind::ArityMismatch, + format!( + "Expected {} argument{} calling function '{}::{}', but found {}", + num_parameters, + if num_parameters == 1 { "" } else { "s" }, + function.storage_id, + function.name, + num_args, + ), + )); + } + let mut args = arguments(env, context, 0, arg_locs, parameter_tys.iter().cloned())?; + match tx_context_kind { + TxContextKind::None => (), + TxContextKind::Mutable | TxContextKind::Immutable => { + let is_mut = match tx_context_kind { + TxContextKind::Mutable => true, + TxContextKind::Immutable => false, + TxContextKind::None => unreachable!(), + }; + // TODO this is out of bounds of the original PTB arguments... what do we + // do here? + let idx = args.len() as u16; + let arg__ = T::Argument__::Borrow(is_mut, T::Location::TxContext); + let ty = Type::Reference(is_mut, Rc::new(env.tx_context_type()?)); + args.push(sp(idx, (arg__, ty))); + } + } + let result = function.signature.return_.clone(); + ( + T::Command__::MoveCall(Box::new(T::MoveCall { + function, + arguments: args, + })), + result, + ) + } + L::Command::TransferObjects(lobjects, laddress) => { + let object_locs = locations(context, 0, lobjects)?; + let address_loc = one_location(context, object_locs.len(), laddress)?; + let objects = constrained_arguments( + env, + context, + 0, + object_locs, + |ty| { + let abilities = ty.abilities(); + Ok(abilities.has_store() && abilities.has_key()) + }, + CommandArgumentError::InvalidTransferObject, + )?; + let address = argument(env, context, objects.len(), address_loc, Type::Address)?; + (T::Command__::TransferObjects(objects, address), vec![]) + } + L::Command::SplitCoins(lcoin, lamounts) => { + let coin_loc = one_location(context, 0, lcoin)?; + let amount_locs = locations(context, 1, lamounts)?; + let coin = coin_mut_ref_argument(env, context, 0, coin_loc)?; + let coin_type = match &coin.value.1 { + Type::Reference(true, ty) => (**ty).clone(), + ty => invariant_violation!("coin must be a mutable reference. Found: {ty:?}"), + }; + let amounts = arguments( + env, + context, + 1, + amount_locs, + std::iter::repeat_with(|| Type::U64), + )?; + let result = vec![coin_type.clone(); amounts.len()]; + (T::Command__::SplitCoins(coin_type, coin, amounts), result) + } + L::Command::MergeCoins(ltarget, lcoins) => { + let target_loc = one_location(context, 0, ltarget)?; + let coin_locs = locations(context, 1, lcoins)?; + let target = coin_mut_ref_argument(env, context, 0, target_loc)?; + let coin_type = match &target.value.1 { + Type::Reference(true, ty) => (**ty).clone(), + ty => invariant_violation!("target must be a mutable reference. Found: {ty:?}"), + }; + let coins = arguments( + env, + context, + 1, + coin_locs, + std::iter::repeat_with(|| coin_type.clone()), + )?; + (T::Command__::MergeCoins(coin_type, target, coins), vec![]) + } + L::Command::MakeMoveVec(Some(ty), lelems) => { + let elem_locs = locations(context, 0, lelems)?; + let elems = arguments( + env, + context, + 0, + elem_locs, + std::iter::repeat_with(|| ty.clone()), + )?; + ( + T::Command__::MakeMoveVec(ty.clone(), elems), + vec![env.vector_type(ty)?], + ) + } + L::Command::MakeMoveVec(None, lelems) => { + let mut lelems = lelems.into_iter(); + let Some(lfirst) = lelems.next() else { + // TODO maybe this should be a different errors for CLI usage + invariant_violation!( + "input checker ensures if args are empty, there is a type specified" + ); + }; + let first_loc = one_location(context, 0, lfirst)?; + let first_arg = constrained_argument( + env, + context, + 0, + first_loc, + |ty| Ok(ty.abilities().has_key()), + CommandArgumentError::InvalidMakeMoveVecNonObjectArgument, + )?; + let first_ty = first_arg.value.1.clone(); + let elems_loc = locations(context, 1, lelems)?; + let mut elems = arguments( + env, + context, + 1, + elems_loc, + std::iter::repeat_with(|| first_ty.clone()), + )?; + elems.insert(0, first_arg); + ( + T::Command__::MakeMoveVec(first_ty.clone(), elems), + vec![env.vector_type(first_ty)?], + ) + } + L::Command::Publish(items, object_ids, linkage) => { + let result = if Mode::packages_are_predefined() { + // If packages are predefined, no upgrade cap is made + vec![] + } else { + vec![env.upgrade_cap_type()?.clone()] + }; + (T::Command__::Publish(items, object_ids, linkage), result) + } + L::Command::Upgrade(items, object_ids, object_id, la, linkage) => { + let location = one_location(context, 0, la)?; + let expected_ty = env.upgrade_ticket_type()?; + let a = argument(env, context, 0, location, expected_ty)?; + let res = env.upgrade_receipt_type()?; + ( + T::Command__::Upgrade(items, object_ids, object_id, a, linkage), + vec![res.clone()], + ) + } + }) +} + +fn tx_context_kind(function: &L::LoadedFunction) -> TxContextKind { + match function.signature.parameters.last() { + Some(ty) => ty.is_tx_context(), + None => TxContextKind::None, + } +} + +fn one_location( + context: &mut Context, + command_arg_idx: usize, + arg: L::Argument, +) -> Result { + let locs = locations(context, command_arg_idx, vec![arg])?; + let Ok([loc]): Result<[SplatLocation; 1], _> = locs.try_into() else { + return Err(command_argument_error( + CommandArgumentError::InvalidArgumentArity, + command_arg_idx, + )); + }; + Ok(loc) +} + +fn locations>( + context: &mut Context, + start_idx: usize, + args: Items, +) -> Result, ExecutionError> +where + Items::IntoIter: ExactSizeIterator, +{ + fn splat_arg( + context: &mut Context, + res: &mut Vec, + arg: L::Argument, + ) -> Result<(), EitherError> { + match arg { + L::Argument::GasCoin => res.push(SplatLocation::GasCoin), + L::Argument::Input(i) => { + if i as usize >= context.input_resolution.len() { + return Err(CommandArgumentError::IndexOutOfBounds { idx: i }.into()); + } + res.push(SplatLocation::Input(T::InputIndex(i))) + } + L::Argument::NestedResult(i, j) => { + let Some(command_result) = context.results.get(i as usize) else { + return Err(CommandArgumentError::IndexOutOfBounds { idx: i }.into()); + }; + if j as usize >= command_result.len() { + return Err(CommandArgumentError::SecondaryIndexOutOfBounds { + result_idx: i, + secondary_idx: j, + } + .into()); + }; + res.push(SplatLocation::Result(i, j)) + } + L::Argument::Result(i) => { + let Some(result) = context.results.get(i as usize) else { + return Err(CommandArgumentError::IndexOutOfBounds { idx: i }.into()); + }; + let Ok(len): Result = result.len().try_into() else { + invariant_violation!("Result of length greater than u16::MAX"); + }; + if len != 1 { + // TODO protocol config to allow splatting of args + return Err(CommandArgumentError::InvalidResultArity { result_idx: i }.into()); + } + res.extend((0..len).map(|j| SplatLocation::Result(i, j))) + } + } + Ok(()) + } + + let args = args.into_iter(); + let _args_len = args.len(); + let mut res = vec![]; + for (arg_idx, arg) in args.enumerate() { + splat_arg(context, &mut res, arg).map_err(|e| { + let Some(idx) = start_idx.checked_add(arg_idx) else { + return make_invariant_violation!("usize overflow when calculating argument index"); + }; + e.into_execution_error(idx) + })? + } + debug_assert_eq!(res.len(), _args_len); + Ok(res) +} + +fn arguments( + env: &Env, + context: &mut Context, + start_idx: usize, + locations: Vec, + expected_tys: impl IntoIterator, +) -> Result, ExecutionError> { + locations + .into_iter() + .zip(expected_tys) + .enumerate() + .map(|(i, (location, expected_ty))| { + let Some(idx) = start_idx.checked_add(i) else { + invariant_violation!("usize overflow when calculating argument index"); + }; + argument(env, context, idx, location, expected_ty) + }) + .collect() +} + +fn argument( + env: &Env, + context: &mut Context, + command_arg_idx: usize, + location: SplatLocation, + expected_ty: Type, +) -> Result { + let arg__ = argument_(env, context, command_arg_idx, location, &expected_ty) + .map_err(|e| e.into_execution_error(command_arg_idx))?; + let arg_ = (arg__, expected_ty); + Ok(sp(command_arg_idx as u16, arg_)) +} + +fn argument_( + env: &Env, + context: &mut Context, + command_arg_idx: usize, + location: SplatLocation, + expected_ty: &Type, +) -> Result { + let current_command = context.current_command; + let bytes_constraint = BytesConstraint { + command: current_command, + argument: command_arg_idx as u16, + }; + let (location, actual_ty): (T::Location, Type) = + context.resolve_location(env, location, expected_ty, bytes_constraint)?; + Ok(match (actual_ty, expected_ty) { + // Reference location types + (Type::Reference(a_is_mut, a), Type::Reference(b_is_mut, b)) => { + let needs_freeze = match (a_is_mut, b_is_mut) { + // same mutability + (true, true) | (false, false) => false, + // mut *can* be used as imm + (true, false) => true, + // imm cannot be used as mut + (false, true) => return Err(CommandArgumentError::TypeMismatch.into()), + }; + debug_assert!(expected_ty.abilities().has_copy()); + // unused since the type is fixed + check_type(&a, b)?; + if needs_freeze { + T::Argument__::Freeze(T::Usage::new_copy(location)) + } else { + T::Argument__::new_copy(location) + } + } + (Type::Reference(_, a), b) => { + check_type(&a, b)?; + if !b.abilities().has_copy() { + // TODO this should be a different error for missing copy + return Err(CommandArgumentError::TypeMismatch.into()); + } + T::Argument__::Read(T::Usage::new_copy(location)) + } + + // Non reference location types + (actual_ty, Type::Reference(is_mut, inner)) => { + check_type(&actual_ty, inner)?; + T::Argument__::Borrow(/* mut */ *is_mut, location) + } + (actual_ty, _) => { + check_type(&actual_ty, expected_ty)?; + T::Argument__::Use(if expected_ty.abilities().has_copy() { + T::Usage::new_copy(location) + } else { + T::Usage::new_move(location) + }) + } + }) +} + +fn check_type(actual_ty: &Type, expected_ty: &Type) -> Result<(), CommandArgumentError> { + if actual_ty == expected_ty { + Ok(()) + } else { + Err(CommandArgumentError::TypeMismatch) + } +} + +fn constrained_arguments Result>( + env: &Env, + context: &mut Context, + start_idx: usize, + locations: Vec, + mut is_valid: P, + err_case: CommandArgumentError, +) -> Result, ExecutionError> { + let is_valid = &mut is_valid; + locations + .into_iter() + .enumerate() + .map(|(i, location)| { + let Some(idx) = start_idx.checked_add(i) else { + invariant_violation!("usize overflow when calculating argument index"); + }; + constrained_argument_(env, context, idx, location, is_valid, err_case) + }) + .collect() +} + +fn constrained_argument Result>( + env: &Env, + context: &mut Context, + command_arg_idx: usize, + location: SplatLocation, + mut is_valid: P, + err_case: CommandArgumentError, +) -> Result { + constrained_argument_( + env, + context, + command_arg_idx, + location, + &mut is_valid, + err_case, + ) +} + +fn constrained_argument_ Result>( + env: &Env, + context: &mut Context, + command_arg_idx: usize, + location: SplatLocation, + is_valid: &mut P, + err_case: CommandArgumentError, +) -> Result { + let arg_ = constrained_argument__(env, context, location, is_valid, err_case) + .map_err(|e| e.into_execution_error(command_arg_idx))?; + Ok(sp(command_arg_idx as u16, arg_)) +} + +fn constrained_argument__ Result>( + env: &Env, + context: &mut Context, + location: SplatLocation, + is_valid: &mut P, + err_case: CommandArgumentError, +) -> Result { + if let Some((location, ty)) = constrained_type(env, context, location, is_valid)? { + if ty.abilities().has_copy() { + Ok((T::Argument__::new_copy(location), ty)) + } else { + Ok((T::Argument__::new_move(location), ty)) + } + } else { + Err(err_case.into()) + } +} + +fn constrained_type<'a, P: FnMut(&Type) -> Result>( + env: &'a Env, + context: &'a mut Context, + location: SplatLocation, + mut is_valid: P, +) -> Result, ExecutionError> { + let Some((location, ty)) = context.fixed_type(env, location)? else { + return Ok(None); + }; + Ok(if is_valid(&ty)? { + Some((location, ty)) + } else { + None + }) +} + +fn coin_mut_ref_argument( + env: &Env, + context: &mut Context, + command_arg_idx: usize, + location: SplatLocation, +) -> Result { + let arg_ = coin_mut_ref_argument_(env, context, location) + .map_err(|e| e.into_execution_error(command_arg_idx))?; + Ok(sp(command_arg_idx as u16, arg_)) +} + +fn coin_mut_ref_argument_( + env: &Env, + context: &mut Context, + location: SplatLocation, +) -> Result { + let Some((location, actual_ty)) = context.fixed_type(env, location)? else { + // TODO we do not currently bytes in any mode as that would require additional type + // inference not currently supported + return Err(CommandArgumentError::TypeMismatch.into()); + }; + Ok(match &actual_ty { + Type::Reference(is_mut, ty) if *is_mut => { + check_coin_type(ty)?; + ( + T::Argument__::new_copy(location), + Type::Reference(*is_mut, ty.clone()), + ) + } + ty => { + check_coin_type(ty)?; + ( + T::Argument__::Borrow(/* mut */ true, location), + Type::Reference(true, Rc::new(ty.clone())), + ) + } + }) +} + +fn check_coin_type(ty: &Type) -> Result<(), EitherError> { + let Type::Datatype(dt) = ty else { + return Err(CommandArgumentError::TypeMismatch.into()); + }; + let resolved = dt.qualified_ident(); + let is_coin = resolved == RESOLVED_COIN_STRUCT; + if is_coin { + Ok(()) + } else { + Err(CommandArgumentError::TypeMismatch.into()) + } +} + +//************************************************************************************************** +// Reference scoping +//************************************************************************************************** + +mod scope_references { + use crate::{ + sp, + static_programmable_transactions::typing::ast::{self as T, Type}, + }; + use std::collections::BTreeSet; + + /// To mimic proper scoping of references, the last usage of a reference is made a Move instead + /// of a Copy. + pub fn transaction(ast: &mut T::Transaction) { + let mut used: BTreeSet<(u16, u16)> = BTreeSet::new(); + for c in ast.commands.iter_mut().rev() { + command(&mut used, c); + } + } + + fn command(used: &mut BTreeSet<(u16, u16)>, sp!(_, c): &mut T::Command) { + match &mut c.command { + T::Command__::MoveCall(mc) => arguments(used, &mut mc.arguments), + T::Command__::TransferObjects(objects, recipient) => { + argument(used, recipient); + arguments(used, objects); + } + T::Command__::SplitCoins(_, coin, amounts) => { + arguments(used, amounts); + argument(used, coin); + } + T::Command__::MergeCoins(_, target, coins) => { + arguments(used, coins); + argument(used, target); + } + T::Command__::MakeMoveVec(_, xs) => arguments(used, xs), + T::Command__::Publish(_, _, _) => (), + T::Command__::Upgrade(_, _, _, x, _) => argument(used, x), + } + } + + fn arguments(used: &mut BTreeSet<(u16, u16)>, args: &mut [T::Argument]) { + for arg in args.iter_mut().rev() { + argument(used, arg) + } + } + + fn argument(used: &mut BTreeSet<(u16, u16)>, sp!(_, (arg_, ty)): &mut T::Argument) { + let usage = match arg_ { + T::Argument__::Use(u) | T::Argument__::Read(u) | T::Argument__::Freeze(u) => u, + T::Argument__::Borrow(_, _) => return, + }; + match (&usage, ty) { + (T::Usage::Move(T::Location::Result(i, j)), Type::Reference(_, _)) => { + debug_assert!(false, "No reference should be moved at this point"); + used.insert((*i, *j)); + } + ( + T::Usage::Copy { + location: T::Location::Result(i, j), + .. + }, + Type::Reference(_, _), + ) => { + // we are at the last usage of a reference result if it was not yet added to the set + let last_usage = used.insert((*i, *j)); + if last_usage { + // if it was the last usage, we need to change the Copy to a Move + let loc = T::Location::Result(*i, *j); + *usage = T::Usage::Move(loc); + } + } + _ => (), + } + } +} + +//************************************************************************************************** +// Unused results +//************************************************************************************************** + +mod unused_results { + use indexmap::IndexSet; + + use crate::{sp, static_programmable_transactions::typing::ast as T}; + + /// Finds what `Result` indexes are never used in the transaction. + /// For each command, marks the indexes of result values with `drop` that are never referred to + /// via `Result`. + pub fn transaction(ast: &mut T::Transaction) { + // Collect all used result locations (i, j) across all commands + let mut used: IndexSet<(u16, u16)> = IndexSet::new(); + for c in &ast.commands { + command(&mut used, c); + } + + // For each command, mark unused result indexes with `drop` + for (i, sp!(_, c)) in ast.commands.iter_mut().enumerate() { + debug_assert!(c.drop_values.is_empty()); + let i = i as u16; + c.drop_values = c + .result_type + .iter() + .enumerate() + .map(|(j, ty)| (j as u16, ty)) + .map(|(j, ty)| ty.abilities().has_drop() && !used.contains(&(i, j))) + .collect(); + } + } + + fn command(used: &mut IndexSet<(u16, u16)>, sp!(_, c): &T::Command) { + match &c.command { + T::Command__::MoveCall(mc) => arguments(used, &mc.arguments), + T::Command__::TransferObjects(objects, recipient) => { + argument(used, recipient); + arguments(used, objects); + } + T::Command__::SplitCoins(_, coin, amounts) => { + arguments(used, amounts); + argument(used, coin); + } + T::Command__::MergeCoins(_, target, coins) => { + arguments(used, coins); + argument(used, target); + } + T::Command__::MakeMoveVec(_, elements) => arguments(used, elements), + T::Command__::Publish(_, _, _) => (), + T::Command__::Upgrade(_, _, _, x, _) => argument(used, x), + } + } + + fn arguments(used: &mut IndexSet<(u16, u16)>, args: &[T::Argument]) { + for arg in args { + argument(used, arg) + } + } + + fn argument(used: &mut IndexSet<(u16, u16)>, sp!(_, (arg_, _)): &T::Argument) { + if let T::Location::Result(i, j) = arg_.location() { + used.insert((i, j)); + } + } +} + +//************************************************************************************************** +// consumed shared object IDs +//************************************************************************************************** + +mod consumed_shared_objects { + + use crate::{ + sp, static_programmable_transactions::loading::ast as L, + static_programmable_transactions::typing::ast as T, + }; + use sui_types::{base_types::ObjectID, error::ExecutionError}; + + // Shared object (non-party) IDs contained in each location + struct Context { + // (legacy) shared object IDs that are used as inputs + inputs: Vec>, + results: Vec>>>, + } + + impl Context { + pub fn new(ast: &T::Transaction) -> Self { + let T::Transaction { + bytes: _, + objects, + withdrawals: _, + pure: _, + receiving: _, + commands: _, + } = ast; + let inputs = objects + .iter() + .map(|o| match &o.arg { + L::ObjectArg::SharedObject { + id, + kind: L::SharedObjectKind::Legacy, + .. + } => Some(*id), + L::ObjectArg::ImmObject(_) + | L::ObjectArg::OwnedObject(_) + | L::ObjectArg::SharedObject { + kind: L::SharedObjectKind::Party, + .. + } => None, + }) + .collect::>(); + Self { + inputs, + results: vec![], + } + } + } + + /// Finds what shared objects are taken by-value by each command and must be either + /// deleted or re-shared. + /// MakeMoveVec is the only command that can take shared objects by-value and propagate them + /// for another command. + pub fn transaction(ast: &mut T::Transaction) -> Result<(), ExecutionError> { + let mut context = Context::new(ast); + + // For each command, find what shared objects are taken by-value and mark them as being + // consumed + for c in &mut ast.commands { + debug_assert!(c.value.consumed_shared_objects.is_empty()); + command(&mut context, c)?; + debug_assert!(context.results.last().unwrap().len() == c.value.result_type.len()); + } + Ok(()) + } + + fn command(context: &mut Context, sp!(_, c): &mut T::Command) -> Result<(), ExecutionError> { + let mut acc = vec![]; + match &c.command { + T::Command__::MoveCall(mc) => arguments(context, &mut acc, &mc.arguments), + T::Command__::TransferObjects(objects, recipient) => { + argument(context, &mut acc, recipient); + arguments(context, &mut acc, objects); + } + T::Command__::SplitCoins(_, coin, amounts) => { + arguments(context, &mut acc, amounts); + argument(context, &mut acc, coin); + } + T::Command__::MergeCoins(_, target, coins) => { + arguments(context, &mut acc, coins); + argument(context, &mut acc, target); + } + T::Command__::MakeMoveVec(_, elements) => arguments(context, &mut acc, elements), + T::Command__::Publish(_, _, _) => (), + T::Command__::Upgrade(_, _, _, x, _) => argument(context, &mut acc, x), + } + let (consumed, result) = match &c.command { + // make move vec does not "consume" any by-value shared objects, and can propagate + // them to a later command + T::Command__::MakeMoveVec(_, _) => { + assert_invariant!( + c.result_type.len() == 1, + "MakeMoveVec must return a single value" + ); + (vec![], vec![Some(acc)]) + } + // these commands do not propagate shared objects, and consume any in the acc + T::Command__::MoveCall(_) + | T::Command__::TransferObjects(_, _) + | T::Command__::SplitCoins(_, _, _) + | T::Command__::MergeCoins(_, _, _) + | T::Command__::Publish(_, _, _) + | T::Command__::Upgrade(_, _, _, _, _) => (acc, vec![None; c.result_type.len()]), + }; + c.consumed_shared_objects = consumed; + context.results.push(result); + Ok(()) + } + + fn arguments(context: &mut Context, acc: &mut Vec, args: &[T::Argument]) { + for arg in args { + argument(context, acc, arg) + } + } + + fn argument(context: &mut Context, acc: &mut Vec, sp!(_, (arg_, _)): &T::Argument) { + let T::Argument__::Use(T::Usage::Move(loc)) = arg_ else { + // only Move usage can take shared objects by-value since they cannot be copied + return; + }; + match loc { + // no shared objects in these locations + T::Location::TxContext + | T::Location::GasCoin + | T::Location::WithdrawalInput(_) + | T::Location::PureInput(_) + | T::Location::ReceivingInput(_) => (), + T::Location::ObjectInput(i) => { + if let Some(id) = context.inputs[*i as usize] { + acc.push(id); + } + } + + T::Location::Result(i, j) => { + if let Some(ids) = &context.results[*i as usize][*j as usize] { + acc.extend(ids.iter().copied()); + } + } + } + } +} diff --git a/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/verify/drop_safety.rs b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/verify/drop_safety.rs new file mode 100644 index 0000000000000..2872c005cbf78 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/verify/drop_safety.rs @@ -0,0 +1,365 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + execution_mode::ExecutionMode, + static_programmable_transactions::{env::Env, typing::ast as T}, +}; +use sui_types::error::ExecutionError; + +/// Refines usage of values so that the last `Copy` of a value is a `Move` if it is not borrowed +/// After, it verifies the following +/// - No results without `drop` are unused (all unused non-input values have `drop`) +pub fn refine_and_verify( + env: &Env, + ast: &mut T::Transaction, +) -> Result<(), ExecutionError> { + refine::transaction(ast); + verify::transaction::(env, ast)?; + Ok(()) +} + +mod refine { + use crate::{ + sp, + static_programmable_transactions::typing::ast::{self as T}, + }; + use std::collections::BTreeSet; + + /// After memory safety, we can switch the last usage of a `Copy` to a `Move` if it is not + /// borrowed at the time of the last usage. + pub fn transaction(ast: &mut T::Transaction) { + let mut used: BTreeSet = BTreeSet::new(); + for c in ast.commands.iter_mut().rev() { + command(&mut used, c); + } + } + + fn command(used: &mut BTreeSet, sp!(_, c): &mut T::Command) { + match &mut c.command { + T::Command__::MoveCall(mc) => arguments(used, &mut mc.arguments), + T::Command__::TransferObjects(objects, recipient) => { + argument(used, recipient); + arguments(used, objects); + } + T::Command__::SplitCoins(_, coin, amounts) => { + arguments(used, amounts); + argument(used, coin); + } + T::Command__::MergeCoins(_, target, coins) => { + arguments(used, coins); + argument(used, target); + } + T::Command__::MakeMoveVec(_, xs) => arguments(used, xs), + T::Command__::Publish(_, _, _) => (), + T::Command__::Upgrade(_, _, _, x, _) => argument(used, x), + } + } + + fn arguments(used: &mut BTreeSet, args: &mut [T::Argument]) { + for arg in args.iter_mut().rev() { + argument(used, arg) + } + } + + fn argument(used: &mut BTreeSet, arg: &mut T::Argument) { + let usage = match &mut arg.value.0 { + T::Argument__::Use(u) | T::Argument__::Read(u) | T::Argument__::Freeze(u) => u, + T::Argument__::Borrow(_, loc) => { + // mark location as used + used.insert(*loc); + return; + } + }; + match &usage { + T::Usage::Move(loc) => { + // mark location as used + used.insert(*loc); + } + T::Usage::Copy { location, borrowed } => { + // we are at the last usage of a reference result if it was not yet added to the set + let location = *location; + let last_usage = used.insert(location); + if last_usage && !borrowed.get().unwrap() { + // if it was the last usage, we need to change the Copy to a Move + *usage = T::Usage::Move(location); + } + } + } + } +} + +mod verify { + use crate::{ + execution_mode::ExecutionMode, + sp, + static_programmable_transactions::{ + env::Env, + typing::ast::{self as T, Type}, + }, + }; + use sui_types::error::{ExecutionError, ExecutionErrorKind}; + + #[must_use] + struct Value; + + struct Context { + tx_context: Option, + gas_coin: Option, + objects: Vec>, + withdrawals: Vec>, + pure: Vec>, + receiving: Vec>, + results: Vec>>, + } + + impl Context { + fn new(ast: &T::Transaction) -> Result { + let objects = ast.objects.iter().map(|_| Some(Value)).collect::>(); + let withdrawals = ast + .withdrawals + .iter() + .map(|_| Some(Value)) + .collect::>(); + let pure = ast.pure.iter().map(|_| Some(Value)).collect::>(); + let receiving = ast + .receiving + .iter() + .map(|_| Some(Value)) + .collect::>(); + Ok(Self { + tx_context: Some(Value), + gas_coin: Some(Value), + objects, + withdrawals, + pure, + receiving, + results: Vec::with_capacity(ast.commands.len()), + }) + } + + fn location(&mut self, l: T::Location) -> &mut Option { + match l { + T::Location::TxContext => &mut self.tx_context, + T::Location::GasCoin => &mut self.gas_coin, + T::Location::ObjectInput(i) => &mut self.objects[i as usize], + T::Location::WithdrawalInput(i) => &mut self.withdrawals[i as usize], + T::Location::PureInput(i) => &mut self.pure[i as usize], + T::Location::ReceivingInput(i) => &mut self.receiving[i as usize], + T::Location::Result(i, j) => &mut self.results[i as usize][j as usize], + } + } + } + + /// Checks the following + /// - All unused result values have `drop` + pub fn transaction( + _env: &Env, + ast: &T::Transaction, + ) -> Result<(), ExecutionError> { + let mut context = Context::new(ast)?; + let commands = &ast.commands; + for c in commands { + let result = + command(&mut context, c).map_err(|e| e.with_command_index(c.idx as usize))?; + assert_invariant!( + result.len() == c.value.result_type.len(), + "result length mismatch" + ); + // drop unused result values + assert_invariant!( + result.len() == c.value.drop_values.len(), + "drop values length mismatch" + ); + let result_values = result + .into_iter() + .zip(c.value.drop_values.iter().copied()) + .map(|(v, drop)| { + if !drop { + Some(v) + } else { + consume_value(v); + None + } + }) + .collect(); + context.results.push(result_values); + } + + let Context { + tx_context, + gas_coin, + objects, + withdrawals, + pure, + receiving, + results, + } = context; + consume_value_opt(gas_coin); + // TODO do we want to check inputs in the dev inspect case? + consume_value_opts(objects); + consume_value_opts(withdrawals); + consume_value_opts(pure); + consume_value_opts(receiving); + assert_invariant!(results.len() == commands.len(), "result length mismatch"); + for (i, (result, c)) in results.into_iter().zip(&ast.commands).enumerate() { + let tys = &c.value.result_type; + assert_invariant!(result.len() == tys.len(), "result length mismatch"); + for (j, (vopt, ty)) in result.into_iter().zip(tys).enumerate() { + drop_value_opt::((i, j), vopt, ty)?; + } + } + assert_invariant!(tx_context.is_some(), "tx_context should never be moved"); + Ok(()) + } + + fn command( + context: &mut Context, + sp!(_, c): &T::Command, + ) -> Result, ExecutionError> { + let result_tys = &c.result_type; + Ok(match &c.command { + T::Command__::MoveCall(mc) => { + let T::MoveCall { + function, + arguments: args, + } = &**mc; + let return_ = &function.signature.return_; + let arg_values = arguments(context, args)?; + consume_values(arg_values); + (0..return_.len()).map(|_| Value).collect() + } + T::Command__::TransferObjects(objects, recipient) => { + let object_values = arguments(context, objects)?; + let recipient_value = argument(context, recipient)?; + consume_values(object_values); + consume_value(recipient_value); + vec![] + } + T::Command__::SplitCoins(_, coin, amounts) => { + let coin_value = argument(context, coin)?; + let amount_values = arguments(context, amounts)?; + consume_values(amount_values); + consume_value(coin_value); + (0..amounts.len()).map(|_| Value).collect() + } + T::Command__::MergeCoins(_, target, coins) => { + let target_value = argument(context, target)?; + let coin_values = arguments(context, coins)?; + consume_values(coin_values); + consume_value(target_value); + vec![] + } + T::Command__::MakeMoveVec(_, xs) => { + let vs = arguments(context, xs)?; + consume_values(vs); + vec![Value] + } + T::Command__::Publish(_, _, _) => result_tys.iter().map(|_| Value).collect(), + T::Command__::Upgrade(_, _, _, x, _) => { + let v = argument(context, x)?; + consume_value(v); + vec![Value] + } + }) + } + + fn consume_values(_: Vec) {} + + fn consume_value(_: Value) {} + + fn consume_value_opts(_: Vec>) {} + + fn consume_value_opt(_: Option) {} + + fn drop_value_opt( + idx: (usize, usize), + value: Option, + ty: &Type, + ) -> Result<(), ExecutionError> { + match value { + Some(v) => drop_value::(idx, v, ty), + None => Ok(()), + } + } + + fn drop_value( + (i, j): (usize, usize), + value: Value, + ty: &Type, + ) -> Result<(), ExecutionError> { + let abilities = ty.abilities(); + if !abilities.has_drop() && !Mode::allow_arbitrary_values() { + let msg = if abilities.has_copy() { + "The value has copy, but not drop. \ + Its last usage must be by-value so it can be taken." + } else { + "Unused value without drop" + }; + return Err(ExecutionError::new_with_source( + ExecutionErrorKind::UnusedValueWithoutDrop { + result_idx: i as u16, + secondary_idx: j as u16, + }, + msg, + )); + } + consume_value(value); + Ok(()) + } + + fn arguments(context: &mut Context, xs: &[T::Argument]) -> Result, ExecutionError> { + xs.iter().map(|x| argument(context, x)).collect() + } + + fn argument(context: &mut Context, sp!(_, x): &T::Argument) -> Result { + match &x.0 { + T::Argument__::Use(T::Usage::Move(location)) => move_value(context, *location), + T::Argument__::Use(T::Usage::Copy { location, .. }) => copy_value(context, *location), + T::Argument__::Borrow(_, location) => borrow_location(context, *location), + T::Argument__::Read(usage) => read_ref(context, usage), + T::Argument__::Freeze(usage) => freeze_ref(context, usage), + } + } + + fn move_value(context: &mut Context, l: T::Location) -> Result { + let Some(value) = context.location(l).take() else { + invariant_violation!("memory safety should have failed") + }; + Ok(value) + } + + fn copy_value(context: &mut Context, l: T::Location) -> Result { + assert_invariant!( + context.location(l).is_some(), + "memory safety should have failed" + ); + Ok(Value) + } + + fn borrow_location(context: &mut Context, l: T::Location) -> Result { + assert_invariant!( + context.location(l).is_some(), + "memory safety should have failed" + ); + Ok(Value) + } + + fn read_ref(context: &mut Context, u: &T::Usage) -> Result { + let value = match u { + T::Usage::Move(l) => move_value(context, *l)?, + T::Usage::Copy { location, .. } => copy_value(context, *location)?, + }; + consume_value(value); + Ok(Value) + } + + fn freeze_ref(context: &mut Context, u: &T::Usage) -> Result { + let value = match u { + T::Usage::Move(l) => move_value(context, *l)?, + T::Usage::Copy { location, .. } => copy_value(context, *location)?, + }; + consume_value(value); + Ok(Value) + } +} diff --git a/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/verify/input_arguments.rs b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/verify/input_arguments.rs new file mode 100644 index 0000000000000..7ac8995ddf16e --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/verify/input_arguments.rs @@ -0,0 +1,378 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + execution_mode::ExecutionMode, + programmable_transactions::execution::{PrimitiveArgumentLayout, bcs_argument_validate}, + sp, + static_programmable_transactions::{ + env::Env, + loading::ast::{ObjectMutability, Type}, + typing::ast::{self as T, BytesConstraint, ObjectArg}, + }, +}; +use indexmap::IndexSet; +use sui_types::{ + base_types::{RESOLVED_ASCII_STR, RESOLVED_STD_OPTION, RESOLVED_UTF8_STR}, + error::{ExecutionError, ExecutionErrorKind, command_argument_error}, + execution_status::CommandArgumentError, + id::RESOLVED_SUI_ID, + transfer::RESOLVED_RECEIVING_STRUCT, +}; + +struct ObjectUsage { + allow_by_value: bool, + allow_by_mut_ref: bool, +} + +struct Context { + objects: Vec, +} + +impl Context { + fn new(txn: &T::Transaction) -> Self { + let objects = txn + .objects + .iter() + .map(|object_input| match &object_input.arg { + ObjectArg::ImmObject(_) => ObjectUsage { + allow_by_value: false, + allow_by_mut_ref: false, + }, + ObjectArg::OwnedObject(_) => ObjectUsage { + allow_by_value: true, + allow_by_mut_ref: true, + }, + ObjectArg::SharedObject { mutability, .. } => ObjectUsage { + allow_by_value: match mutability { + ObjectMutability::Mutable => true, + ObjectMutability::Immutable => false, + // NonExclusiveWrite can be taken by value, but unless it is re-shared + // with no mutations, the transaction will abort. + ObjectMutability::NonExclusiveWrite => true, + }, + allow_by_mut_ref: match mutability { + ObjectMutability::Mutable => true, + ObjectMutability::Immutable => false, + ObjectMutability::NonExclusiveWrite => true, + }, + }, + }) + .collect(); + Self { objects } + } +} + +/// Verifies two properties for input objects: +/// 1. That the `Pure` inputs can be serialized to the type inferred and that the type is +/// permissible +/// - Can be relaxed under certain execution modes +/// 2. That any `Object` arguments are used validly. This means mutable references are taken only +/// on mutable objects. And that the gas coin is only taken by value in transfer objects +pub fn verify(_env: &Env, txn: &T::Transaction) -> Result<(), ExecutionError> { + let T::Transaction { + bytes, + objects: _, + withdrawals: _, + pure, + receiving, + commands, + } = txn; + for pure in pure { + check_pure_input::(bytes, pure)?; + } + for receiving in receiving { + check_receving_input(receiving)?; + } + let context = &mut Context::new(txn); + for c in commands { + command(context, c).map_err(|e| e.with_command_index(c.idx as usize))?; + } + Ok(()) +} + +//************************************************************************************************** +// Pure bytes +//************************************************************************************************** + +fn check_pure_input( + bytes: &IndexSet>, + pure: &T::PureInput, +) -> Result<(), ExecutionError> { + let T::PureInput { + original_input_index, + byte_index, + ty, + constraint, + } = pure; + let Some(bcs_bytes) = bytes.get_index(*byte_index) else { + invariant_violation!( + "Unbound byte index {} for pure input at index {}", + byte_index, + original_input_index.0 + ); + }; + let BytesConstraint { command, argument } = constraint; + check_pure_bytes::(*argument, bcs_bytes, ty) + .map_err(|e| e.with_command_index(*command as usize)) +} + +fn check_pure_bytes( + command_arg_idx: u16, + bytes: &[u8], + constraint: &Type, +) -> Result<(), ExecutionError> { + assert_invariant!( + !matches!(constraint, Type::Reference(_, _)), + "references should not be added as a constraint" + ); + if Mode::allow_arbitrary_values() { + return Ok(()); + } + let Some(layout) = primitive_serialization_layout(constraint)? else { + let msg = format!( + "Invalid usage of `Pure` argument for a non-primitive argument type at index {command_arg_idx}.", + ); + return Err(ExecutionError::new_with_source( + ExecutionErrorKind::command_argument_error( + CommandArgumentError::InvalidUsageOfPureArg, + command_arg_idx, + ), + msg, + )); + }; + bcs_argument_validate(bytes, command_arg_idx, layout)?; + Ok(()) +} + +fn primitive_serialization_layout( + param_ty: &Type, +) -> Result, ExecutionError> { + Ok(match param_ty { + Type::Signer => return Ok(None), + Type::Reference(_, _) => { + invariant_violation!("references should not be added as a constraint") + } + Type::Bool => Some(PrimitiveArgumentLayout::Bool), + Type::U8 => Some(PrimitiveArgumentLayout::U8), + Type::U16 => Some(PrimitiveArgumentLayout::U16), + Type::U32 => Some(PrimitiveArgumentLayout::U32), + Type::U64 => Some(PrimitiveArgumentLayout::U64), + Type::U128 => Some(PrimitiveArgumentLayout::U128), + Type::U256 => Some(PrimitiveArgumentLayout::U256), + Type::Address => Some(PrimitiveArgumentLayout::Address), + + Type::Vector(v) => { + let info_opt = primitive_serialization_layout(&v.element_type)?; + info_opt.map(|layout| PrimitiveArgumentLayout::Vector(Box::new(layout))) + } + Type::Datatype(dt) => { + let resolved = dt.qualified_ident(); + // is option of a string + if resolved == RESOLVED_STD_OPTION && dt.type_arguments.len() == 1 { + let info_opt = primitive_serialization_layout(&dt.type_arguments[0])?; + info_opt.map(|layout| PrimitiveArgumentLayout::Option(Box::new(layout))) + } else if dt.type_arguments.is_empty() { + if resolved == RESOLVED_SUI_ID { + Some(PrimitiveArgumentLayout::Address) + } else if resolved == RESOLVED_ASCII_STR { + Some(PrimitiveArgumentLayout::Ascii) + } else if resolved == RESOLVED_UTF8_STR { + Some(PrimitiveArgumentLayout::UTF8) + } else { + None + } + } else { + None + } + } + }) +} + +fn check_receving_input(receiving: &T::ReceivingInput) -> Result<(), ExecutionError> { + let T::ReceivingInput { + original_input_index: _, + object_ref: _, + ty, + constraint, + } = receiving; + let BytesConstraint { command, argument } = constraint; + check_receiving(*argument, ty).map_err(|e| e.with_command_index(*command as usize)) +} + +fn check_receiving(command_arg_idx: u16, constraint: &Type) -> Result<(), ExecutionError> { + if is_valid_receiving(constraint) { + Ok(()) + } else { + Err(command_argument_error( + CommandArgumentError::TypeMismatch, + command_arg_idx as usize, + )) + } +} + +pub fn is_valid_pure_type(constraint: &Type) -> Result { + Ok(primitive_serialization_layout(constraint)?.is_some()) +} + +/// Returns true if a type is a `Receiving` where `t` has `key` +pub fn is_valid_receiving(constraint: &Type) -> bool { + let Type::Datatype(dt) = constraint else { + return false; + }; + dt.qualified_ident() == RESOLVED_RECEIVING_STRUCT + && dt.type_arguments.len() == 1 + && dt.type_arguments[0].abilities().has_key() +} + +//************************************************************************************************** +// Object usage +//************************************************************************************************** + +fn command(context: &mut Context, sp!(_, c): &T::Command) -> Result<(), ExecutionError> { + match &c.command { + T::Command__::MoveCall(mc) => { + check_obj_usages(context, &mc.arguments)?; + check_gas_by_values(&mc.arguments)?; + } + T::Command__::TransferObjects(objects, recipient) => { + check_obj_usages(context, objects)?; + check_obj_usage(context, recipient)?; + // gas can be used by value in TransferObjects + } + T::Command__::SplitCoins(_, coin, amounts) => { + check_obj_usage(context, coin)?; + check_obj_usages(context, amounts)?; + check_gas_by_value(coin)?; + check_gas_by_values(amounts)?; + } + T::Command__::MergeCoins(_, target, coins) => { + check_obj_usage(context, target)?; + check_obj_usages(context, coins)?; + check_gas_by_value(target)?; + check_gas_by_values(coins)?; + } + T::Command__::MakeMoveVec(_, xs) => { + check_obj_usages(context, xs)?; + check_gas_by_values(xs)?; + } + T::Command__::Publish(_, _, _) => (), + T::Command__::Upgrade(_, _, _, x, _) => { + check_obj_usage(context, x)?; + check_gas_by_value(x)?; + } + } + Ok(()) +} + +// Checks for valid by-mut-ref and by-value usage of input objects +fn check_obj_usages( + context: &mut Context, + arguments: &[T::Argument], +) -> Result<(), ExecutionError> { + for arg in arguments { + check_obj_usage(context, arg)?; + } + Ok(()) +} + +fn check_obj_usage(context: &mut Context, arg: &T::Argument) -> Result<(), ExecutionError> { + match &arg.value.0 { + T::Argument__::Borrow(true, l) => check_obj_by_mut_ref(context, arg.idx, l), + T::Argument__::Use(T::Usage::Move(l)) => check_by_value(context, arg.idx, l), + // We do not care about + // - immutable object borrowing + // - copying/read ref (since you cannot copy objects) + // - freeze (since an input object cannot be a reference without a borrow) + T::Argument__::Borrow(false, _) + | T::Argument__::Use(T::Usage::Copy { .. }) + | T::Argument__::Read(_) + | T::Argument__::Freeze(_) => Ok(()), + } +} + +// Checks for valid by-mut-ref usage of input objects +fn check_obj_by_mut_ref( + context: &mut Context, + arg_idx: u16, + location: &T::Location, +) -> Result<(), ExecutionError> { + match location { + T::Location::WithdrawalInput(_) + | T::Location::PureInput(_) + | T::Location::ReceivingInput(_) + | T::Location::TxContext + | T::Location::GasCoin + | T::Location::Result(_, _) => Ok(()), + T::Location::ObjectInput(idx) => { + if !context.objects[*idx as usize].allow_by_mut_ref { + Err(command_argument_error( + CommandArgumentError::InvalidObjectByMutRef, + arg_idx as usize, + )) + } else { + Ok(()) + } + } + } +} + +// Checks for valid by-value usage of input objects +fn check_by_value( + context: &mut Context, + arg_idx: u16, + location: &T::Location, +) -> Result<(), ExecutionError> { + match location { + T::Location::GasCoin + | T::Location::Result(_, _) + | T::Location::TxContext + | T::Location::WithdrawalInput(_) + | T::Location::PureInput(_) + | T::Location::ReceivingInput(_) => Ok(()), + T::Location::ObjectInput(idx) => { + if !context.objects[*idx as usize].allow_by_value { + Err(command_argument_error( + CommandArgumentError::InvalidObjectByValue, + arg_idx as usize, + )) + } else { + Ok(()) + } + } + } +} + +// Checks for no by value usage of gas +fn check_gas_by_values(arguments: &[T::Argument]) -> Result<(), ExecutionError> { + for arg in arguments { + check_gas_by_value(arg)?; + } + Ok(()) +} + +fn check_gas_by_value(arg: &T::Argument) -> Result<(), ExecutionError> { + match &arg.value.0 { + T::Argument__::Use(T::Usage::Move(l)) => check_gas_by_value_loc(arg.idx, l), + // We do not care about the read/freeze case since they cannot move an object input + T::Argument__::Borrow(_, _) + | T::Argument__::Use(T::Usage::Copy { .. }) + | T::Argument__::Read(_) + | T::Argument__::Freeze(_) => Ok(()), + } +} + +fn check_gas_by_value_loc(idx: u16, location: &T::Location) -> Result<(), ExecutionError> { + match location { + T::Location::GasCoin => Err(command_argument_error( + CommandArgumentError::InvalidGasCoinUsage, + idx as usize, + )), + T::Location::TxContext + | T::Location::ObjectInput(_) + | T::Location::WithdrawalInput(_) + | T::Location::PureInput(_) + | T::Location::ReceivingInput(_) + | T::Location::Result(_, _) => Ok(()), + } +} diff --git a/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/verify/memory_safety.rs b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/verify/memory_safety.rs new file mode 100644 index 0000000000000..5b62ace318d98 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/verify/memory_safety.rs @@ -0,0 +1,580 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + cell::OnceCell, + collections::{BTreeMap, BTreeSet}, + fmt, +}; + +use crate::{ + sp, + static_programmable_transactions::{ + env::Env, + typing::ast::{self as T, Type}, + }, +}; +use move_regex_borrow_graph::references::Ref; +use sui_types::{ + error::{ExecutionError, command_argument_error}, + execution_status::CommandArgumentError, +}; + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +struct Location(T::Location); + +type Graph = move_regex_borrow_graph::collections::Graph<(), Location>; +type Paths = move_regex_borrow_graph::collections::Paths<(), Location>; + +#[must_use] +enum Value { + Ref(Ref), + NonRef, +} + +struct Context { + graph: Graph, + local_root: Ref, + tx_context: Option, + gas_coin: Option, + objects: Vec>, + withdrawals: Vec>, + pure: Vec>, + receiving: Vec>, + results: Vec>>, +} + +impl Value { + fn is_ref(&self) -> bool { + match self { + Value::Ref(_) => true, + Value::NonRef => false, + } + } + + fn is_non_ref(&self) -> bool { + match self { + Value::Ref(_) => false, + Value::NonRef => true, + } + } + + fn to_ref(&self) -> Option { + match self { + Value::Ref(r) => Some(*r), + Value::NonRef => None, + } + } +} + +impl Context { + fn new(ast: &T::Transaction) -> Result { + let objects = ast.objects.iter().map(|_| Some(Value::NonRef)).collect(); + let withdrawals = ast + .withdrawals + .iter() + .map(|_| Some(Value::NonRef)) + .collect::>(); + let pure = ast + .pure + .iter() + .map(|_| Some(Value::NonRef)) + .collect::>(); + let receiving = ast + .receiving + .iter() + .map(|_| Some(Value::NonRef)) + .collect::>(); + let (mut graph, _locals) = Graph::new::<()>([]).map_err(graph_err)?; + let local_root = graph + .extend_by_epsilon((), std::iter::empty(), /* is_mut */ true) + .map_err(graph_err)?; + Ok(Self { + graph, + local_root, + tx_context: Some(Value::NonRef), + gas_coin: Some(Value::NonRef), + objects, + withdrawals, + pure, + receiving, + results: Vec::with_capacity(ast.commands.len()), + }) + } + + fn location(&mut self, l: T::Location) -> &mut Option { + match l { + T::Location::TxContext => &mut self.tx_context, + T::Location::GasCoin => &mut self.gas_coin, + T::Location::ObjectInput(i) => &mut self.objects[i as usize], + T::Location::WithdrawalInput(i) => &mut self.withdrawals[i as usize], + T::Location::PureInput(i) => &mut self.pure[i as usize], + T::Location::ReceivingInput(i) => &mut self.receiving[i as usize], + T::Location::Result(i, j) => &mut self.results[i as usize][j as usize], + } + } + + fn is_mutable(&self, r: Ref) -> Result { + self.graph.is_mutable(r).map_err(graph_err) + } + + fn borrowed_by(&self, r: Ref) -> Result, ExecutionError> { + self.graph.borrowed_by(r).map_err(graph_err) + } + + /// Used for checking if a location is borrowed + /// Used for updating the borrowed marker in Copy, and for correctness of Move + fn is_location_borrowed(&self, l: T::Location) -> Result { + let borrowed_by = self.borrowed_by(self.local_root)?; + Ok(borrowed_by + .iter() + .any(|(_, paths)| paths.iter().any(|path| path.starts_with(&Location(l))))) + } + + fn release(&mut self, r: Ref) -> Result<(), ExecutionError> { + self.graph.release(r).map_err(graph_err) + } + + fn extend_by_epsilon(&mut self, r: Ref, is_mut: bool) -> Result { + let new_r = self + .graph + .extend_by_epsilon((), std::iter::once(r), is_mut) + .map_err(graph_err)?; + Ok(new_r) + } + + fn extend_by_label( + &mut self, + r: Ref, + is_mut: bool, + extension: T::Location, + ) -> Result { + let new_r = self + .graph + .extend_by_label((), std::iter::once(r), is_mut, Location(extension)) + .map_err(graph_err)?; + Ok(new_r) + } + + fn extend_by_dot_star_for_call( + &mut self, + sources: &BTreeSet, + mutabilities: Vec, + ) -> Result, ExecutionError> { + let new_refs = self + .graph + .extend_by_dot_star_for_call((), sources.iter().copied(), mutabilities) + .map_err(graph_err)?; + Ok(new_refs) + } + + // Writable if + // No imm equal + // No extensions + fn is_writable(&self, r: Ref) -> Result { + debug_assert!(self.is_mutable(r)?); + Ok(self + .borrowed_by(r)? + .values() + .all(|paths| paths.iter().all(|path| path.is_epsilon()))) + } + + // is in reference not able to be used in a call or return + fn find_non_transferrable(&self, refs: &BTreeSet) -> Result, ExecutionError> { + let borrows = refs + .iter() + .copied() + .map(|r| Ok((r, self.borrowed_by(r)?))) + .collect::, ExecutionError>>()?; + let mut_refs = refs + .iter() + .copied() + .filter_map(|r| match self.is_mutable(r) { + Ok(true) => Some(Ok(r)), + Ok(false) => None, + Err(e) => Some(Err(e)), + }) + .collect::, ExecutionError>>()?; + for (r, borrowed_by) in borrows { + let is_mut = mut_refs.contains(&r); + for (borrower, paths) in borrowed_by { + if !is_mut { + if mut_refs.contains(&borrower) { + // If the ref is imm, but is borrowed by a mut ref in the set + // the mut ref is not transferrable + // In other words, the mut ref is an extension of the imm ref + return Ok(Some(borrower)); + } + } else { + for path in paths { + if !path.is_epsilon() || refs.contains(&borrower) { + // If the ref is mut, it cannot have any non-epsilon extensions + // If extension is epsilon (an alias), it cannot be in the transfer set + return Ok(Some(r)); + } + } + } + } + } + Ok(None) + } +} + +/// Checks the following +/// - Values are not used after being moved +/// - Reference safety is upheld (no dangling references) +pub fn verify(_env: &Env, ast: &T::Transaction) -> Result<(), ExecutionError> { + let mut context = Context::new(ast)?; + let commands = &ast.commands; + for c in commands { + let result = command(&mut context, c).map_err(|e| e.with_command_index(c.idx as usize))?; + assert_invariant!( + result.len() == c.value.result_type.len(), + "result length mismatch for command. {c:?}" + ); + // drop unused result values + assert_invariant!( + result.len() == c.value.drop_values.len(), + "drop values length mismatch for command. {c:?}" + ); + let result_values = result + .into_iter() + .zip(c.value.drop_values.iter().copied()) + .map(|(v, drop)| { + Ok(if !drop { + Some(v) + } else { + consume_value(&mut context, v)?; + None + }) + }) + .collect::, ExecutionError>>()?; + context.results.push(result_values); + } + + let Context { + gas_coin, + objects, + pure, + receiving, + results, + .. + } = &mut context; + let gas_coin = gas_coin.take(); + let objects = std::mem::take(objects); + let pure = std::mem::take(pure); + let receiving = std::mem::take(receiving); + let results = std::mem::take(results); + consume_value_opt(&mut context, gas_coin)?; + for vopt in objects.into_iter().chain(pure).chain(receiving) { + consume_value_opt(&mut context, vopt)?; + } + for result in results { + for vopt in result { + consume_value_opt(&mut context, vopt)?; + } + } + + assert_invariant!( + context.borrowed_by(context.local_root)?.is_empty(), + "reference to local root not released" + ); + context.release(context.local_root)?; + assert_invariant!(context.graph.abstract_size() == 0, "reference not released"); + assert_invariant!( + context.tx_context.is_some(), + "tx_context should never be moved" + ); + + Ok(()) +} + +fn command(context: &mut Context, sp!(_, c): &T::Command) -> Result, ExecutionError> { + let result_tys = &c.result_type; + Ok(match &c.command { + T::Command__::MoveCall(mc) => { + let T::MoveCall { + function, + arguments: args, + } = &**mc; + let arg_values = arguments(context, args)?; + call(context, arg_values, &function.signature)? + } + T::Command__::TransferObjects(objects, recipient) => { + let object_values = arguments(context, objects)?; + let recipient_value = argument(context, recipient)?; + consume_values(context, object_values)?; + consume_value(context, recipient_value)?; + vec![] + } + T::Command__::SplitCoins(_, coin, amounts) => { + let coin_value = argument(context, coin)?; + let amount_values = arguments(context, amounts)?; + consume_values(context, amount_values)?; + write_ref(context, 0, coin_value)?; + (0..amounts.len()).map(|_| Value::NonRef).collect() + } + T::Command__::MergeCoins(_, target, coins) => { + let target_value = argument(context, target)?; + let coin_values = arguments(context, coins)?; + consume_values(context, coin_values)?; + write_ref(context, 0, target_value)?; + vec![] + } + T::Command__::MakeMoveVec(_, xs) => { + let vs = arguments(context, xs)?; + consume_values(context, vs)?; + vec![Value::NonRef] + } + T::Command__::Publish(_, _, _) => result_tys.iter().map(|_| Value::NonRef).collect(), + T::Command__::Upgrade(_, _, _, x, _) => { + let v = argument(context, x)?; + consume_value(context, v)?; + vec![Value::NonRef] + } + }) +} + +//************************************************************************************************** +// Abstract State +//************************************************************************************************** + +fn consume_values(context: &mut Context, values: Vec) -> Result<(), ExecutionError> { + for v in values { + consume_value(context, v)?; + } + Ok(()) +} + +fn consume_value_opt(context: &mut Context, value: Option) -> Result<(), ExecutionError> { + match value { + Some(v) => consume_value(context, v), + None => Ok(()), + } +} + +fn consume_value(context: &mut Context, value: Value) -> Result<(), ExecutionError> { + match value { + Value::NonRef => Ok(()), + Value::Ref(r) => { + context.release(r)?; + Ok(()) + } + } +} + +fn arguments(context: &mut Context, xs: &[T::Argument]) -> Result, ExecutionError> { + xs.iter().map(|x| argument(context, x)).collect() +} + +fn argument(context: &mut Context, x: &T::Argument) -> Result { + match &x.value.0 { + T::Argument__::Use(T::Usage::Move(location)) => move_value(context, x.idx, *location), + T::Argument__::Use(T::Usage::Copy { location, borrowed }) => { + copy_value(context, x.idx, *location, borrowed) + } + T::Argument__::Borrow(is_mut, location) => { + borrow_location(context, x.idx, *is_mut, *location) + } + T::Argument__::Read(usage) => read_ref(context, x.idx, usage), + T::Argument__::Freeze(usage) => freeze_ref(context, x.idx, usage), + } +} + +fn move_value( + context: &mut Context, + arg_idx: u16, + l: T::Location, +) -> Result { + if context.is_location_borrowed(l)? { + // TODO more specific error + return Err(command_argument_error( + CommandArgumentError::CannotMoveBorrowedValue, + arg_idx as usize, + )); + } + let Some(value) = context.location(l).take() else { + return Err(command_argument_error( + CommandArgumentError::ArgumentWithoutValue, + arg_idx as usize, + )); + }; + Ok(value) +} + +fn copy_value( + context: &mut Context, + arg_idx: u16, + l: T::Location, + borrowed: &OnceCell, +) -> Result { + let is_borrowed = context.is_location_borrowed(l)?; + borrowed + .set(is_borrowed) + .map_err(|_| make_invariant_violation!("Copy's borrowed marker should not yet be set"))?; + + let Some(value) = context.location(l) else { + // TODO more specific error + return Err(command_argument_error( + CommandArgumentError::ArgumentWithoutValue, + arg_idx as usize, + )); + }; + Ok(match value { + Value::Ref(r) => { + let r = *r; + let is_mut = context.is_mutable(r)?; + let new_r = context.extend_by_epsilon(r, is_mut)?; + Value::Ref(new_r) + } + Value::NonRef => Value::NonRef, + }) +} + +fn borrow_location( + context: &mut Context, + arg_idx: u16, + is_mut: bool, + l: T::Location, +) -> Result { + // check that the location has a value + let Some(value) = context.location(l) else { + // TODO more specific error + return Err(command_argument_error( + CommandArgumentError::ArgumentWithoutValue, + arg_idx as usize, + )); + }; + assert_invariant!( + value.is_non_ref(), + "type checking should guarantee no borrowing of references" + ); + let new_r = context.extend_by_label(context.local_root, is_mut, l)?; + Ok(Value::Ref(new_r)) +} + +/// Creates an alias to the reference, but one that is immutable +fn freeze_ref(context: &mut Context, arg_idx: u16, u: &T::Usage) -> Result { + let value = match u { + T::Usage::Move(l) => move_value(context, arg_idx, *l)?, + T::Usage::Copy { location, borrowed } => copy_value(context, arg_idx, *location, borrowed)?, + }; + let Some(r) = value.to_ref() else { + invariant_violation!("type checking should guarantee FreezeRef is used on only references") + }; + let new_r = context.extend_by_epsilon(r, /* is_mut */ false)?; + consume_value(context, value)?; + Ok(Value::Ref(new_r)) +} + +fn read_ref(context: &mut Context, arg_idx: u16, u: &T::Usage) -> Result { + let value = match u { + T::Usage::Move(l) => move_value(context, arg_idx, *l)?, + T::Usage::Copy { location, borrowed } => copy_value(context, arg_idx, *location, borrowed)?, + }; + assert_invariant!( + value.is_ref(), + "type checking should guarantee ReadRef is used on only references" + ); + consume_value(context, value)?; + Ok(Value::NonRef) +} + +fn write_ref(context: &mut Context, arg_idx: usize, value: Value) -> Result<(), ExecutionError> { + let Value::Ref(r) = value else { + invariant_violation!("type checking should guarantee WriteRef is used on only references"); + }; + + if !context.is_writable(r)? { + // TODO more specific error + return Err(command_argument_error( + CommandArgumentError::CannotWriteToExtendedReference, + arg_idx, + )); + } + consume_value(context, value)?; + Ok(()) +} + +fn call( + context: &mut Context, + arg_values: Vec, + signature: &T::LoadedFunctionInstantiation, +) -> Result, ExecutionError> { + let sources = arg_values + .iter() + .filter_map(|v| v.to_ref()) + .collect::>(); + if let Some(v) = context.find_non_transferrable(&sources)? { + let mut_idx = arg_values + .iter() + .zip(&signature.parameters) + .enumerate() + .find(|(_, (x, ty))| x.to_ref() == Some(v) && matches!(ty, Type::Reference(true, _))); + + let Some((idx, _)) = mut_idx else { + invariant_violation!("non transferrable value was not found in arguments"); + }; + return Err(command_argument_error( + CommandArgumentError::InvalidReferenceArgument, + idx, + )); + } + let mutabilities = signature + .return_ + .iter() + .filter_map(|ty| match ty { + Type::Reference(is_mut, _) => Some(*is_mut), + _ => None, + }) + .collect::>(); + let mutabilities_len = mutabilities.len(); + let mut return_references = context.extend_by_dot_star_for_call(&sources, mutabilities)?; + assert_invariant!( + return_references.len() == mutabilities_len, + "return_references should have the same length as mutabilities" + ); + + let mut return_values: Vec<_> = signature + .return_ + .iter() + .rev() + .map(|ty| { + Ok(match ty { + Type::Reference(_is_mut, _) => { + let Some(new_ref) = return_references.pop() else { + invariant_violation!("return_references has less references than return_"); + }; + debug_assert_eq!(context.is_mutable(new_ref)?, *_is_mut); + Value::Ref(new_ref) + } + _ => Value::NonRef, + }) + }) + .collect::, ExecutionError>>()?; + return_values.reverse(); + assert_invariant!( + return_references.is_empty(), + "return_references has more references than return_" + ); + consume_values(context, arg_values)?; + Ok(return_values) +} + +fn graph_err(e: move_regex_borrow_graph::InvariantViolation) -> ExecutionError { + ExecutionError::invariant_violation(format!("Borrow graph invariant violation: {}", e.0)) +} + +impl fmt::Display for Location { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0 { + T::Location::TxContext => write!(f, "TxContext"), + T::Location::GasCoin => write!(f, "GasCoin"), + T::Location::ObjectInput(idx) => write!(f, "ObjectInput({idx})"), + T::Location::WithdrawalInput(idx) => write!(f, "WithdrawalInput({idx})"), + T::Location::PureInput(idx) => write!(f, "PureInput({idx})"), + T::Location::ReceivingInput(idx) => write!(f, "ReceivingInput({idx})"), + T::Location::Result(i, j) => write!(f, "Result({i}, {j})"), + } + } +} diff --git a/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/verify/mod.rs b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/verify/mod.rs new file mode 100644 index 0000000000000..63dfd237020f3 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/verify/mod.rs @@ -0,0 +1,27 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use sui_types::error::ExecutionError; + +use crate::{ + execution_mode::ExecutionMode, + static_programmable_transactions::{env, typing::ast as T}, +}; + +pub mod drop_safety; +pub mod input_arguments; +pub mod memory_safety; +pub mod move_functions; +pub mod private_entry_arguments; + +pub fn transaction( + env: &env::Env, + ast: &mut T::Transaction, +) -> Result<(), ExecutionError> { + input_arguments::verify::(env, &*ast)?; + move_functions::verify::(env, &*ast)?; + memory_safety::verify(env, &*ast)?; + drop_safety::refine_and_verify::(env, ast)?; + private_entry_arguments::verify::(env, &*ast)?; + Ok(()) +} diff --git a/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/verify/move_functions.rs b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/verify/move_functions.rs new file mode 100644 index 0000000000000..ea57a14f2887c --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/verify/move_functions.rs @@ -0,0 +1,121 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::execution_mode::ExecutionMode; +use crate::programmable_transactions::execution::check_private_generics_v2; +use crate::sp; +use crate::static_programmable_transactions::{env::Env, loading::ast::Type, typing::ast as T}; +use move_binary_format::{CompiledModule, file_format::Visibility}; +use sui_types::error::{ExecutionError, ExecutionErrorKind}; + +/// Checks the following +/// - valid visibility for move function calls +/// - Can be disabled under certain execution modes +/// - private generics rules for move function calls +/// - no references returned from move calls +/// - Can be disabled under certain execution modes +/// - Can be disabled via a feature flag +pub fn verify(env: &Env, txn: &T::Transaction) -> Result<(), ExecutionError> { + for c in &txn.commands { + command::(env, c).map_err(|e| e.with_command_index(c.idx as usize))?; + } + Ok(()) +} + +fn command(env: &Env, sp!(_, c): &T::Command) -> Result<(), ExecutionError> { + let T::Command_ { + command, + result_type: _, + drop_values: _, + consumed_shared_objects: _, + } = c; + match command { + T::Command__::MoveCall(call) => move_call::(env, call)?, + T::Command__::TransferObjects(_, _) + | T::Command__::SplitCoins(_, _, _) + | T::Command__::MergeCoins(_, _, _) + | T::Command__::MakeMoveVec(_, _) + | T::Command__::Publish(_, _, _) + | T::Command__::Upgrade(_, _, _, _, _) => (), + } + Ok(()) +} + +/// Checks a move call for +/// - valid signature (no references in return type) +/// - valid visibility +/// - private generics rules +fn move_call(env: &Env, call: &T::MoveCall) -> Result<(), ExecutionError> { + let T::MoveCall { + function, + arguments: _, + } = call; + check_signature::(env, function)?; + check_private_generics_v2(&function.runtime_id, function.name.as_ident_str())?; + check_visibility::(env, function)?; + Ok(()) +} + +fn check_signature( + env: &Env, + function: &T::LoadedFunction, +) -> Result<(), ExecutionError> { + fn check_return_type( + idx: usize, + return_type: &T::Type, + ) -> Result<(), ExecutionError> { + if let Type::Reference(_, _) = return_type + && !Mode::allow_arbitrary_values() + { + return Err(ExecutionError::from_kind( + ExecutionErrorKind::InvalidPublicFunctionReturnType { idx: idx as u16 }, + )); + } + Ok(()) + } + + if env.protocol_config.allow_references_in_ptbs() { + return Ok(()); + } + + for (idx, ty) in function.signature.return_.iter().enumerate() { + check_return_type::(idx, ty)?; + } + Ok(()) +} + +fn check_visibility( + env: &Env, + function: &T::LoadedFunction, +) -> Result<(), ExecutionError> { + let module = env.module_definition(&function.runtime_id, &function.linkage)?; + let module: &CompiledModule = module.as_ref(); + let Some((_index, fdef)) = module.find_function_def_by_name(function.name.as_str()) else { + invariant_violation!( + "Could not resolve function '{}' in module {}. \ + This should have been checked when linking", + &function.name, + module.self_id(), + ); + }; + let visibility = fdef.visibility; + let is_entry = fdef.is_entry; + match (visibility, is_entry) { + // can call entry + (Visibility::Private | Visibility::Friend, true) => (), + // can call public entry + (Visibility::Public, true) => (), + // can call public + (Visibility::Public, false) => (), + // cannot call private or friend if not entry + (Visibility::Private | Visibility::Friend, false) => { + if !Mode::allow_arbitrary_function_calls() { + return Err(ExecutionError::new_with_source( + ExecutionErrorKind::NonEntryFunctionInvoked, + "Can only call `entry` or `public` functions", + )); + } + } + }; + Ok(()) +} diff --git a/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/verify/private_entry_arguments.rs b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/verify/private_entry_arguments.rs new file mode 100644 index 0000000000000..57228a6b44188 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/static_programmable_transactions/typing/verify/private_entry_arguments.rs @@ -0,0 +1,514 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//! See `verify` function for details about this verification pass. + +use std::collections::BTreeSet; + +use crate::execution_mode::ExecutionMode; +use crate::sp; +use crate::static_programmable_transactions::{env::Env, typing::ast as T}; +use move_binary_format::{CompiledModule, file_format::Visibility}; +use sui_types::{ + error::{ExecutionError, command_argument_error}, + execution_status::CommandArgumentError, +}; + +/// Marks if a clique is hot or not +#[derive(Clone, Copy, Debug)] +enum Temperature { + /// The clique is always hot--since it's heat was not the result of a hot potato value + AlwaysHot, + /// The clique is hot because it contains hot potato values--tracked via the `usize` count + Count(usize), +} + +/// The data for a clique, which tracks the what values/locations are used together. Once a value +/// is used with another in a `Command`, their cliques are merged forever. +enum Clique { + Merged(CliqueID), + Root(Temperature), +} + +type CliqueID = usize; + +#[must_use] +enum Value { + // We don't want to entangle the TxContext with cliques + TxContext, + Normal { + /// The clique this value belongs to + clique: CliqueID, + /// If the value contributes to the hot-value count in its clique + heats: bool, + }, +} + +/// A list of all cliques. The size is bound by the number of inputs + number of commands +struct Cliques(Vec); + +struct Context { + cliques: Cliques, + tx_context: Option, + gas_coin: Option, + objects: Vec>, + withdrawals: Vec>, + pure: Vec>, + receiving: Vec>, + results: Vec>>, +} + +impl Temperature { + fn add(self, other: Temperature) -> Result { + Ok(match (self, other) { + (Temperature::AlwaysHot, _) | (_, Temperature::AlwaysHot) => Temperature::AlwaysHot, + (Temperature::Count(a), Temperature::Count(b)) => Temperature::Count( + a.checked_add(b) + .ok_or_else(|| make_invariant_violation!("Hot count overflow"))?, + ), + }) + } + + fn sub(self, b: usize) -> Result { + Ok(match self { + Temperature::AlwaysHot => Temperature::AlwaysHot, + Temperature::Count(a) => Temperature::Count( + a.checked_sub(b) + .ok_or_else(|| make_invariant_violation!("Hot count cannot go negative"))?, + ), + }) + } + + /// Returns true if the clique is always hot or has a positive hot count + fn is_hot(&self) -> bool { + match self { + Temperature::AlwaysHot => true, + Temperature::Count(c) => *c > 0, + } + } +} + +impl Cliques { + /// Creates a new empty set of cliques + fn new() -> Self { + Self(vec![]) + } + + /// Creates a new clique and returns its ID + fn next(&mut self) -> CliqueID { + let id = self.0.len(); + self.0.push(Clique::Root(Temperature::Count(0))); + id + } + + /// Returns the root of the clique (resolving any merges/forwards) + fn root(&self, id: CliqueID) -> Result { + let mut visited = BTreeSet::from([id]); + let mut cur = id; + loop { + match &self.0[cur] { + Clique::Root(_) => return Ok(cur), + Clique::Merged(next) => { + let newly_visited = visited.insert(*next); + if !newly_visited { + invariant_violation!("Clique merge cycle detected"); + } + cur = *next + } + } + } + } + + /// Returns the temperature of the clique (at the root) + fn temp(&self, id: CliqueID) -> Result { + let root = self.root(id)?; + let Clique::Root(temp) = self.0[root] else { + invariant_violation!("Clique {root} should be a root"); + }; + Ok(temp) + } + + /// Returns a mutable reference to the temperature of the clique (at the root) + fn temp_mut(&mut self, id: CliqueID) -> Result<&mut Temperature, ExecutionError> { + let root = self.root(id)?; + let Clique::Root(temp) = &mut self.0[root] else { + invariant_violation!("Clique {root} should be a root"); + }; + Ok(temp) + } + + /// Modifies the temperature of this clique (at the root) via `f`, whose first parameter is the + /// current temperature + fn modify_temp( + &mut self, + id: CliqueID, + f: impl FnOnce(Temperature) -> Result, + ) -> Result<(), ExecutionError> { + let temp = self.temp_mut(id)?; + *temp = f(*temp)?; + Ok(()) + } + + /// Merges the given cliques into one clique + fn merge(&mut self, clique_ids: BTreeSet) -> Result { + let roots: BTreeSet = clique_ids + .iter() + .map(|&id| self.root(id)) + .collect::>()?; + Ok(match roots.len() { + 0 => self.next(), + 1 => *roots.iter().next().unwrap(), + _ => { + let merged = self.next(); + let mut merged_temp = Temperature::Count(0); + for &root in &roots { + let temp = self.temp(root)?; + self.0[root] = Clique::Merged(merged); + merged_temp = merged_temp.add(temp)?; + } + self.0[merged] = Clique::Root(merged_temp); + // For efficiency, forward all the non-roots to the merged root + // (bypassing the old root) + for id in clique_ids { + self.0[id] = Clique::Merged(merged); + } + merged + } + }) + } + + /// Creates a new value in its own clique (not hot) + fn input_value(&mut self) -> Value { + let clique = self.next(); + Value::Normal { + clique, + heats: false, + } + } + + /// Creates a new value in the given `clique` and bumps the hot count if `heats` is true + fn new_value(&mut self, clique: CliqueID, heats: bool) -> Result { + if heats { + self.modify_temp(clique, |t| t.add(Temperature::Count(1)))?; + } + Ok(Value::Normal { clique, heats }) + } + + /// Releases a value, decrementing the hot count of its clique if it `heats`. + fn release_value(&mut self, value: Value) -> Result, ExecutionError> { + let (clique, heats) = match value { + Value::TxContext => return Ok(None), + Value::Normal { clique, heats } => (clique, heats), + }; + if heats { + self.modify_temp(clique, |t| t.sub(1))?; + } + Ok(Some(clique)) + } + + /// Returns true if the clique is hot, always hot or a positive hot count + fn is_hot(&self, clique: CliqueID) -> Result { + Ok(self.temp(clique)?.is_hot()) + } + + /// Marks the given clique as always hot + fn mark_always_hot(&mut self, clique: CliqueID) -> Result<(), ExecutionError> { + self.modify_temp(clique, |_| Ok(Temperature::AlwaysHot)) + } +} + +impl Context { + fn new(txn: &T::Transaction) -> Self { + let mut cliques = Cliques::new(); + let tx_context = Some(Value::TxContext); + let gas_coin = Some(cliques.input_value()); + let objects = (0..txn.objects.len()) + .map(|_| Some(cliques.input_value())) + .collect(); + let withdrawals = (0..txn.withdrawals.len()) + .map(|_| Some(cliques.input_value())) + .collect(); + let pure = (0..txn.pure.len()) + .map(|_| Some(cliques.input_value())) + .collect(); + let receiving = (0..txn.receiving.len()) + .map(|_| Some(cliques.input_value())) + .collect(); + Self { + tx_context, + cliques, + gas_coin, + objects, + withdrawals, + pure, + receiving, + results: vec![], + } + } + + // Checks if all values are released and that all hot counts are zero + fn finish(self) -> Result<(), ExecutionError> { + let Context { + mut cliques, + tx_context, + gas_coin, + objects, + withdrawals, + pure, + receiving, + results, + } = self; + + // Check that if all values are released, the hot counts are zero + let all_values = tx_context + .into_iter() + .chain(gas_coin) + .chain(objects.into_iter().flatten()) + .chain(withdrawals.into_iter().flatten()) + .chain(pure.into_iter().flatten()) + .chain(receiving.into_iter().flatten()) + .chain(results.into_iter().flatten().flatten()); + let mut clique_ids: BTreeSet = BTreeSet::new(); + for value in all_values { + let clique = match &value { + Value::TxContext => continue, + Value::Normal { clique, .. } => clique, + }; + clique_ids.insert(*clique); + cliques.release_value(value)?; + } + for id in clique_ids { + match cliques.temp(id)? { + Temperature::AlwaysHot => (), + Temperature::Count(c) => { + assert_invariant!(c == 0, "All hot counts should be zero at end") + } + } + } + Ok(()) + } + + fn location(&mut self, location: &T::Location) -> &mut Option { + match location { + T::Location::GasCoin => &mut self.gas_coin, + T::Location::ObjectInput(i) => &mut self.objects[*i as usize], + T::Location::WithdrawalInput(i) => &mut self.withdrawals[*i as usize], + T::Location::PureInput(i) => &mut self.pure[*i as usize], + T::Location::ReceivingInput(i) => &mut self.receiving[*i as usize], + T::Location::Result(i, j) => &mut self.results[*i as usize][*j as usize], + T::Location::TxContext => &mut self.tx_context, + } + } + + fn usage(&mut self, usage: &T::Usage) -> Result { + match usage { + T::Usage::Move(location) => { + let Some(value) = self.location(location).take() else { + invariant_violation!("Move of moved value"); + }; + Ok(value) + } + T::Usage::Copy { location, .. } => { + let Some(location) = self.location(location).as_ref() else { + invariant_violation!("Copy of moved value"); + }; + let (clique, heats) = match location { + Value::TxContext => { + invariant_violation!("Cannot copy TxContext"); + } + Value::Normal { clique, heats } => (*clique, *heats), + }; + self.cliques.new_value(clique, heats) + } + } + } + + fn argument(&mut self, sp!(_, (arg, _ty)): &T::Argument) -> Result { + Ok(match arg { + T::Argument__::Use(usage) => self.usage(usage)?, + T::Argument__::Read(usage) | T::Argument__::Freeze(usage) => { + // This is equivalent to just the `usage` but we go through the steps of + // creating a new value and releasing the old one for "correctness" and clarity + let value = self.usage(usage)?; + let (clique, heats) = match &value { + Value::TxContext => { + invariant_violation!("Cannot read or freeze TxContext"); + } + Value::Normal { clique, heats } => (*clique, *heats), + }; + let new_value = self.cliques.new_value(clique, heats)?; + self.cliques.release_value(value)?; + new_value + } + T::Argument__::Borrow(_, location) => { + let Some(location) = self.location(location).as_ref() else { + invariant_violation!("Borrow of moved value"); + }; + let (clique, heats) = match location { + Value::TxContext => { + // no clique/heat for TxContext + return Ok(Value::TxContext); + } + Value::Normal { clique, heats } => (*clique, *heats), + }; + // Create a new value (representing the reference to this value) + // that is in the same clique and has the same heat + self.cliques.new_value(clique, heats)? + } + }) + } +} + +/// Checks entry taint rules. An `entry` function cannot have any arguments in a "hot" clique. +/// We define cliques and "hot" with the following rules: +/// - Each input value starts as a node in the graph, as such in its own clique. +/// - When a value is used in a command, we "entangle" all of the values and the outputs. +/// Conceptually, we add an edge between all of the inputs/outputs and any other nodes in the +/// inputs's cliques. As such, we don't really need to track the graph as a whole, just what +/// clique each value is in. +/// - A value is "heats" its clique if it is a "hot potato" value, i.e. a value whose type +/// does not have `drop` and does not have `store`. +/// - A clique is the "hot" in one of two ways: +/// - If it contains 1 or more "hot potato" values +/// - If it is marked as "always hot" if a command consumes a shared object by-value +/// - A non-public `entry` function cannot have any inputs that are in a `hot` clique. +/// - Note that command inputs are released before checking the rules, so an `entry` function can +/// consume a hot potato value if it is the last "heating" value in its clique. +pub fn verify(env: &Env, txn: &T::Transaction) -> Result<(), ExecutionError> { + let mut context = Context::new(txn); + for c in &txn.commands { + let result_values = command::(env, &mut context, c) + .map_err(|e| e.with_command_index(c.idx as usize))?; + assert_invariant!( + result_values.len() == c.value.result_type.len(), + "result length mismatch" + ); + context.results.push(result_values); + } + context.finish()?; + Ok(()) +} + +fn command( + env: &Env, + context: &mut Context, + sp!(_, c): &T::Command, +) -> Result>, ExecutionError> { + let T::Command_ { + command, + result_type, + drop_values, + consumed_shared_objects, + } = c; + let argument_cliques = arguments(env, context, command.arguments())?; + match command { + T::Command__::MoveCall(call) => move_call::(env, context, call, &argument_cliques)?, + T::Command__::TransferObjects(_, _) + | T::Command__::SplitCoins(_, _, _) + | T::Command__::MergeCoins(_, _, _) + | T::Command__::MakeMoveVec(_, _) + | T::Command__::Publish(_, _, _) + | T::Command__::Upgrade(_, _, _, _, _) => (), + } + let merged_clique = context + .cliques + .merge(argument_cliques.into_iter().map(|(_, c)| c).collect())?; + let consumes_shared_objects = !consumed_shared_objects.is_empty(); + if consumes_shared_objects { + context.cliques.mark_always_hot(merged_clique)?; + } + assert_invariant!( + drop_values.len() == result_type.len(), + "drop_values length mismatch" + ); + result_type + .iter() + .zip(drop_values) + .map(|(ty, dropped)| { + Ok(if *dropped { + None + } else { + let heats = is_hot_potato_return_type(ty); + Some(context.cliques.new_value(merged_clique, heats)?) + }) + }) + .collect() +} + +/// Returns the index of the first hot argument, if any +fn arguments<'a>( + env: &Env, + context: &mut Context, + args: impl IntoIterator, +) -> Result, ExecutionError> { + let mut arguments = vec![]; + for arg in args { + if let Some(clique) = argument(env, context, arg)? { + arguments.push((arg.idx, clique)); + } + } + Ok(arguments) +} + +fn argument( + _env: &Env, + context: &mut Context, + arg: &T::Argument, +) -> Result, ExecutionError> { + let value = context.argument(arg)?; + context.cliques.release_value(value) +} + +/// Checks a move call for +/// - valid signature (no references in return type) +/// - valid visibility +/// - private generics rules +/// - if entry, no hot arguments +/// +/// Returns true iff any return type is a hot potato +fn move_call( + env: &Env, + context: &mut Context, + call: &T::MoveCall, + argument_cliques: &[(u16, CliqueID)], +) -> Result<(), ExecutionError> { + let T::MoveCall { + function, + arguments: _, + } = call; + let module = env.module_definition(&function.runtime_id, &function.linkage)?; + let module: &CompiledModule = module.as_ref(); + let Some((_index, fdef)) = module.find_function_def_by_name(function.name.as_str()) else { + invariant_violation!( + "Could not resolve function '{}' in module {}. \ + This should have been checked when linking", + &function.name, + module.self_id(), + ); + }; + let visibility = fdef.visibility; + let is_entry = fdef.is_entry; + // check rules around hot arguments and entry functions + if is_entry && matches!(visibility, Visibility::Private) && !Mode::allow_arbitrary_values() { + let mut hot_argument: Option = None; + for (idx, clique) in argument_cliques { + if context.cliques.is_hot(*clique)? { + hot_argument = Some(*idx); + break; + } + } + if let Some(idx) = hot_argument { + return Err(command_argument_error( + CommandArgumentError::InvalidArgumentToPrivateEntryFunction, + idx as usize, + )); + } + } + Ok(()) +} + +// is missing both drop and store +fn is_hot_potato_return_type(ty: &T::Type) -> bool { + let abilities = ty.abilities(); + !abilities.has_drop() && !abilities.has_store() +} diff --git a/sui-execution/replay_cut/sui-adapter/src/temporary_store.rs b/sui-execution/replay_cut/sui-adapter/src/temporary_store.rs new file mode 100644 index 0000000000000..d7b6dc0836c17 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/temporary_store.rs @@ -0,0 +1,1263 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::gas_charger::GasCharger; +use mysten_metrics::monitored_scope; +use parking_lot::RwLock; +use std::collections::{BTreeMap, BTreeSet, HashSet}; +use sui_protocol_config::ProtocolConfig; +use sui_types::accumulator_event::AccumulatorEvent; +use sui_types::base_types::VersionDigest; +use sui_types::committee::EpochId; +use sui_types::deny_list_v2::check_coin_deny_list_v2_during_execution; +use sui_types::effects::{AccumulatorWriteV1, TransactionEffects, TransactionEvents}; +use sui_types::error::ExecutionErrorKind; +use sui_types::execution::{ + DynamicallyLoadedObjectMetadata, ExecutionResults, ExecutionResultsV2, SharedInput, +}; +use sui_types::execution_status::ExecutionStatus; +use sui_types::inner_temporary_store::InnerTemporaryStore; +use sui_types::layout_resolver::LayoutResolver; +use sui_types::object::Data; +use sui_types::storage::{BackingStore, DenyListResult, PackageObject}; +use sui_types::sui_system_state::{AdvanceEpochParams, get_sui_system_state_wrapper}; +use sui_types::{ + SUI_DENY_LIST_OBJECT_ID, + base_types::{ObjectID, ObjectRef, SequenceNumber, SuiAddress, TransactionDigest}, + effects::EffectsObjectChange, + error::{ExecutionError, SuiResult}, + gas::GasCostSummary, + object::Object, + object::Owner, + storage::{BackingPackageStore, ChildObjectResolver, ParentSync, Storage}, + transaction::InputObjects, +}; +use sui_types::{SUI_SYSTEM_STATE_OBJECT_ID, TypeTag, is_system_package}; + +pub struct TemporaryStore<'backing> { + // The backing store for retrieving Move packages onchain. + // When executing a Move call, the dependent packages are not going to be + // in the input objects. They will be fetched from the backing store. + // Also used for fetching the backing parent_sync to get the last known version for wrapped + // objects + store: &'backing dyn BackingStore, + tx_digest: TransactionDigest, + input_objects: BTreeMap, + + /// Store the original versions of the non-exclusive write inputs, in order to detect + /// mutations (which are illegal, but not prevented by the type system). + non_exclusive_input_original_versions: BTreeMap, + + stream_ended_consensus_objects: BTreeMap, + /// The version to assign to all objects written by the transaction using this store. + lamport_timestamp: SequenceNumber, + /// Inputs that will be mutated by the transaction. Does not include NonExclusiveWrite inputs, + /// which can be taken as `&mut T` but cannot be directly mutated. + mutable_input_refs: BTreeMap, + execution_results: ExecutionResultsV2, + /// Objects that were loaded during execution (dynamic fields + received objects). + loaded_runtime_objects: BTreeMap, + /// A map from wrapped object to its container. Used during expensive invariant checks. + wrapped_object_containers: BTreeMap, + protocol_config: &'backing ProtocolConfig, + + /// Every package that was loaded from DB store during execution. + /// These packages were not previously loaded into the temporary store. + runtime_packages_loaded_from_db: RwLock>, + + /// The set of objects that we may receive during execution. Not guaranteed to receive all, or + /// any of the objects referenced in this set. + receiving_objects: Vec, + + /// The set of all generated object IDs from the object runtime during the transaction. This includes any + /// created-and-then-deleted objects in addition to any `new_ids` which contains only the set + /// of created (but not deleted) IDs in the transaction. + generated_runtime_ids: BTreeSet, + + // TODO: Now that we track epoch here, there are a few places we don't need to pass it around. + /// The current epoch. + cur_epoch: EpochId, + + /// The set of per-epoch config objects that were loaded during execution, and are not in the + /// input objects. This allows us to commit them to the effects. + loaded_per_epoch_config_objects: RwLock>, +} + +impl<'backing> TemporaryStore<'backing> { + /// Creates a new store associated with an authority store, and populates it with + /// initial objects. + pub fn new( + store: &'backing dyn BackingStore, + input_objects: InputObjects, + receiving_objects: Vec, + tx_digest: TransactionDigest, + protocol_config: &'backing ProtocolConfig, + cur_epoch: EpochId, + ) -> Self { + let mutable_input_refs = input_objects.exclusive_mutable_inputs(); + let non_exclusive_input_original_versions = input_objects.non_exclusive_input_objects(); + + let lamport_timestamp = input_objects.lamport_timestamp(&receiving_objects); + let stream_ended_consensus_objects = input_objects.consensus_stream_ended_objects(); + let objects = input_objects.into_object_map(); + #[cfg(debug_assertions)] + { + // Ensure that input objects and receiving objects must not overlap. + assert!( + objects + .keys() + .collect::>() + .intersection( + &receiving_objects + .iter() + .map(|oref| &oref.0) + .collect::>() + ) + .next() + .is_none() + ); + } + Self { + store, + tx_digest, + input_objects: objects, + non_exclusive_input_original_versions, + stream_ended_consensus_objects, + lamport_timestamp, + mutable_input_refs, + execution_results: ExecutionResultsV2::default(), + protocol_config, + loaded_runtime_objects: BTreeMap::new(), + wrapped_object_containers: BTreeMap::new(), + runtime_packages_loaded_from_db: RwLock::new(BTreeMap::new()), + receiving_objects, + generated_runtime_ids: BTreeSet::new(), + cur_epoch, + loaded_per_epoch_config_objects: RwLock::new(BTreeSet::new()), + } + } + + // Helpers to access private fields + pub fn objects(&self) -> &BTreeMap { + &self.input_objects + } + + pub fn update_object_version_and_prev_tx(&mut self) { + self.execution_results.update_version_and_previous_tx( + self.lamport_timestamp, + self.tx_digest, + &self.input_objects, + self.protocol_config.reshare_at_same_initial_version(), + ); + + #[cfg(debug_assertions)] + { + self.check_invariants(); + } + } + + fn merge_accumulator_events( + accumulator_events: &[AccumulatorEvent], + ) -> impl Iterator + '_ { + accumulator_events + .iter() + .fold( + BTreeMap::>::new(), + |mut map, event| { + map.entry(*event.accumulator_obj.inner()) + .or_default() + .push(event.write.clone()); + map + }, + ) + .into_iter() + .map(|(obj_id, writes)| { + ( + obj_id, + EffectsObjectChange::new_from_accumulator_write(AccumulatorWriteV1::merge( + writes, + )), + ) + }) + } + + /// Break up the structure and return its internal stores (objects, active_inputs, written, deleted) + pub fn into_inner(self) -> InnerTemporaryStore { + let results = self.execution_results; + InnerTemporaryStore { + input_objects: self.input_objects, + stream_ended_consensus_objects: self.stream_ended_consensus_objects, + mutable_inputs: self.mutable_input_refs, + written: results.written_objects, + events: TransactionEvents { + data: results.user_events, + }, + accumulator_events: results.accumulator_events, + loaded_runtime_objects: self.loaded_runtime_objects, + runtime_packages_loaded_from_db: self.runtime_packages_loaded_from_db.into_inner(), + lamport_version: self.lamport_timestamp, + binary_config: self.protocol_config.binary_config(None), + } + } + + /// For every object from active_inputs (i.e. all mutable objects), if they are not + /// mutated during the transaction execution, force mutating them by incrementing the + /// sequence number. This is required to achieve safety. + pub(crate) fn ensure_active_inputs_mutated(&mut self) { + let mut to_be_updated = vec![]; + // Note: we do not mutate input objects if they are non-exclusive write + for id in self.mutable_input_refs.keys() { + if !self.execution_results.modified_objects.contains(id) { + // We cannot update here but have to push to `to_be_updated` and update later + // because the for loop is holding a reference to `self`, and calling + // `self.mutate_input_object` requires a mutable reference to `self`. + to_be_updated.push(self.input_objects[id].clone()); + } + } + for object in to_be_updated { + // The object must be mutated as it was present in the input objects + self.mutate_input_object(object.clone()); + } + } + + fn get_object_changes(&self) -> BTreeMap { + let results = &self.execution_results; + let all_ids = results + .created_object_ids + .iter() + .chain(&results.deleted_object_ids) + .chain(&results.modified_objects) + .chain(results.written_objects.keys()) + .collect::>(); + all_ids + .into_iter() + .map(|id| { + ( + *id, + EffectsObjectChange::new( + self.get_object_modified_at(id) + .map(|metadata| ((metadata.version, metadata.digest), metadata.owner)), + results.written_objects.get(id), + results.created_object_ids.contains(id), + results.deleted_object_ids.contains(id), + ), + ) + }) + .chain(Self::merge_accumulator_events(&results.accumulator_events)) + .collect() + } + + pub fn into_effects( + mut self, + shared_object_refs: Vec, + transaction_digest: &TransactionDigest, + mut transaction_dependencies: BTreeSet, + gas_cost_summary: GasCostSummary, + status: ExecutionStatus, + gas_charger: &mut GasCharger, + epoch: EpochId, + ) -> (InnerTemporaryStore, TransactionEffects) { + self.update_object_version_and_prev_tx(); + + // Regardless of execution status (including aborts), we insert the previous transaction + // for any successfully received objects during the transaction. + for (id, expected_version, expected_digest) in &self.receiving_objects { + // If the receiving object is in the loaded runtime objects, then that means that it + // was actually successfully loaded (so existed, and there was authenticated mutable + // access to it). So we insert the previous transaction as a dependency. + if let Some(obj_meta) = self.loaded_runtime_objects.get(id) { + // Check that the expected version, digest, and owner match the loaded version, + // digest, and owner. If they don't then don't register a dependency. + // This is because this could be "spoofed" by loading a dynamic object field. + let loaded_via_receive = obj_meta.version == *expected_version + && obj_meta.digest == *expected_digest + && obj_meta.owner.is_address_owned(); + if loaded_via_receive { + transaction_dependencies.insert(obj_meta.previous_transaction); + } + } + } + + assert!(self.protocol_config.enable_effects_v2()); + + // In the case of special transactions that don't require a gas object, + // we don't really care about the effects to gas, just use the input for it. + // Gas coins are guaranteed to be at least size 1 and if more than 1 + // the first coin is where all the others are merged. + let gas_coin = gas_charger.gas_coin(); + + let object_changes = self.get_object_changes(); + + let lamport_version = self.lamport_timestamp; + // TODO: Cleanup this clone. Potentially add unchanged_shraed_objects directly to InnerTempStore. + let loaded_per_epoch_config_objects = self.loaded_per_epoch_config_objects.read().clone(); + let inner = self.into_inner(); + + let effects = TransactionEffects::new_from_execution_v2( + status, + epoch, + gas_cost_summary, + // TODO: Provide the list of read-only shared objects directly. + shared_object_refs, + loaded_per_epoch_config_objects, + *transaction_digest, + lamport_version, + object_changes, + gas_coin, + if inner.events.data.is_empty() { + None + } else { + Some(inner.events.digest()) + }, + transaction_dependencies.into_iter().collect(), + ); + + (inner, effects) + } + + /// An internal check of the invariants (will only fire in debug) + #[cfg(debug_assertions)] + fn check_invariants(&self) { + // Check not both deleted and written + debug_assert!( + { + self.execution_results + .written_objects + .keys() + .all(|id| !self.execution_results.deleted_object_ids.contains(id)) + }, + "Object both written and deleted." + ); + + // Check all mutable inputs are modified + debug_assert!( + { + self.mutable_input_refs + .keys() + .all(|id| self.execution_results.modified_objects.contains(id)) + }, + "Mutable input not modified." + ); + + debug_assert!( + { + self.execution_results + .written_objects + .values() + .all(|obj| obj.previous_transaction == self.tx_digest) + }, + "Object previous transaction not properly set", + ); + } + + /// Mutate a mutable input object. This is used to mutate input objects outside of PT execution. + pub fn mutate_input_object(&mut self, object: Object) { + let id = object.id(); + debug_assert!(self.input_objects.contains_key(&id)); + debug_assert!(!object.is_immutable()); + self.execution_results.modified_objects.insert(id); + self.execution_results.written_objects.insert(id, object); + } + + /// Mutate a child object outside of PT. This should be used extremely rarely. + /// Currently it's only used by advance_epoch_safe_mode because it's all native + /// without PT. This should almost never be used otherwise. + pub fn mutate_child_object(&mut self, old_object: Object, new_object: Object) { + let id = new_object.id(); + let old_ref = old_object.compute_object_reference(); + debug_assert_eq!(old_ref.0, id); + self.loaded_runtime_objects.insert( + id, + DynamicallyLoadedObjectMetadata { + version: old_ref.1, + digest: old_ref.2, + owner: old_object.owner.clone(), + storage_rebate: old_object.storage_rebate, + previous_transaction: old_object.previous_transaction, + }, + ); + self.execution_results.modified_objects.insert(id); + self.execution_results + .written_objects + .insert(id, new_object); + } + + /// Upgrade system package during epoch change. This requires special treatment + /// since the system package to be upgraded is not in the input objects. + /// We could probably fix above to make it less special. + pub fn upgrade_system_package(&mut self, package: Object) { + let id = package.id(); + assert!(package.is_package() && is_system_package(id)); + self.execution_results.modified_objects.insert(id); + self.execution_results.written_objects.insert(id, package); + } + + /// Crate a new objcet. This is used to create objects outside of PT execution. + pub fn create_object(&mut self, object: Object) { + // Created mutable objects' versions are set to the store's lamport timestamp when it is + // committed to effects. Creating an object at a non-zero version risks violating the + // lamport timestamp invariant (that a transaction's lamport timestamp is strictly greater + // than all versions witnessed by the transaction). + debug_assert!( + object.is_immutable() || object.version() == SequenceNumber::MIN, + "Created mutable objects should not have a version set", + ); + let id = object.id(); + self.execution_results.created_object_ids.insert(id); + self.execution_results.written_objects.insert(id, object); + } + + /// Delete a mutable input object. This is used to delete input objects outside of PT execution. + pub fn delete_input_object(&mut self, id: &ObjectID) { + // there should be no deletion after write + debug_assert!(!self.execution_results.written_objects.contains_key(id)); + debug_assert!(self.input_objects.contains_key(id)); + self.execution_results.modified_objects.insert(*id); + self.execution_results.deleted_object_ids.insert(*id); + } + + pub fn drop_writes(&mut self) { + self.execution_results.drop_writes(); + } + + pub fn read_object(&self, id: &ObjectID) -> Option<&Object> { + // there should be no read after delete + debug_assert!(!self.execution_results.deleted_object_ids.contains(id)); + self.execution_results + .written_objects + .get(id) + .or_else(|| self.input_objects.get(id)) + } + + pub fn save_loaded_runtime_objects( + &mut self, + loaded_runtime_objects: BTreeMap, + ) { + #[cfg(debug_assertions)] + { + for (id, v1) in &loaded_runtime_objects { + if let Some(v2) = self.loaded_runtime_objects.get(id) { + assert_eq!(v1, v2); + } + } + for (id, v1) in &self.loaded_runtime_objects { + if let Some(v2) = loaded_runtime_objects.get(id) { + assert_eq!(v1, v2); + } + } + } + // Merge the two maps because we may be calling the execution engine more than once + // (e.g. in advance epoch transaction, where we may be publishing a new system package). + self.loaded_runtime_objects.extend(loaded_runtime_objects); + } + + pub fn save_wrapped_object_containers( + &mut self, + wrapped_object_containers: BTreeMap, + ) { + #[cfg(debug_assertions)] + { + for (id, container1) in &wrapped_object_containers { + if let Some(container2) = self.wrapped_object_containers.get(id) { + assert_eq!(container1, container2); + } + } + for (id, container1) in &self.wrapped_object_containers { + if let Some(container2) = wrapped_object_containers.get(id) { + assert_eq!(container1, container2); + } + } + } + // Merge the two maps because we may be calling the execution engine more than once + // (e.g. in advance epoch transaction, where we may be publishing a new system package). + self.wrapped_object_containers + .extend(wrapped_object_containers); + } + + pub fn save_generated_object_ids(&mut self, generated_ids: BTreeSet) { + #[cfg(debug_assertions)] + { + for id in &self.generated_runtime_ids { + assert!(!generated_ids.contains(id)) + } + for id in &generated_ids { + assert!(!self.generated_runtime_ids.contains(id)); + } + } + self.generated_runtime_ids.extend(generated_ids); + } + + pub fn estimate_effects_size_upperbound(&self) -> usize { + TransactionEffects::estimate_effects_size_upperbound_v2( + self.execution_results.written_objects.len(), + self.execution_results.modified_objects.len(), + self.input_objects.len(), + ) + } + + pub fn written_objects_size(&self) -> usize { + self.execution_results + .written_objects + .values() + .fold(0, |sum, obj| sum + obj.object_size_for_gas_metering()) + } + + /// If there are unmetered storage rebate (due to system transaction), we put them into + /// the storage rebate of 0x5 object. + /// TODO: This will not work for potential future new system transactions if 0x5 is not in the input. + /// We should fix this. + pub fn conserve_unmetered_storage_rebate(&mut self, unmetered_storage_rebate: u64) { + if unmetered_storage_rebate == 0 { + // If unmetered_storage_rebate is 0, we are most likely executing the genesis transaction. + // And in that case we cannot mutate the 0x5 object because it's newly created. + // And there is no storage rebate that needs distribution anyway. + return; + } + tracing::debug!( + "Amount of unmetered storage rebate from system tx: {:?}", + unmetered_storage_rebate + ); + let mut system_state_wrapper = self + .read_object(&SUI_SYSTEM_STATE_OBJECT_ID) + .expect("0x5 object must be mutated in system tx with unmetered storage rebate") + .clone(); + // In unmetered execution, storage_rebate field of mutated object must be 0. + // If not, we would be dropping SUI on the floor by overriding it. + assert_eq!(system_state_wrapper.storage_rebate, 0); + system_state_wrapper.storage_rebate = unmetered_storage_rebate; + self.mutate_input_object(system_state_wrapper); + } + + /// Add an accumulator event to the execution results + pub fn add_accumulator_event(&mut self, event: AccumulatorEvent) { + self.execution_results.accumulator_events.push(event); + } + + /// Given an object ID, if it's not modified, returns None. + /// Otherwise returns its metadata, including version, digest, owner and storage rebate. + /// A modified object must be either a mutable input, or a loaded child object. + /// The only exception is when we upgrade system packages, in which case the upgraded + /// system packages are not part of input, but are modified. + fn get_object_modified_at( + &self, + object_id: &ObjectID, + ) -> Option { + if self.execution_results.modified_objects.contains(object_id) { + Some( + self.mutable_input_refs + .get(object_id) + .map( + |((version, digest), owner)| DynamicallyLoadedObjectMetadata { + version: *version, + digest: *digest, + owner: owner.clone(), + // It's guaranteed that a mutable input object is an input object. + storage_rebate: self.input_objects[object_id].storage_rebate, + previous_transaction: self.input_objects[object_id] + .previous_transaction, + }, + ) + .or_else(|| self.loaded_runtime_objects.get(object_id).cloned()) + .unwrap_or_else(|| { + debug_assert!(is_system_package(*object_id)); + let package_obj = + self.store.get_package_object(object_id).unwrap().unwrap(); + let obj = package_obj.object(); + DynamicallyLoadedObjectMetadata { + version: obj.version(), + digest: obj.digest(), + owner: obj.owner.clone(), + storage_rebate: obj.storage_rebate, + previous_transaction: obj.previous_transaction, + } + }), + ) + } else { + None + } + } +} + +impl TemporaryStore<'_> { + // check that every object read is owned directly or indirectly by sender, sponsor, + // or a shared object input + pub fn check_ownership_invariants( + &self, + sender: &SuiAddress, + sponsor: &Option, + gas_charger: &mut GasCharger, + mutable_inputs: &HashSet, + is_epoch_change: bool, + ) -> SuiResult<()> { + let gas_objs: HashSet<&ObjectID> = gas_charger.gas_coins().iter().map(|g| &g.0).collect(); + let gas_owner = sponsor.as_ref().unwrap_or(sender); + + // mark input objects as authenticated + let mut authenticated_for_mutation: HashSet<_> = self + .input_objects + .iter() + .filter_map(|(id, obj)| { + match &obj.owner { + Owner::AddressOwner(a) => { + if gas_objs.contains(id) { + // gas object must be owned by sender or sponsor + assert!( + a == gas_owner, + "Gas object must be owned by sender or sponsor" + ); + } else { + assert!(sender == a, "Input object must be owned by sender"); + } + Some(id) + } + Owner::Shared { .. } | Owner::ConsensusAddressOwner { .. } => Some(id), + Owner::Immutable => { + // object is authenticated, but it cannot own other objects, + // so we should not add it to `authenticated_objs` + // However, we would definitely want to add immutable objects + // to the set of authenticated roots if we were doing runtime + // checks inside the VM instead of after-the-fact in the temporary + // store. Here, we choose not to add them because this will catch a + // bug where we mutate or delete an object that belongs to an immutable + // object (though it will show up somewhat opaquely as an authentication + // failure), whereas adding the immutable object to the roots will prevent + // us from catching this. + None + } + Owner::ObjectOwner(_parent) => { + unreachable!( + "Input objects must be address owned, shared, consensus, or immutable" + ) + } + } + }) + .filter(|id| { + // remove any non-mutable inputs. This will remove deleted or readonly shared + // objects + mutable_inputs.contains(id) + }) + .copied() + // Add any object IDs generated in the object runtime during execution to the + // authenticated set (i.e., new (non-package) objects, and possibly ephemeral UIDs). + .chain(self.generated_runtime_ids.iter().copied()) + .collect(); + + // Add sender and sponsor (if present) to authenticated set + authenticated_for_mutation.insert((*sender).into()); + if let Some(sponsor) = sponsor { + authenticated_for_mutation.insert((*sponsor).into()); + } + + // check all modified objects are authenticated + let mut objects_to_authenticate = self + .execution_results + .modified_objects + .iter() + .copied() + .collect::>(); + + while let Some(to_authenticate) = objects_to_authenticate.pop() { + if authenticated_for_mutation.contains(&to_authenticate) { + // object has already been authenticated + continue; + } + + let parent = if let Some(container_id) = + self.wrapped_object_containers.get(&to_authenticate) + { + // It's a wrapped object, so check that the container is authenticated + *container_id + } else { + // It's non-wrapped, so check the owner -- we can load the object from the + // store. + let Some(old_obj) = self.store.get_object(&to_authenticate) else { + panic!( + "Failed to load object {to_authenticate:?}.\n \ + If it cannot be loaded, we would expect it to be in the wrapped object map: {:#?}", + &self.wrapped_object_containers + ) + }; + + match &old_obj.owner { + // We mutated a dynamic field, we can continue to trace this back to verify + // proper ownership. + Owner::ObjectOwner(parent) => ObjectID::from(*parent), + // We mutated an address owned or sequenced address owned object -- one of two cases apply: + // 1) the object is owned by an object or address in the authenticated set, + // 2) the object is owned by some other address, in which case we should + // continue to trace this back. + Owner::AddressOwner(parent) + | Owner::ConsensusAddressOwner { owner: parent, .. } => { + // For Receiving<_> objects, the address owner is actually an object. + // If it was actually an address, we should have caught it as an input and + // it would already have been in authenticated_for_mutation + ObjectID::from(*parent) + } + // We mutated a shared object -- we checked if this object was in the + // authenticated set at the top of this loop and it wasn't so this is a failure. + owner @ Owner::Shared { .. } => { + panic!( + "Unauthenticated root at {to_authenticate:?} with owner {owner:?}\n\ + Potentially covering objects in: {authenticated_for_mutation:#?}" + ); + } + Owner::Immutable => { + assert!( + is_epoch_change, + "Immutable objects cannot be written, except for \ + Sui Framework/Move stdlib upgrades at epoch change boundaries" + ); + // Note: this assumes that the only immutable objects an epoch change + // tx can update are system packages, + // but in principle we could allow others. + assert!( + is_system_package(to_authenticate), + "Only system packages can be upgraded" + ); + continue; + } + } + }; + + // we now assume the object is authenticated and check the parent + authenticated_for_mutation.insert(to_authenticate); + objects_to_authenticate.push(parent); + } + Ok(()) + } +} + +impl TemporaryStore<'_> { + /// Track storage gas for each mutable input object (including the gas coin) + /// and each created object. Compute storage refunds for each deleted object. + /// Will *not* charge anything, gas status keeps track of storage cost and rebate. + /// All objects will be updated with their new (current) storage rebate/cost. + /// `SuiGasStatus` `storage_rebate` and `storage_gas_units` track the transaction + /// overall storage rebate and cost. + pub(crate) fn collect_storage_and_rebate(&mut self, gas_charger: &mut GasCharger) { + // Use two loops because we cannot mut iterate written while calling get_object_modified_at. + let old_storage_rebates: Vec<_> = self + .execution_results + .written_objects + .keys() + .map(|object_id| { + self.get_object_modified_at(object_id) + .map(|metadata| metadata.storage_rebate) + .unwrap_or_default() + }) + .collect(); + for (object, old_storage_rebate) in self + .execution_results + .written_objects + .values_mut() + .zip(old_storage_rebates) + { + // new object size + let new_object_size = object.object_size_for_gas_metering(); + // track changes and compute the new object `storage_rebate` + let new_storage_rebate = gas_charger.track_storage_mutation( + object.id(), + new_object_size, + old_storage_rebate, + ); + object.storage_rebate = new_storage_rebate; + } + + self.collect_rebate(gas_charger); + } + + pub(crate) fn collect_rebate(&self, gas_charger: &mut GasCharger) { + for object_id in &self.execution_results.modified_objects { + if self + .execution_results + .written_objects + .contains_key(object_id) + { + continue; + } + // get and track the deleted object `storage_rebate` + let storage_rebate = self + .get_object_modified_at(object_id) + // Unwrap is safe because this loop iterates through all modified objects. + .unwrap() + .storage_rebate; + gas_charger.track_storage_mutation(*object_id, 0, storage_rebate); + } + } + + pub fn check_execution_results_consistency(&self) -> Result<(), ExecutionError> { + assert_invariant!( + self.execution_results + .created_object_ids + .iter() + .all(|id| !self.execution_results.deleted_object_ids.contains(id) + && !self.execution_results.modified_objects.contains(id)), + "Created object IDs cannot also be deleted or modified" + ); + assert_invariant!( + self.execution_results.modified_objects.iter().all(|id| { + self.mutable_input_refs.contains_key(id) + || self.loaded_runtime_objects.contains_key(id) + || is_system_package(*id) + }), + "A modified object must be either a mutable input, a loaded child object, or a system package" + ); + Ok(()) + } +} +//============================================================================== +// Charge gas current - end +//============================================================================== + +impl TemporaryStore<'_> { + pub fn advance_epoch_safe_mode( + &mut self, + params: &AdvanceEpochParams, + protocol_config: &ProtocolConfig, + ) { + let wrapper = get_sui_system_state_wrapper(self.store.as_object_store()) + .expect("System state wrapper object must exist"); + let (old_object, new_object) = + wrapper.advance_epoch_safe_mode(params, self.store.as_object_store(), protocol_config); + self.mutate_child_object(old_object, new_object); + } +} + +type ModifiedObjectInfo<'a> = ( + ObjectID, + // old object metadata, including version, digest, owner, and storage rebate. + Option, + Option<&'a Object>, +); + +impl TemporaryStore<'_> { + fn get_input_sui( + &self, + id: &ObjectID, + expected_version: SequenceNumber, + layout_resolver: &mut impl LayoutResolver, + ) -> Result { + if let Some(obj) = self.input_objects.get(id) { + // the assumption here is that if it is in the input objects must be the right one + if obj.version() != expected_version { + invariant_violation!( + "Version mismatching when resolving input object to check conservation--\ + expected {}, got {}", + expected_version, + obj.version(), + ); + } + obj.get_total_sui(layout_resolver).map_err(|e| { + make_invariant_violation!( + "Failed looking up input SUI in SUI conservation checking for input with \ + type {:?}: {e:#?}", + obj.struct_tag(), + ) + }) + } else { + // not in input objects, must be a dynamic field + let Some(obj) = self.store.get_object_by_key(id, expected_version) else { + invariant_violation!( + "Failed looking up dynamic field {id} in SUI conservation checking" + ); + }; + obj.get_total_sui(layout_resolver).map_err(|e| { + make_invariant_violation!( + "Failed looking up input SUI in SUI conservation checking for type \ + {:?}: {e:#?}", + obj.struct_tag(), + ) + }) + } + } + + /// Return the list of all modified objects, for each object, returns + /// - Object ID, + /// - Input: If the object existed prior to this transaction, include their version and storage_rebate, + /// - Output: If a new version of the object is written, include the new object. + fn get_modified_objects(&self) -> Vec> { + self.execution_results + .modified_objects + .iter() + .map(|id| { + let metadata = self.get_object_modified_at(id); + let output = self.execution_results.written_objects.get(id); + (*id, metadata, output) + }) + .chain( + self.execution_results + .written_objects + .iter() + .filter_map(|(id, object)| { + if self.execution_results.modified_objects.contains(id) { + None + } else { + Some((*id, None, Some(object))) + } + }), + ) + .collect() + } + + /// Check that this transaction neither creates nor destroys SUI. This should hold for all txes + /// except the epoch change tx, which mints staking rewards equal to the gas fees burned in the + /// previous epoch. Specifically, this checks two key invariants about storage + /// fees and storage rebate: + /// + /// 1. all SUI in storage rebate fields of input objects should flow either to the transaction + /// storage rebate, or the transaction non-refundable storage rebate + /// 2. all SUI charged for storage should flow into the storage rebate field of some output + /// object + /// + /// This function is intended to be called *after* we have charged for + /// gas + applied the storage rebate to the gas object, but *before* we + /// have updated object versions. + pub fn check_sui_conserved( + &self, + simple_conservation_checks: bool, + gas_summary: &GasCostSummary, + ) -> Result<(), ExecutionError> { + if !simple_conservation_checks { + return Ok(()); + } + // total amount of SUI in storage rebate of input objects + let mut total_input_rebate = 0; + // total amount of SUI in storage rebate of output objects + let mut total_output_rebate = 0; + for (_, input, output) in self.get_modified_objects() { + if let Some(input) = input { + total_input_rebate += input.storage_rebate; + } + if let Some(object) = output { + total_output_rebate += object.storage_rebate; + } + } + + if gas_summary.storage_cost == 0 { + // this condition is usually true when the transaction went OOG and no + // gas is left for storage charges. + // The storage cost has to be there at least for the gas coin which + // will not be deleted even when going to 0. + // However if the storage cost is 0 and if there is any object touched + // or deleted the value in input must be equal to the output plus rebate and + // non refundable. + // Rebate and non refundable will be positive when there are object deleted + // (gas smashing being the primary and possibly only example). + // A more typical condition is for all storage charges in summary to be 0 and + // then input and output must be the same value + if total_input_rebate + != total_output_rebate + + gas_summary.storage_rebate + + gas_summary.non_refundable_storage_fee + { + return Err(ExecutionError::invariant_violation(format!( + "SUI conservation failed -- no storage charges in gas summary \ + and total storage input rebate {} not equal \ + to total storage output rebate {}", + total_input_rebate, total_output_rebate, + ))); + } + } else { + // all SUI in storage rebate fields of input objects should flow either to + // the transaction storage rebate, or the non-refundable storage rebate pool + if total_input_rebate + != gas_summary.storage_rebate + gas_summary.non_refundable_storage_fee + { + return Err(ExecutionError::invariant_violation(format!( + "SUI conservation failed -- {} SUI in storage rebate field of input objects, \ + {} SUI in tx storage rebate or tx non-refundable storage rebate", + total_input_rebate, gas_summary.non_refundable_storage_fee, + ))); + } + + // all SUI charged for storage should flow into the storage rebate field + // of some output object + if gas_summary.storage_cost != total_output_rebate { + return Err(ExecutionError::invariant_violation(format!( + "SUI conservation failed -- {} SUI charged for storage, \ + {} SUI in storage rebate field of output objects", + gas_summary.storage_cost, total_output_rebate + ))); + } + } + Ok(()) + } + + /// Check that this transaction neither creates nor destroys SUI. + /// This more expensive check will check a third invariant on top of the 2 performed + /// by `check_sui_conserved` above: + /// + /// * all SUI in input objects (including coins etc in the Move part of an object) should flow + /// either to an output object, or be burned as part of computation fees or non-refundable + /// storage rebate + /// + /// This function is intended to be called *after* we have charged for gas + applied the + /// storage rebate to the gas object, but *before* we have updated object versions. The + /// advance epoch transaction would mint `epoch_fees` amount of SUI, and burn `epoch_rebates` + /// amount of SUI. We need these information for this check. + pub fn check_sui_conserved_expensive( + &self, + gas_summary: &GasCostSummary, + advance_epoch_gas_summary: Option<(u64, u64)>, + layout_resolver: &mut impl LayoutResolver, + ) -> Result<(), ExecutionError> { + // total amount of SUI in input objects, including both coins and storage rebates + let mut total_input_sui = 0; + // total amount of SUI in output objects, including both coins and storage rebates + let mut total_output_sui = 0; + + // settlement input/output sui is used by the settlement transactions to account for + // Sui that has been gathered from the accumulator writes of transactions which it is + // settling. + total_input_sui += self.execution_results.settlement_input_sui; + total_output_sui += self.execution_results.settlement_output_sui; + + for (id, input, output) in self.get_modified_objects() { + if let Some(input) = input { + total_input_sui += self.get_input_sui(&id, input.version, layout_resolver)?; + } + if let Some(object) = output { + total_output_sui += object.get_total_sui(layout_resolver).map_err(|e| { + make_invariant_violation!( + "Failed looking up output SUI in SUI conservation checking for \ + mutated type {:?}: {e:#?}", + object.struct_tag(), + ) + })?; + } + } + + for event in &self.execution_results.accumulator_events { + let (input, output) = event.total_sui_in_event(); + total_input_sui += input; + total_output_sui += output; + } + + // note: storage_cost flows into the storage_rebate field of the output objects, which is + // why it is not accounted for here. + // similarly, all of the storage_rebate *except* the storage_fund_rebate_inflow + // gets credited to the gas coin both computation costs and storage rebate inflow are + total_output_sui += gas_summary.computation_cost + gas_summary.non_refundable_storage_fee; + if let Some((epoch_fees, epoch_rebates)) = advance_epoch_gas_summary { + total_input_sui += epoch_fees; + total_output_sui += epoch_rebates; + } + if total_input_sui != total_output_sui { + return Err(ExecutionError::invariant_violation(format!( + "SUI conservation failed: input={}, output={}, \ + this transaction either mints or burns SUI", + total_input_sui, total_output_sui, + ))); + } + Ok(()) + } +} + +impl ChildObjectResolver for TemporaryStore<'_> { + fn read_child_object( + &self, + parent: &ObjectID, + child: &ObjectID, + child_version_upper_bound: SequenceNumber, + ) -> SuiResult> { + let obj_opt = self.execution_results.written_objects.get(child); + if obj_opt.is_some() { + Ok(obj_opt.cloned()) + } else { + let _scope = monitored_scope("Execution::read_child_object"); + self.store + .read_child_object(parent, child, child_version_upper_bound) + } + } + + fn get_object_received_at_version( + &self, + owner: &ObjectID, + receiving_object_id: &ObjectID, + receive_object_at_version: SequenceNumber, + epoch_id: EpochId, + ) -> SuiResult> { + // You should never be able to try and receive an object after deleting it or writing it in the same + // transaction since `Receiving` doesn't have copy. + debug_assert!( + !self + .execution_results + .written_objects + .contains_key(receiving_object_id) + ); + debug_assert!( + !self + .execution_results + .deleted_object_ids + .contains(receiving_object_id) + ); + self.store.get_object_received_at_version( + owner, + receiving_object_id, + receive_object_at_version, + epoch_id, + ) + } +} + +/// Compares the owner and payload of an object. +/// This is used to detect illegal writes to non-exclusive write objects. +fn was_object_mutated(object: &Object, original: &Object) -> bool { + let data_equal = match (&object.data, &original.data) { + (Data::Move(a), Data::Move(b)) => a.contents_and_type_equal(b), + // We don't have a use for package content-equality, so we remain as strict as + // possible for now. + (Data::Package(a), Data::Package(b)) => a == b, + _ => false, + }; + + let owner_equal = match (&object.owner, &original.owner) { + // We don't compare initial shared versions, because re-shared objects do not have the + // correct initial shared version at this point in time, and this field is not something + // that can be modified by a single transaction anyway. + (Owner::Shared { .. }, Owner::Shared { .. }) => true, + ( + Owner::ConsensusAddressOwner { owner: a, .. }, + Owner::ConsensusAddressOwner { owner: b, .. }, + ) => a == b, + (Owner::AddressOwner(a), Owner::AddressOwner(b)) => a == b, + (Owner::Immutable, Owner::Immutable) => true, + (Owner::ObjectOwner(a), Owner::ObjectOwner(b)) => a == b, + + // Keep the left hand side of the match exhaustive to catch future + // changes to Owner + (Owner::AddressOwner(_), _) + | (Owner::Immutable, _) + | (Owner::ObjectOwner(_), _) + | (Owner::Shared { .. }, _) + | (Owner::ConsensusAddressOwner { .. }, _) => false, + }; + + !data_equal || !owner_equal +} + +impl Storage for TemporaryStore<'_> { + fn reset(&mut self) { + self.drop_writes(); + } + + fn read_object(&self, id: &ObjectID) -> Option<&Object> { + TemporaryStore::read_object(self, id) + } + + /// Take execution results v2, and translate it back to be compatible with effects v1. + fn record_execution_results( + &mut self, + results: ExecutionResults, + ) -> Result<(), ExecutionError> { + let ExecutionResults::V2(mut results) = results else { + panic!("ExecutionResults::V2 expected in sui-execution v1 and above"); + }; + + // for all non-exclusive write inputs, remove them from written objects + let mut to_remove = Vec::new(); + for (id, original) in &self.non_exclusive_input_original_versions { + // Object must be present in `written_objects` and identical + if results + .written_objects + .get(id) + .map(|obj| was_object_mutated(obj, original)) + .unwrap_or(true) + { + return Err(ExecutionError::new_with_source( + ExecutionErrorKind::NonExclusiveWriteInputObjectModified { id: *id }, + "Non-exclusive write input object has been modified or deleted", + )); + } + to_remove.push(*id); + } + + for id in to_remove { + results.written_objects.remove(&id); + results.modified_objects.remove(&id); + } + + // It's important to merge instead of override results because it's + // possible to execute PT more than once during tx execution. + self.execution_results.merge_results(results); + + Ok(()) + } + + fn save_loaded_runtime_objects( + &mut self, + loaded_runtime_objects: BTreeMap, + ) { + TemporaryStore::save_loaded_runtime_objects(self, loaded_runtime_objects) + } + + fn save_wrapped_object_containers( + &mut self, + wrapped_object_containers: BTreeMap, + ) { + TemporaryStore::save_wrapped_object_containers(self, wrapped_object_containers) + } + + fn check_coin_deny_list( + &self, + receiving_funds_type_and_owners: BTreeMap>, + ) -> DenyListResult { + let result = check_coin_deny_list_v2_during_execution( + receiving_funds_type_and_owners, + self.cur_epoch, + self.store.as_object_store(), + ); + // The denylist object is only loaded if there are regulated transfers. + // And also if we already have it in the input there is no need to commit it again in the effects. + if result.num_non_gas_coin_owners > 0 + && !self.input_objects.contains_key(&SUI_DENY_LIST_OBJECT_ID) + { + self.loaded_per_epoch_config_objects + .write() + .insert(SUI_DENY_LIST_OBJECT_ID); + } + result + } + + fn record_generated_object_ids(&mut self, generated_ids: BTreeSet) { + TemporaryStore::save_generated_object_ids(self, generated_ids) + } +} + +impl BackingPackageStore for TemporaryStore<'_> { + fn get_package_object(&self, package_id: &ObjectID) -> SuiResult> { + // We first check the objects in the temporary store because in non-production code path, + // it is possible to read packages that are just written in the same transaction. + // This can happen for example when we run the expensive conservation checks, where we may + // look into the types of each written object in the output, and some of them need the + // newly written packages for type checking. + // In production path though, this should never happen. + if let Some(obj) = self.execution_results.written_objects.get(package_id) { + Ok(Some(PackageObject::new(obj.clone()))) + } else { + self.store.get_package_object(package_id).inspect(|obj| { + // Track object but leave unchanged + if let Some(v) = obj + && !self + .runtime_packages_loaded_from_db + .read() + .contains_key(package_id) + { + // TODO: Can this lock ever block execution? + // TODO: Another way to avoid the cost of maintaining this map is to not + // enable it in normal runs, and if a fork is detected, rerun it with a flag + // turned on and start populating this field. + self.runtime_packages_loaded_from_db + .write() + .insert(*package_id, v.clone()); + } + }) + } + } +} + +impl ParentSync for TemporaryStore<'_> { + fn get_latest_parent_entry_ref_deprecated(&self, _object_id: ObjectID) -> Option { + unreachable!("Never called in newer protocol versions") + } +} diff --git a/sui-execution/replay_cut/sui-adapter/src/type_layout_resolver.rs b/sui-execution/replay_cut/sui-adapter/src/type_layout_resolver.rs new file mode 100644 index 0000000000000..7cf00816d814f --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/type_layout_resolver.rs @@ -0,0 +1,64 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::data_store::cached_package_store::CachedPackageStore; +use crate::data_store::legacy::linkage_view::LinkageView; +use crate::programmable_transactions::context::load_type_from_struct; +use move_core_types::annotated_value as A; +use move_core_types::language_storage::StructTag; +use move_vm_runtime::move_vm::MoveVM; +use sui_types::base_types::ObjectID; +use sui_types::error::{SuiErrorKind, SuiResult}; +use sui_types::execution::TypeLayoutStore; +use sui_types::storage::{BackingPackageStore, PackageObject}; +use sui_types::{error::SuiError, layout_resolver::LayoutResolver}; + +/// Retrieve a `MoveStructLayout` from a `Type`. +/// Invocation into the `Session` to leverage the `LinkageView` implementation +/// common to the runtime. +pub struct TypeLayoutResolver<'state, 'vm> { + vm: &'vm MoveVM, + linkage_view: LinkageView<'state>, +} + +/// Implements SuiResolver traits by providing null implementations for module and resource +/// resolution and delegating backing package resolution to the trait object. +struct NullSuiResolver<'state>(Box); + +impl<'state, 'vm> TypeLayoutResolver<'state, 'vm> { + pub fn new(vm: &'vm MoveVM, state_view: Box) -> Self { + let linkage_view = LinkageView::new(Box::new(CachedPackageStore::new(Box::new( + NullSuiResolver(state_view), + )))); + Self { vm, linkage_view } + } +} + +impl LayoutResolver for TypeLayoutResolver<'_, '_> { + fn get_annotated_layout( + &mut self, + struct_tag: &StructTag, + ) -> Result { + let Ok(ty) = load_type_from_struct(self.vm, &self.linkage_view, &[], struct_tag) else { + return Err(SuiErrorKind::FailObjectLayout { + st: format!("{}", struct_tag), + } + .into()); + }; + let layout = self.vm.get_runtime().type_to_fully_annotated_layout(&ty); + match layout { + Ok(A::MoveTypeLayout::Struct(s)) => Ok(A::MoveDatatypeLayout::Struct(s)), + Ok(A::MoveTypeLayout::Enum(e)) => Ok(A::MoveDatatypeLayout::Enum(e)), + _ => Err(SuiErrorKind::FailObjectLayout { + st: format!("{}", struct_tag), + } + .into()), + } + } +} + +impl BackingPackageStore for NullSuiResolver<'_> { + fn get_package_object(&self, package_id: &ObjectID) -> SuiResult> { + self.0.get_package_object(package_id) + } +} diff --git a/sui-execution/replay_cut/sui-adapter/src/type_resolver.rs b/sui-execution/replay_cut/sui-adapter/src/type_resolver.rs new file mode 100644 index 0000000000000..c9227aa01b382 --- /dev/null +++ b/sui-execution/replay_cut/sui-adapter/src/type_resolver.rs @@ -0,0 +1,10 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use move_core_types::language_storage::TypeTag; +use move_vm_types::loaded_data::runtime_types::Type; +use sui_types::error::ExecutionError; + +pub trait TypeTagResolver { + fn get_type_tag(&self, type_: &Type) -> Result; +} diff --git a/sui-execution/replay_cut/sui-move-natives/Cargo.toml b/sui-execution/replay_cut/sui-move-natives/Cargo.toml new file mode 100644 index 0000000000000..da16b0bf4050f --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "sui-move-natives-replay_cut" +version = "0.1.0" +edition = "2024" +authors = ["Mysten Labs "] +description = "Move framework for sui platform" +license = "Apache-2.0" +publish = false + +[lints] +workspace = true + +[dependencies] +better_any.workspace = true +bcs.workspace = true +indexmap.workspace = true +smallvec.workspace = true +rand = { workspace = true, features = ["small_rng"] } + +fastcrypto-zkp.workspace = true +fastcrypto-vdf.workspace = true +fastcrypto.workspace = true +move-binary-format.workspace = true +move-core-types.workspace = true + +move-stdlib-natives = { path = "../../../external-crates/move/move-execution/replay_cut/crates/move-stdlib-natives", package = "move-stdlib-natives-replay_cut" } +move-vm-runtime = { path = "../../../external-crates/move/move-execution/replay_cut/crates/move-vm-runtime", package = "move-vm-runtime-replay_cut" } +move-vm-types = { path = "../../../external-crates/move/move-execution/replay_cut/crates/move-vm-types", package = "move-vm-types-replay_cut" } + +sui-protocol-config.workspace = true +sui-types.workspace = true +tracing.workspace = true diff --git a/sui-execution/replay_cut/sui-move-natives/src/accumulator.rs b/sui-execution/replay_cut/sui-move-natives/src/accumulator.rs new file mode 100644 index 0000000000000..52704e083ea54 --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/accumulator.rs @@ -0,0 +1,94 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + NativesCostTable, get_extension, get_extension_mut, + object_runtime::{MoveAccumulatorAction, MoveAccumulatorValue, ObjectRuntime}, +}; +use move_binary_format::errors::PartialVMResult; +use move_core_types::account_address::AccountAddress; +use move_vm_runtime::{native_charge_gas_early_exit, native_functions::NativeContext}; +use move_vm_types::{ + loaded_data::runtime_types::Type, natives::function::NativeResult, values::Value, +}; +use smallvec::smallvec; +use std::collections::VecDeque; +use sui_types::base_types::ObjectID; + +pub fn emit_deposit_event( + context: &mut NativeContext, + ty_args: Vec, + args: VecDeque, +) -> PartialVMResult { + emit_event(context, ty_args, args, MoveAccumulatorAction::Merge) +} + +pub fn emit_withdraw_event( + context: &mut NativeContext, + ty_args: Vec, + args: VecDeque, +) -> PartialVMResult { + emit_event(context, ty_args, args, MoveAccumulatorAction::Split) +} + +fn emit_event( + context: &mut NativeContext, + mut ty_args: Vec, + mut args: VecDeque, + action: MoveAccumulatorAction, +) -> PartialVMResult { + debug_assert!(ty_args.len() == 1); + debug_assert!(args.len() == 3); + + let event_emit_cost_params = get_extension!(context, NativesCostTable)? + .event_emit_cost_params + .clone(); + + // TODO(address-balances): add specific cost for this + native_charge_gas_early_exit!(context, event_emit_cost_params.event_emit_cost_base); + + let amount = args.pop_back().unwrap().value_as::().unwrap(); + let recipient = args + .pop_back() + .unwrap() + .value_as::() + .unwrap(); + let accumulator: ObjectID = args + .pop_back() + .unwrap() + .value_as::() + .unwrap() + .into(); + + let ty_tag = context.type_to_type_tag(&ty_args.pop().unwrap())?; + + let obj_runtime: &mut ObjectRuntime = get_extension_mut!(context)?; + + obj_runtime.emit_accumulator_event( + accumulator, + action, + recipient, + ty_tag, + MoveAccumulatorValue::U64(amount), + )?; + + Ok(NativeResult::ok(context.gas_used(), smallvec![])) +} + +pub fn record_settlement_sui_conservation( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 2); + + let output_sui = args.pop_back().unwrap().value_as::().unwrap(); + let input_sui = args.pop_back().unwrap().value_as::().unwrap(); + + let obj_runtime: &mut ObjectRuntime = context.extensions_mut().get_mut()?; + + obj_runtime.record_settlement_sui_conservation(input_sui, output_sui); + + Ok(NativeResult::ok(context.gas_used(), smallvec![])) +} diff --git a/sui-execution/replay_cut/sui-move-natives/src/address.rs b/sui-execution/replay_cut/sui-move-natives/src/address.rs new file mode 100644 index 0000000000000..de3f61b5f800c --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/address.rs @@ -0,0 +1,124 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::{NativesCostTable, get_extension}; +use move_binary_format::errors::PartialVMResult; +use move_core_types::{account_address::AccountAddress, gas_algebra::InternalGas, u256::U256}; +use move_vm_runtime::{native_charge_gas_early_exit, native_functions::NativeContext}; +use move_vm_types::{ + loaded_data::runtime_types::Type, natives::function::NativeResult, pop_arg, values::Value, +}; +use smallvec::smallvec; +use std::collections::VecDeque; + +const E_ADDRESS_PARSE_ERROR: u64 = 0; +#[derive(Clone)] +pub struct AddressFromBytesCostParams { + /// addresses are constant size, so base cost suffices + pub address_from_bytes_cost_base: InternalGas, +} +/*************************************************************************************************** + * native fun from_bytes + * Implementation of the Move native function `address::from_bytes(bytes: vector)` + * gas cost: address_from_bytes_cost_base | addresses are constant size, so base cost suffices + **************************************************************************************************/ +pub fn from_bytes( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 1); + + let address_from_bytes_cost_params = get_extension!(context, NativesCostTable)? + .address_from_bytes_cost_params + .clone(); + + // Charge base fee + native_charge_gas_early_exit!( + context, + address_from_bytes_cost_params.address_from_bytes_cost_base + ); + + let addr_bytes = pop_arg!(args, Vec); + let cost = context.gas_used(); + + // Address parsing can fail if fed the incorrect number of bytes. + Ok(match AccountAddress::from_bytes(addr_bytes) { + Ok(addr) => NativeResult::ok(cost, smallvec![Value::address(addr)]), + Err(_) => NativeResult::err(cost, E_ADDRESS_PARSE_ERROR), + }) +} +#[derive(Clone)] +pub struct AddressToU256CostParams { + /// addresses and u256 are constant size, so base cost suffices + pub address_to_u256_cost_base: InternalGas, +} +/*************************************************************************************************** + * native fun to_u256 + * Implementation of the Move native function `address::to_u256(address): u256` + * gas cost: address_to_u256_cost_base | addresses and u256 are constant size, so base cost suffices + **************************************************************************************************/ +pub fn to_u256( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 1); + + let address_to_u256_cost_params = get_extension!(context, NativesCostTable)? + .address_to_u256_cost_params + .clone(); + + // Charge flat cost + native_charge_gas_early_exit!( + context, + address_to_u256_cost_params.address_to_u256_cost_base + ); + + let addr = pop_arg!(args, AccountAddress); + let mut addr_bytes_le = addr.to_vec(); + addr_bytes_le.reverse(); + + // unwrap safe because we know addr_bytes_le is length 32 + let u256_val = Value::u256(U256::from_le_bytes(&addr_bytes_le.try_into().unwrap())); + Ok(NativeResult::ok(context.gas_used(), smallvec![u256_val])) +} + +#[derive(Clone)] +pub struct AddressFromU256CostParams { + /// addresses and u256 are constant size, so base cost suffices + pub address_from_u256_cost_base: InternalGas, +} +/*************************************************************************************************** + * native fun from_u256 + * Implementation of the Move native function `address::from_u256(u256): address` + * gas cost: address_from_u256_cost_base | addresses and u256 are constant size, so base cost suffices + **************************************************************************************************/ +pub fn from_u256( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 1); + + let address_from_u256_cost_params = get_extension!(context, NativesCostTable)? + .address_from_u256_cost_params + .clone(); + + // charge flat fee + native_charge_gas_early_exit!( + context, + address_from_u256_cost_params.address_from_u256_cost_base + ); + + let u256 = pop_arg!(args, U256); + let mut u256_bytes = u256.to_le_bytes().to_vec(); + u256_bytes.reverse(); + + // unwrap safe because we are passing a 32 byte slice + let addr_val = Value::address(AccountAddress::from_bytes(&u256_bytes[..]).unwrap()); + Ok(NativeResult::ok(context.gas_used(), smallvec![addr_val])) +} diff --git a/sui-execution/replay_cut/sui-move-natives/src/config.rs b/sui-execution/replay_cut/sui-move-natives/src/config.rs new file mode 100644 index 0000000000000..30225fb5dda81 --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/config.rs @@ -0,0 +1,189 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + NativesCostTable, abstract_size, get_extension, get_extension_mut, + object_runtime::ObjectRuntime, +}; +use move_binary_format::errors::{PartialVMError, PartialVMResult}; +use move_core_types::{ + account_address::AccountAddress, gas_algebra::InternalGas, language_storage::StructTag, + runtime_value as R, vm_status::StatusCode, +}; +use move_vm_runtime::native_charge_gas_early_exit; +use move_vm_runtime::native_functions::NativeContext; +use move_vm_types::{ + loaded_data::runtime_types::Type, + natives::function::NativeResult, + pop_arg, + values::{Struct, Value, Vector}, +}; +use smallvec::smallvec; +use std::collections::VecDeque; +use sui_types::{TypeTag, base_types::MoveObjectType}; +use tracing::{error, instrument}; + +const E_BCS_SERIALIZATION_FAILURE: u64 = 2; + +#[derive(Clone)] +pub struct ConfigReadSettingImplCostParams { + pub config_read_setting_impl_cost_base: Option, + pub config_read_setting_impl_cost_per_byte: Option, +} + +#[instrument(level = "trace", skip_all)] +pub fn read_setting_impl( + context: &mut NativeContext, + mut ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + assert_eq!(ty_args.len(), 4); + assert_eq!(args.len(), 3); + + let ConfigReadSettingImplCostParams { + config_read_setting_impl_cost_base, + config_read_setting_impl_cost_per_byte, + } = get_extension!(context, NativesCostTable)? + .config_read_setting_impl_cost_params + .clone(); + + let config_read_setting_impl_cost_base = + config_read_setting_impl_cost_base.ok_or_else(|| { + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("gas cost is not set".to_string()) + })?; + let config_read_setting_impl_cost_per_byte = config_read_setting_impl_cost_per_byte + .ok_or_else(|| { + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("gas cost is not set".to_string()) + })?; + // Charge base fee + native_charge_gas_early_exit!(context, config_read_setting_impl_cost_base); + + let value_ty = ty_args.pop().unwrap(); + let setting_data_value_ty = ty_args.pop().unwrap(); + let setting_value_ty = ty_args.pop().unwrap(); + let field_setting_ty = ty_args.pop().unwrap(); + + let current_epoch = pop_arg!(args, u64); + let name_df_addr = pop_arg!(args, AccountAddress); + let config_addr = pop_arg!(args, AccountAddress); + + let field_setting_tag: StructTag = match context.type_to_type_tag(&field_setting_ty)? { + TypeTag::Struct(s) => *s, + _ => { + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("Sui verifier guarantees this is a struct".to_string()), + ); + } + }; + let Some(field_setting_layout) = context.type_to_type_layout(&field_setting_ty)? else { + return Ok(NativeResult::err( + context.gas_used(), + E_BCS_SERIALIZATION_FAILURE, + )); + }; + let object_runtime: &mut ObjectRuntime = get_extension_mut!(context)?; + + let read_value_opt = consistent_value_before_current_epoch( + object_runtime, + field_setting_tag, + &field_setting_layout, + &setting_value_ty, + &setting_data_value_ty, + &value_ty, + config_addr, + name_df_addr, + current_epoch, + )?; + + let size = abstract_size(object_runtime.protocol_config, &read_value_opt); + + native_charge_gas_early_exit!( + context, + config_read_setting_impl_cost_per_byte * u64::from(size).into() + ); + + Ok(NativeResult::ok( + context.gas_used(), + smallvec![read_value_opt], + )) +} + +fn consistent_value_before_current_epoch( + object_runtime: &mut ObjectRuntime, + field_setting_tag: StructTag, + field_setting_layout: &R::MoveTypeLayout, + _setting_value_ty: &Type, + setting_data_value_ty: &Type, + value_ty: &Type, + config_addr: AccountAddress, + name_df_addr: AccountAddress, + current_epoch: u64, +) -> PartialVMResult { + let field_setting_obj_ty = MoveObjectType::from(field_setting_tag); + let Some(field) = object_runtime.config_setting_unsequenced_read( + config_addr.into(), + name_df_addr.into(), + field_setting_layout, + &field_setting_obj_ty, + ) else { + return option_none(value_ty); + }; + + let [_id, _name, setting]: [Value; 3] = unpack_struct(field)?; + let [data_opt]: [Value; 1] = unpack_struct(setting)?; + let data = match unpack_option(data_opt, setting_data_value_ty)? { + None => { + error!( + " + SettingData is none. + config_addr: {config_addr}, + name_df_addr: {name_df_addr}, + field_setting_obj_ty: {field_setting_obj_ty:?}", + ); + return option_none(value_ty); + } + Some(data) => data, + }; + let [newer_value_epoch, newer_value, older_value_opt]: [Value; 3] = unpack_struct(data)?; + let newer_value_epoch: u64 = newer_value_epoch.value_as()?; + debug_assert!( + unpack_option(newer_value.copy_value()?, value_ty)?.is_some() + || unpack_option(older_value_opt.copy_value()?, value_ty)?.is_some() + ); + Ok(if current_epoch > newer_value_epoch { + newer_value + } else { + older_value_opt + }) +} + +fn unpack_struct(s: Value) -> PartialVMResult<[Value; N]> { + let s: Struct = s.value_as()?; + s.unpack()?.collect::>().try_into().map_err(|e| { + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message(format!("struct expected to have {N} fields: {e:?}")) + }) +} + +fn unpack_option(option: Value, type_param: &Type) -> PartialVMResult> { + let [vec_value]: [Value; 1] = unpack_struct(option)?; + let vec: Vector = vec_value.value_as()?; + Ok(if vec.elem_len() == 0 { + None + } else { + let [elem]: [Value; 1] = vec.unpack(type_param, 1)?.try_into().map_err(|e| { + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message(format!("vector expected to have one element: {e:?}")) + })?; + Some(elem) + }) +} + +fn option_none(type_param: &Type) -> PartialVMResult { + Ok(Value::struct_(Struct::pack(vec![Vector::empty( + type_param.try_into()?, + )?]))) +} diff --git a/sui-execution/replay_cut/sui-move-natives/src/crypto/bls12381.rs b/sui-execution/replay_cut/sui-move-natives/src/crypto/bls12381.rs new file mode 100644 index 0000000000000..8c18fa83e362d --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/crypto/bls12381.rs @@ -0,0 +1,178 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 +use fastcrypto::{ + bls12381::{min_pk, min_sig}, + traits::{ToFromBytes, VerifyingKey}, +}; +use move_binary_format::errors::PartialVMResult; +use move_core_types::gas_algebra::InternalGas; +use move_vm_runtime::{native_charge_gas_early_exit, native_functions::NativeContext}; +use move_vm_types::{ + loaded_data::runtime_types::Type, + natives::function::NativeResult, + pop_arg, + values::{Value, VectorRef}, +}; +use smallvec::smallvec; +use std::collections::VecDeque; + +use crate::{NativesCostTable, get_extension}; + +const BLS12381_BLOCK_SIZE: usize = 64; + +#[derive(Clone)] +pub struct Bls12381Bls12381MinSigVerifyCostParams { + /// Base cost for invoking the `bls12381_min_sig_verify` function + pub bls12381_bls12381_min_sig_verify_cost_base: InternalGas, + /// Cost per byte of `msg` + pub bls12381_bls12381_min_sig_verify_msg_cost_per_byte: InternalGas, + /// Cost per block of `msg`, where a block is 64 bytes + pub bls12381_bls12381_min_sig_verify_msg_cost_per_block: InternalGas, +} +/*************************************************************************************************** + * native fun bls12381_min_sig_verify + * Implementation of the Move native function `bls12381_min_sig_verify(signature: &vector, public_key: &vector, msg: &vector): bool` + * gas cost: bls12381_bls12381_min_sig_verify_cost_base | covers various fixed costs in the oper + * + bls12381_bls12381_min_sig_verify_msg_cost_per_byte * size_of(msg) | covers cost of operating on each byte of `msg` + * + bls12381_bls12381_min_sig_verify_msg_cost_per_block * num_blocks(msg) | covers cost of operating on each block in `msg` + * Note: each block is of size `BLS12381_BLOCK_SIZE` bytes, and we round up. + * `signature` and `public_key` are fixed size, so their costs are included in the base cost. + **************************************************************************************************/ +pub fn bls12381_min_sig_verify( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 3); + + // Load the cost parameters from the protocol config + let bls12381_bls12381_min_sig_verify_cost_params = get_extension!(context, NativesCostTable)? + .bls12381_bls12381_min_sig_verify_cost_params + .clone(); + // Charge the base cost for this oper + native_charge_gas_early_exit!( + context, + bls12381_bls12381_min_sig_verify_cost_params.bls12381_bls12381_min_sig_verify_cost_base + ); + + let msg = pop_arg!(args, VectorRef); + let public_key_bytes = pop_arg!(args, VectorRef); + let signature_bytes = pop_arg!(args, VectorRef); + + let msg_ref = msg.as_bytes_ref(); + let public_key_bytes_ref = public_key_bytes.as_bytes_ref(); + let signature_bytes_ref = signature_bytes.as_bytes_ref(); + + // Charge the arg size dependent costs + native_charge_gas_early_exit!( + context, + bls12381_bls12381_min_sig_verify_cost_params + .bls12381_bls12381_min_sig_verify_msg_cost_per_byte + * (msg_ref.len() as u64).into() + + bls12381_bls12381_min_sig_verify_cost_params + .bls12381_bls12381_min_sig_verify_msg_cost_per_block + * (msg_ref.len().div_ceil(BLS12381_BLOCK_SIZE) as u64).into() + ); + + let cost = context.gas_used(); + + let Ok(signature) = + ::from_bytes(&signature_bytes_ref) + else { + return Ok(NativeResult::ok(cost, smallvec![Value::bool(false)])); + }; + + let public_key = + match ::from_bytes(&public_key_bytes_ref) { + Ok(public_key) => match public_key.validate() { + Ok(_) => public_key, + Err(_) => return Ok(NativeResult::ok(cost, smallvec![Value::bool(false)])), + }, + Err(_) => return Ok(NativeResult::ok(cost, smallvec![Value::bool(false)])), + }; + + Ok(NativeResult::ok( + cost, + smallvec![Value::bool(public_key.verify(&msg_ref, &signature).is_ok())], + )) +} + +#[derive(Clone)] +pub struct Bls12381Bls12381MinPkVerifyCostParams { + /// Base cost for invoking the `bls12381_min_sig_verify` function + pub bls12381_bls12381_min_pk_verify_cost_base: InternalGas, + /// Cost per byte of `msg` + pub bls12381_bls12381_min_pk_verify_msg_cost_per_byte: InternalGas, + /// Cost per block of `msg`, where a block is 64 bytes + pub bls12381_bls12381_min_pk_verify_msg_cost_per_block: InternalGas, +} +/*************************************************************************************************** + * native fun bls12381_min_pk_verify + * Implementation of the Move native function `bls12381_min_pk_verify(signature: &vector, public_key: &vector, msg: &vector): bool` + * gas cost: bls12381_bls12381_min_pk_verify_cost_base | covers various fixed costs in the oper + * + bls12381_bls12381_min_pk_verify_msg_cost_per_byte * size_of(msg) | covers cost of operating on each byte of `msg` + * + bls12381_bls12381_min_pk_verify_msg_cost_per_block * num_blocks(msg) | covers cost of operating on each block in `msg` + * Note: each block is of size `BLS12381_BLOCK_SIZE` bytes, and we round up. + * `signature` and `public_key` are fixed size, so their costs are included in the base cost. + **************************************************************************************************/ +pub fn bls12381_min_pk_verify( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 3); + + // Load the cost parameters from the protocol config + let bls12381_bls12381_min_pk_verify_cost_params = get_extension!(context, NativesCostTable)? + .bls12381_bls12381_min_pk_verify_cost_params + .clone(); + + // Charge the base cost for this oper + native_charge_gas_early_exit!( + context, + bls12381_bls12381_min_pk_verify_cost_params.bls12381_bls12381_min_pk_verify_cost_base + ); + + let msg = pop_arg!(args, VectorRef); + let public_key_bytes = pop_arg!(args, VectorRef); + let signature_bytes = pop_arg!(args, VectorRef); + + let msg_ref = msg.as_bytes_ref(); + let public_key_bytes_ref = public_key_bytes.as_bytes_ref(); + let signature_bytes_ref = signature_bytes.as_bytes_ref(); + + // Charge the arg size dependent costs + native_charge_gas_early_exit!( + context, + bls12381_bls12381_min_pk_verify_cost_params + .bls12381_bls12381_min_pk_verify_msg_cost_per_byte + * (msg_ref.len() as u64).into() + + bls12381_bls12381_min_pk_verify_cost_params + .bls12381_bls12381_min_pk_verify_msg_cost_per_block + * (msg_ref.len().div_ceil(BLS12381_BLOCK_SIZE) as u64).into() + ); + + let cost = context.gas_used(); + + let signature = + match ::from_bytes(&signature_bytes_ref) { + Ok(signature) => signature, + Err(_) => return Ok(NativeResult::ok(cost, smallvec![Value::bool(false)])), + }; + + let public_key = + match ::from_bytes(&public_key_bytes_ref) { + Ok(public_key) => match public_key.validate() { + Ok(_) => public_key, + Err(_) => return Ok(NativeResult::ok(cost, smallvec![Value::bool(false)])), + }, + Err(_) => return Ok(NativeResult::ok(cost, smallvec![Value::bool(false)])), + }; + + Ok(NativeResult::ok( + cost, + smallvec![Value::bool(public_key.verify(&msg_ref, &signature).is_ok())], + )) +} diff --git a/sui-execution/replay_cut/sui-move-natives/src/crypto/ecdsa_k1.rs b/sui-execution/replay_cut/sui-move-natives/src/crypto/ecdsa_k1.rs new file mode 100644 index 0000000000000..290cf05713f52 --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/crypto/ecdsa_k1.rs @@ -0,0 +1,392 @@ +use crate::get_extension; +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 +use crate::NativesCostTable; +use fastcrypto::secp256k1::Secp256k1KeyPair; +use fastcrypto::secp256k1::Secp256k1PrivateKey; +use fastcrypto::traits::RecoverableSigner; +use fastcrypto::{ + error::FastCryptoError, + hash::{Keccak256, Sha256}, + secp256k1::{ + Secp256k1PublicKey, Secp256k1Signature, recoverable::Secp256k1RecoverableSignature, + }, + traits::{RecoverableSignature, ToFromBytes}, +}; +use move_binary_format::errors::PartialVMResult; +use move_core_types::gas_algebra::InternalGas; +use move_vm_runtime::{native_charge_gas_early_exit, native_functions::NativeContext}; +use move_vm_types::{ + loaded_data::runtime_types::Type, + natives::function::NativeResult, + pop_arg, + values::{self, Value, VectorRef}, +}; +use rand::SeedableRng; +use rand::rngs::StdRng; +use smallvec::smallvec; +use std::collections::VecDeque; +use sui_types::crypto::KeypairTraits; + +pub const FAIL_TO_RECOVER_PUBKEY: u64 = 0; +pub const INVALID_SIGNATURE: u64 = 1; +pub const INVALID_PUBKEY: u64 = 2; +pub const INVALID_PRIVKEY: u64 = 3; +pub const INVALID_HASH_FUNCTION: u64 = 4; +pub const INVALID_SEED: u64 = 5; + +pub const KECCAK256: u8 = 0; +pub const SHA256: u8 = 1; + +const KECCAK256_BLOCK_SIZE: usize = 136; +const SHA256_BLOCK_SIZE: usize = 64; +const SEED_LENGTH: usize = 32; + +#[derive(Clone)] +pub struct EcdsaK1EcrecoverCostParams { + /// Base cost for invoking the `ecrecover` function with `hash=0` implying KECCAK256 + pub ecdsa_k1_ecrecover_keccak256_cost_base: InternalGas, + /// Cost per byte of `msg` with `hash=0`implying KECCAK256 + pub ecdsa_k1_ecrecover_keccak256_msg_cost_per_byte: InternalGas, + /// Cost per block of `msg` with `hash=0`implying KECCAK256, with block size = 136 + pub ecdsa_k1_ecrecover_keccak256_msg_cost_per_block: InternalGas, + + /// Base cost for invoking the `ecrecover` function with `hash=1` implying SHA256 + pub ecdsa_k1_ecrecover_sha256_cost_base: InternalGas, + /// Cost per byte of `msg` with `hash=1`implying SHA256 + pub ecdsa_k1_ecrecover_sha256_msg_cost_per_byte: InternalGas, + /// Cost per block of `msg` with `hash=1`implying SHA256, with block size = 64 + pub ecdsa_k1_ecrecover_sha256_msg_cost_per_block: InternalGas, +} +/*************************************************************************************************** + * native fun secp256k1_ecrecover + * Implementation of the Move native function `secp256k1_ecrecover(signature: &vector, msg: &vector, hash: u8): vector` + * This function has two cost modes depending on the hash being set to `KECCAK256` or `SHA256`. The core formula is same but constants differ. + * If hash = 0, we use the `keccak256` cost constants, otherwise we use the `sha256` cost constants. + * gas cost: ecdsa_k1_ecrecover_cost_base | covers various fixed costs in the oper + * + ecdsa_k1_ecrecover_msg_cost_per_byte * size_of(msg) | covers cost of operating on each byte of `msg` + * + ecdsa_k1_ecrecover_msg_cost_per_block * num_blocks(msg) | covers cost of operating on each block in `msg` + * Note: each block is of size `KECCAK256_BLOCK_SIZE` bytes for `keccak256` and `SHA256_BLOCK_SIZE` for `sha256`, and we round up. + * `signature` is fixed size, so the cost is included in the base cost. + **************************************************************************************************/ +pub fn ecrecover( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 3); + + let hash = pop_arg!(args, u8); + + // Load the cost parameters from the protocol config + let (ecdsa_k1_ecrecover_cost_params, crypto_invalid_arguments_cost) = { + let cost_table: &NativesCostTable = get_extension!(context)?; + ( + cost_table.ecdsa_k1_ecrecover_cost_params.clone(), + cost_table.crypto_invalid_arguments_cost, + ) + }; + let (base_cost, cost_per_byte, cost_per_block, block_size) = match hash { + KECCAK256 => ( + ecdsa_k1_ecrecover_cost_params.ecdsa_k1_ecrecover_keccak256_cost_base, + ecdsa_k1_ecrecover_cost_params.ecdsa_k1_ecrecover_keccak256_msg_cost_per_byte, + ecdsa_k1_ecrecover_cost_params.ecdsa_k1_ecrecover_keccak256_msg_cost_per_block, + KECCAK256_BLOCK_SIZE, + ), + SHA256 => ( + ecdsa_k1_ecrecover_cost_params.ecdsa_k1_ecrecover_sha256_cost_base, + ecdsa_k1_ecrecover_cost_params.ecdsa_k1_ecrecover_sha256_msg_cost_per_byte, + ecdsa_k1_ecrecover_cost_params.ecdsa_k1_ecrecover_sha256_msg_cost_per_block, + SHA256_BLOCK_SIZE, + ), + _ => { + // Charge for failure but dont fail if we run out of gas otherwise the actual error is masked by OUT_OF_GAS error + context.charge_gas(crypto_invalid_arguments_cost); + return Ok(NativeResult::err( + context.gas_used(), + FAIL_TO_RECOVER_PUBKEY, + )); + } + }; + + // Charge the base cost for this oper + native_charge_gas_early_exit!(context, base_cost); + + let msg = pop_arg!(args, VectorRef); + let signature = pop_arg!(args, VectorRef); + + let msg_ref = msg.as_bytes_ref(); + let signature_ref = signature.as_bytes_ref(); + + // Charge the arg size dependent costs + native_charge_gas_early_exit!( + context, + cost_per_byte * (msg_ref.len() as u64).into() + + cost_per_block * (msg_ref.len().div_ceil(block_size) as u64).into() + ); + + let cost = context.gas_used(); + + let Ok(sig) = ::from_bytes(&signature_ref) else { + return Ok(NativeResult::err(cost, INVALID_SIGNATURE)); + }; + + let pk = match hash { + KECCAK256 => sig.recover_with_hash::(&msg_ref), + SHA256 => sig.recover_with_hash::(&msg_ref), + _ => Err(FastCryptoError::InvalidInput), // We should never reach here + }; + + match pk { + Ok(pk) => Ok(NativeResult::ok( + cost, + smallvec![Value::vector_u8(pk.as_bytes().to_vec())], + )), + Err(_) => Ok(NativeResult::err(cost, FAIL_TO_RECOVER_PUBKEY)), + } +} + +#[derive(Clone)] +pub struct EcdsaK1DecompressPubkeyCostParams { + pub ecdsa_k1_decompress_pubkey_cost_base: InternalGas, +} +pub fn decompress_pubkey( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 1); + + // Load the cost parameters from the protocol config + let ecdsa_k1_decompress_pubkey_cost_params = get_extension!(context, NativesCostTable)? + .ecdsa_k1_decompress_pubkey_cost_params + .clone(); + // Charge the base cost for this oper + native_charge_gas_early_exit!( + context, + ecdsa_k1_decompress_pubkey_cost_params.ecdsa_k1_decompress_pubkey_cost_base + ); + + let pubkey = pop_arg!(args, VectorRef); + let pubkey_ref = pubkey.as_bytes_ref(); + + let cost = context.gas_used(); + + match Secp256k1PublicKey::from_bytes(&pubkey_ref) { + Ok(pubkey) => { + let uncompressed = &pubkey.pubkey.serialize_uncompressed(); + Ok(NativeResult::ok( + cost, + smallvec![Value::vector_u8(uncompressed.to_vec())], + )) + } + Err(_) => Ok(NativeResult::err(cost, INVALID_PUBKEY)), + } +} + +#[derive(Clone)] +pub struct EcdsaK1Secp256k1VerifyCostParams { + /// Base cost for invoking the `secp256k1_verify` function with `hash=0` implying KECCAK256 + pub ecdsa_k1_secp256k1_verify_keccak256_cost_base: InternalGas, + /// Cost per byte of `msg` with `hash=0`implying KECCAK256 + pub ecdsa_k1_secp256k1_verify_keccak256_msg_cost_per_byte: InternalGas, + /// Cost per block of `msg` with `hash=0`implying KECCAK256, with block size = 136 + pub ecdsa_k1_secp256k1_verify_keccak256_msg_cost_per_block: InternalGas, + + /// Base cost for invoking the `secp256k1_verify` function with `hash=1` implying SHA256 + pub ecdsa_k1_secp256k1_verify_sha256_cost_base: InternalGas, + /// Cost per byte of `msg` with `hash=1`implying SHA256 + pub ecdsa_k1_secp256k1_verify_sha256_msg_cost_per_byte: InternalGas, + /// Cost per block of `msg` with `hash=1`implying SHA256, with block size = 64 + pub ecdsa_k1_secp256k1_verify_sha256_msg_cost_per_block: InternalGas, +} +/*************************************************************************************************** + * native fun secp256k1_verify + * Implementation of the Move native function `secp256k1_verify(signature: &vector, public_key: &vector, msg: &vector, hash: u8): bool` + * This function has two cost modes depending on the hash being set to`KECCAK256` or `SHA256`. The core formula is same but constants differ. + * If hash = 0, we use the `keccak256` cost constants, otherwise we use the `sha256` cost constants. + * gas cost: ecdsa_k1_secp256k1_verify_cost_base | covers various fixed costs in the oper + * + ecdsa_k1_secp256k1_verify_msg_cost_per_byte * size_of(msg) | covers cost of operating on each byte of `msg` + * + ecdsa_k1_secp256k1_verify_msg_cost_per_block * num_blocks(msg) | covers cost of operating on each block in `msg` + * Note: each block is of size `KECCAK256_BLOCK_SIZE` bytes for `keccak256` and `SHA256_BLOCK_SIZE` for `sha256`, and we round up. + * `signature` and `public_key` are fixed size, so their costs are included in the base cost. + **************************************************************************************************/ +pub fn secp256k1_verify( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 4); + + let hash = pop_arg!(args, u8); + + // Load the cost parameters from the protocol config + let (ecdsa_k1_secp256k1_verify_cost_params, crypto_invalid_arguments_cost) = { + let cost_table: &NativesCostTable = get_extension!(context)?; + ( + cost_table.ecdsa_k1_secp256k1_verify_cost_params.clone(), + cost_table.crypto_invalid_arguments_cost, + ) + }; + + let (base_cost, cost_per_byte, cost_per_block, block_size) = match hash { + KECCAK256 => ( + ecdsa_k1_secp256k1_verify_cost_params.ecdsa_k1_secp256k1_verify_keccak256_cost_base, + ecdsa_k1_secp256k1_verify_cost_params + .ecdsa_k1_secp256k1_verify_keccak256_msg_cost_per_byte, + ecdsa_k1_secp256k1_verify_cost_params + .ecdsa_k1_secp256k1_verify_keccak256_msg_cost_per_block, + KECCAK256_BLOCK_SIZE, + ), + SHA256 => ( + ecdsa_k1_secp256k1_verify_cost_params.ecdsa_k1_secp256k1_verify_sha256_cost_base, + ecdsa_k1_secp256k1_verify_cost_params + .ecdsa_k1_secp256k1_verify_sha256_msg_cost_per_byte, + ecdsa_k1_secp256k1_verify_cost_params + .ecdsa_k1_secp256k1_verify_sha256_msg_cost_per_block, + SHA256_BLOCK_SIZE, + ), + _ => { + // Charge for failure but dont fail if we run out of gas otherwise the actual error is masked by OUT_OF_GAS error + context.charge_gas(crypto_invalid_arguments_cost); + + return Ok(NativeResult::ok( + context.gas_used(), + smallvec![Value::bool(false)], + )); + } + }; + // Charge the base cost for this oper + native_charge_gas_early_exit!(context, base_cost); + + let msg = pop_arg!(args, VectorRef); + let public_key_bytes = pop_arg!(args, VectorRef); + let signature_bytes = pop_arg!(args, VectorRef); + + let msg_ref = msg.as_bytes_ref(); + let public_key_bytes_ref = public_key_bytes.as_bytes_ref(); + let signature_bytes_ref = signature_bytes.as_bytes_ref(); + + // Charge the arg size dependent costs + native_charge_gas_early_exit!( + context, + cost_per_byte * (msg_ref.len() as u64).into() + + cost_per_block * (msg_ref.len().div_ceil(block_size) as u64).into() + ); + + let cost = context.gas_used(); + + let Ok(sig) = ::from_bytes(&signature_bytes_ref) else { + return Ok(NativeResult::ok(cost, smallvec![Value::bool(false)])); + }; + + let Ok(pk) = ::from_bytes(&public_key_bytes_ref) else { + return Ok(NativeResult::ok(cost, smallvec![Value::bool(false)])); + }; + + let result = match hash { + KECCAK256 => pk.verify_with_hash::(&msg_ref, &sig).is_ok(), + SHA256 => pk.verify_with_hash::(&msg_ref, &sig).is_ok(), + _ => false, + }; + + Ok(NativeResult::ok(cost, smallvec![Value::bool(result)])) +} + +/*************************************************************************************************** + * native fun secp256k1_sign (TEST ONLY) + * Implementation of the Move native function `secp256k1_sign(private_key: &vector, msg: &vector, hash: u8): vector` + * This function has two cost modes depending on the hash being set to`KECCAK256` or `SHA256`. The core formula is same but constants differ. + * If hash = 0, we use the `keccak256` cost constants, otherwise we use the `sha256` cost constants. + * gas cost: 0 (because it is only for test purposes) + **************************************************************************************************/ +pub fn secp256k1_sign( + _context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 4); + + // The corresponding Move function, sui::ecdsa_k1::secp256k1_sign, is only used for testing, so + // we don't need to charge any gas. + let cost = 0.into(); + + let recoverable = pop_arg!(args, bool); + let hash = pop_arg!(args, u8); + let msg = pop_arg!(args, VectorRef); + let private_key_bytes = pop_arg!(args, VectorRef); + + let msg_ref = msg.as_bytes_ref(); + let private_key_bytes_ref = private_key_bytes.as_bytes_ref(); + + let sk = match ::from_bytes(&private_key_bytes_ref) { + Ok(sk) => sk, + Err(_) => return Ok(NativeResult::err(cost, INVALID_PRIVKEY)), + }; + + let kp = Secp256k1KeyPair::from(sk); + + let signature = match (hash, recoverable) { + (KECCAK256, true) => kp + .sign_recoverable_with_hash::(&msg_ref) + .as_bytes() + .to_vec(), + (KECCAK256, false) => kp.sign_with_hash::(&msg_ref).as_bytes().to_vec(), + (SHA256, true) => kp + .sign_recoverable_with_hash::(&msg_ref) + .as_bytes() + .to_vec(), + (SHA256, false) => kp.sign_with_hash::(&msg_ref).as_bytes().to_vec(), + _ => return Ok(NativeResult::err(cost, INVALID_HASH_FUNCTION)), + }; + + Ok(NativeResult::ok( + cost, + smallvec![Value::vector_u8(signature)], + )) +} + +/*************************************************************************************************** + * native fun secp256k1_keypair_from_seed (TEST ONLY) + * Implementation of the Move native function `secp256k1_sign(seed: &vector): KeyPair` + * Seed must be exactly 32 bytes long. + * gas cost: 0 (because it is only for test purposes) + **************************************************************************************************/ +pub fn secp256k1_keypair_from_seed( + _context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 1); + + // The corresponding Move function, sui::ecdsa_k1::secp256k1_keypair_from_seed, is only used for + // testing, so we don't need to charge any gas. + let cost = 0.into(); + + let seed = pop_arg!(args, VectorRef); + let seed_ref = seed.as_bytes_ref(); + + if seed_ref.len() != SEED_LENGTH { + return Ok(NativeResult::err(cost, INVALID_SEED)); + } + let mut seed_array = [0u8; SEED_LENGTH]; + seed_array.clone_from_slice(&seed_ref); + + let kp = Secp256k1KeyPair::generate(&mut StdRng::from_seed(seed_array)); + + let pk_bytes = kp.public().as_bytes().to_vec(); + let sk_bytes = kp.private().as_bytes().to_vec(); + + Ok(NativeResult::ok( + cost, + smallvec![Value::struct_(values::Struct::pack(vec![ + Value::vector_u8(sk_bytes), + Value::vector_u8(pk_bytes), + ]))], + )) +} diff --git a/sui-execution/replay_cut/sui-move-natives/src/crypto/ecdsa_r1.rs b/sui-execution/replay_cut/sui-move-natives/src/crypto/ecdsa_r1.rs new file mode 100644 index 0000000000000..0f25fb3a1c369 --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/crypto/ecdsa_r1.rs @@ -0,0 +1,247 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 +use crate::{NativesCostTable, get_extension}; +use fastcrypto::error::FastCryptoError; +use fastcrypto::hash::{Keccak256, Sha256}; +use fastcrypto::traits::RecoverableSignature; +use fastcrypto::{ + secp256r1::{ + Secp256r1PublicKey, Secp256r1Signature, recoverable::Secp256r1RecoverableSignature, + }, + traits::ToFromBytes, +}; +use move_binary_format::errors::PartialVMResult; +use move_core_types::gas_algebra::InternalGas; +use move_vm_runtime::native_charge_gas_early_exit; +use move_vm_runtime::native_functions::NativeContext; +use move_vm_types::{ + loaded_data::runtime_types::Type, + natives::function::NativeResult, + pop_arg, + values::{Value, VectorRef}, +}; +use smallvec::smallvec; +use std::collections::VecDeque; + +pub const FAIL_TO_RECOVER_PUBKEY: u64 = 0; +pub const INVALID_SIGNATURE: u64 = 1; + +pub const KECCAK256: u8 = 0; +pub const SHA256: u8 = 1; + +const KECCAK256_BLOCK_SIZE: usize = 136; +const SHA256_BLOCK_SIZE: usize = 64; + +#[derive(Clone)] +pub struct EcdsaR1EcrecoverCostParams { + /// Base cost for invoking the `ecrecover` function with `hash=0` implying KECCAK256 + pub ecdsa_r1_ecrecover_keccak256_cost_base: InternalGas, + /// Cost per byte of `msg` with `hash=0`implying KECCAK256 + pub ecdsa_r1_ecrecover_keccak256_msg_cost_per_byte: InternalGas, + /// Cost per block of `msg` with `hash=0`implying KECCAK256, with block size = 136 + pub ecdsa_r1_ecrecover_keccak256_msg_cost_per_block: InternalGas, + + /// Base cost for invoking the `ecrecover` function with `hash=1` implying SHA256 + pub ecdsa_r1_ecrecover_sha256_cost_base: InternalGas, + /// Cost per byte of `msg` with `hash=1`implying SHA256 + pub ecdsa_r1_ecrecover_sha256_msg_cost_per_byte: InternalGas, + /// Cost per block of `msg` with `hash=1`implying SHA256, with block size = 64 + pub ecdsa_r1_ecrecover_sha256_msg_cost_per_block: InternalGas, +} +/*************************************************************************************************** + * native fun secp256r1_ecrecover + * Implementation of the Move native function `secp256r1_ecrecover(signature: &vector, msg: &vector, hash: u8): vector` + * This function has two cost modes depending on the hash being set to `KECCAK256` or `SHA256`. The core formula is same but constants differ. + * If hash = 0, we use the `keccak256` cost constants, otherwise we use the `sha256` cost constants. + * gas cost: ecdsa_r1_ecrecover_cost_base | covers various fixed costs in the oper + * + ecdsa_r1_ecrecover_msg_cost_per_byte * size_of(msg) | covers cost of operating on each byte of `msg` + * + ecdsa_r1_ecrecover_msg_cost_per_block * num_blocks(msg) | covers cost of operating on each block in `msg` + * Note: each block is of size `KECCAK256_BLOCK_SIZE` bytes for `keccak256` and `SHA256_BLOCK_SIZE` for `sha256`, and we round up. + * `signature` is fixed size, so the cost is included in the base cost. + **************************************************************************************************/ +pub fn ecrecover( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 3); + + let hash = pop_arg!(args, u8); + + // Load the cost parameters from the protocol config + let (ecdsa_r1_ecrecover_cost_params, crypto_invalid_arguments_cost) = { + let cost_table: &NativesCostTable = get_extension!(context)?; + ( + cost_table.ecdsa_r1_ecrecover_cost_params.clone(), + cost_table.crypto_invalid_arguments_cost, + ) + }; + + let (base_cost, cost_per_byte, cost_per_block, block_size) = match hash { + KECCAK256 => ( + ecdsa_r1_ecrecover_cost_params.ecdsa_r1_ecrecover_keccak256_cost_base, + ecdsa_r1_ecrecover_cost_params.ecdsa_r1_ecrecover_keccak256_msg_cost_per_byte, + ecdsa_r1_ecrecover_cost_params.ecdsa_r1_ecrecover_keccak256_msg_cost_per_block, + KECCAK256_BLOCK_SIZE, + ), + SHA256 => ( + ecdsa_r1_ecrecover_cost_params.ecdsa_r1_ecrecover_sha256_cost_base, + ecdsa_r1_ecrecover_cost_params.ecdsa_r1_ecrecover_sha256_msg_cost_per_byte, + ecdsa_r1_ecrecover_cost_params.ecdsa_r1_ecrecover_sha256_msg_cost_per_block, + SHA256_BLOCK_SIZE, + ), + _ => { + // Charge for failure but dont fail if we run out of gas otherwise the actual error is masked by OUT_OF_GAS error + context.charge_gas(crypto_invalid_arguments_cost); + + return Ok(NativeResult::err( + context.gas_used(), + FAIL_TO_RECOVER_PUBKEY, + )); + } + }; + + // Charge the base cost for this oper + native_charge_gas_early_exit!(context, base_cost); + + let msg = pop_arg!(args, VectorRef); + let signature = pop_arg!(args, VectorRef); + + let msg_ref = msg.as_bytes_ref(); + let signature_ref = signature.as_bytes_ref(); + + // Charge the arg size dependent costs + native_charge_gas_early_exit!( + context, + cost_per_byte * (msg_ref.len() as u64).into() + + cost_per_block * (msg_ref.len().div_ceil(block_size) as u64).into() + ); + + let cost = context.gas_used(); + + let Ok(sig) = ::from_bytes(&signature_ref) else { + return Ok(NativeResult::err(cost, INVALID_SIGNATURE)); + }; + + let pk = match hash { + KECCAK256 => sig.recover_with_hash::(&msg_ref), + SHA256 => sig.recover_with_hash::(&msg_ref), + _ => Err(FastCryptoError::InvalidInput), + }; + + match pk { + Ok(pk) => Ok(NativeResult::ok( + cost, + smallvec![Value::vector_u8(pk.as_bytes().to_vec())], + )), + Err(_) => Ok(NativeResult::err(cost, FAIL_TO_RECOVER_PUBKEY)), + } +} + +#[derive(Clone)] +pub struct EcdsaR1Secp256R1VerifyCostParams { + /// Base cost for invoking the `secp256r1_verify` function with `hash=0` implying KECCAK256 + pub ecdsa_r1_secp256r1_verify_keccak256_cost_base: InternalGas, + /// Cost per byte of `msg` with `hash=0`implying KECCAK256 + pub ecdsa_r1_secp256r1_verify_keccak256_msg_cost_per_byte: InternalGas, + /// Cost per block of `msg` with `hash=0`implying KECCAK256, with block size = 136 + pub ecdsa_r1_secp256r1_verify_keccak256_msg_cost_per_block: InternalGas, + + /// Base cost for invoking the `secp256r1_verify` function with `hash=1` implying SHA256 + pub ecdsa_r1_secp256r1_verify_sha256_cost_base: InternalGas, + /// Cost per byte of `msg` with `hash=1`implying SHA256 + pub ecdsa_r1_secp256r1_verify_sha256_msg_cost_per_byte: InternalGas, + /// Cost per block of `msg` with `hash=1`implying SHA256, with block size = 64 + pub ecdsa_r1_secp256r1_verify_sha256_msg_cost_per_block: InternalGas, +} +/*************************************************************************************************** + * native fun secp256r1_verify + * Implementation of the Move native function `secp256r1_verify(signature: &vector, public_key: &vector, msg: &vector, hash: u8): bool` + * This function has two cost modes depending on the hash being set to `KECCAK256` or `SHA256`. The core formula is same but constants differ. + * If hash = 0, we use the `keccak256` cost constants, otherwise we use the `sha256` cost constants. + * gas cost: ecdsa_r1_secp256r1_verify_cost_base | covers various fixed costs in the oper + * + ecdsa_r1_secp256r1_verify_msg_cost_per_byte * size_of(msg) | covers cost of operating on each byte of `msg` + * + ecdsa_r1_secp256r1_verify_msg_cost_per_block * num_blocks(msg) | covers cost of operating on each block in `msg` + * Note: each block is of size `KECCAK256_BLOCK_SIZE` bytes for `keccak256` and `SHA256_BLOCK_SIZE` for `sha256`, and we round up. + * `signature` and `public_key` are fixed size, so their costs are included in the base cost. + **************************************************************************************************/ +pub fn secp256r1_verify( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 4); + // Load the cost parameters from the protocol config + let (ecdsa_r1_secp256_r1_verify_cost_params, crypto_invalid_arguments_cost) = { + let cost_table: &NativesCostTable = get_extension!(context)?; + ( + cost_table.ecdsa_r1_secp256_r1_verify_cost_params.clone(), + cost_table.crypto_invalid_arguments_cost, + ) + }; + let hash = pop_arg!(args, u8); + let (base_cost, cost_per_byte, cost_per_block, block_size) = match hash { + KECCAK256 => ( + ecdsa_r1_secp256_r1_verify_cost_params.ecdsa_r1_secp256r1_verify_keccak256_cost_base, + ecdsa_r1_secp256_r1_verify_cost_params + .ecdsa_r1_secp256r1_verify_keccak256_msg_cost_per_byte, + ecdsa_r1_secp256_r1_verify_cost_params + .ecdsa_r1_secp256r1_verify_keccak256_msg_cost_per_block, + KECCAK256_BLOCK_SIZE, + ), + SHA256 => ( + ecdsa_r1_secp256_r1_verify_cost_params.ecdsa_r1_secp256r1_verify_sha256_cost_base, + ecdsa_r1_secp256_r1_verify_cost_params + .ecdsa_r1_secp256r1_verify_sha256_msg_cost_per_byte, + ecdsa_r1_secp256_r1_verify_cost_params + .ecdsa_r1_secp256r1_verify_sha256_msg_cost_per_block, + SHA256_BLOCK_SIZE, + ), + _ => { + // Charge for failure but dont fail if we run out of gas otherwise the actual error is masked by OUT_OF_GAS error + context.charge_gas(crypto_invalid_arguments_cost); + return Ok(NativeResult::ok( + context.gas_used(), + smallvec![Value::bool(false)], + )); + } + }; + + // Charge the base cost for this oper + native_charge_gas_early_exit!(context, base_cost); + + let msg = pop_arg!(args, VectorRef); + let public_key_bytes = pop_arg!(args, VectorRef); + let signature_bytes = pop_arg!(args, VectorRef); + + let msg_ref = msg.as_bytes_ref(); + let public_key_bytes_ref = public_key_bytes.as_bytes_ref(); + let signature_bytes_ref = signature_bytes.as_bytes_ref(); + + // Charge the arg size dependent costs + native_charge_gas_early_exit!( + context, + cost_per_byte * (msg_ref.len() as u64).into() + + cost_per_block * (msg_ref.len().div_ceil(block_size) as u64).into() + ); + + let cost = context.gas_used(); + + let Ok(sig) = ::from_bytes(&signature_bytes_ref) else { + return Ok(NativeResult::ok(cost, smallvec![Value::bool(false)])); + }; + + let Ok(pk) = ::from_bytes(&public_key_bytes_ref) else { + return Ok(NativeResult::ok(cost, smallvec![Value::bool(false)])); + }; + + let result = match hash { + KECCAK256 => pk.verify_with_hash::(&msg_ref, &sig).is_ok(), + SHA256 => pk.verify_with_hash::(&msg_ref, &sig).is_ok(), + _ => false, + }; + + Ok(NativeResult::ok(cost, smallvec![Value::bool(result)])) +} diff --git a/sui-execution/replay_cut/sui-move-natives/src/crypto/ecvrf.rs b/sui-execution/replay_cut/sui-move-natives/src/crypto/ecvrf.rs new file mode 100644 index 0000000000000..7a2946fa88c8d --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/crypto/ecvrf.rs @@ -0,0 +1,96 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 +use crate::{NativesCostTable, get_extension}; +use fastcrypto::vrf::VRFProof; +use fastcrypto::vrf::ecvrf::{ECVRFProof, ECVRFPublicKey}; +use move_binary_format::errors::PartialVMResult; +use move_core_types::gas_algebra::InternalGas; +use move_vm_runtime::{native_charge_gas_early_exit, native_functions::NativeContext}; +use move_vm_types::{ + loaded_data::runtime_types::Type, + natives::function::NativeResult, + pop_arg, + values::{Value, VectorRef}, +}; +use smallvec::smallvec; +use std::collections::VecDeque; + +pub const INVALID_ECVRF_HASH_LENGTH: u64 = 1; +pub const INVALID_ECVRF_PUBLIC_KEY: u64 = 2; +pub const INVALID_ECVRF_PROOF: u64 = 3; + +const ECVRF_SHA512_BLOCK_SIZE: usize = 128; + +#[derive(Clone)] +pub struct EcvrfEcvrfVerifyCostParams { + /// Base cost for invoking the `ecvrf_verify` + pub ecvrf_ecvrf_verify_cost_base: InternalGas, + /// Cost per byte of `alpha_string` + pub ecvrf_ecvrf_verify_alpha_string_cost_per_byte: InternalGas, + /// Cost per block of `alpha_string` with block size = 128 + pub ecvrf_ecvrf_verify_alpha_string_cost_per_block: InternalGas, +} +/*************************************************************************************************** + * native fun ecvrf_verify + * Implementation of the Move native function `ecvrf_verify(hash: &vector, alpha_string: &vector, public_key: &vector, proof: &vector): bool` + * gas cost: ecvrf_ecvrf_verify_cost_base | covers various fixed costs in the oper + * + ecvrf_ecvrf_verify_alpha_string_cost_per_byte * size_of(alpha_string) | covers cost of operating on each byte of `alpha_string` + * + ecvrf_ecvrf_verify_alpha_string_cost_per_block * num_blocks(alpha_string) | covers cost of operating on each block in `alpha_string` + * Note: each block is of size `ECVRF_SHA512_BLOCK_SIZE` bytes, and we round up. + * `hash`, `proof`, and `public_key` are fixed size, so their costs are included in the base cost. + **************************************************************************************************/ +pub fn ecvrf_verify( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 4); + + // Load the cost parameters from the protocol config + let ecvrf_ecvrf_verify_cost_params = get_extension!(context, NativesCostTable)? + .ecvrf_ecvrf_verify_cost_params + .clone(); + // Charge the base cost for this oper + native_charge_gas_early_exit!( + context, + ecvrf_ecvrf_verify_cost_params.ecvrf_ecvrf_verify_cost_base + ); + + let proof_bytes = pop_arg!(args, VectorRef); + let public_key_bytes = pop_arg!(args, VectorRef); + let alpha_string = pop_arg!(args, VectorRef); + let hash_bytes = pop_arg!(args, VectorRef); + + let alpha_string_len = alpha_string.as_bytes_ref().len(); + // Charge the arg size dependent costs + native_charge_gas_early_exit!( + context, + ecvrf_ecvrf_verify_cost_params.ecvrf_ecvrf_verify_alpha_string_cost_per_byte + * (alpha_string_len as u64).into() + + ecvrf_ecvrf_verify_cost_params.ecvrf_ecvrf_verify_alpha_string_cost_per_block + * (alpha_string_len.div_ceil(ECVRF_SHA512_BLOCK_SIZE) as u64).into() + ); + + let cost = context.gas_used(); + + let Ok(hash) = hash_bytes.as_bytes_ref().as_slice().try_into() else { + return Ok(NativeResult::err(cost, INVALID_ECVRF_HASH_LENGTH)); + }; + + let Ok(public_key) = + bcs::from_bytes::(public_key_bytes.as_bytes_ref().as_slice()) + else { + return Ok(NativeResult::err(cost, INVALID_ECVRF_PUBLIC_KEY)); + }; + + let Ok(proof) = bcs::from_bytes::(proof_bytes.as_bytes_ref().as_slice()) else { + return Ok(NativeResult::err(cost, INVALID_ECVRF_PROOF)); + }; + + let result = proof.verify_output(alpha_string.as_bytes_ref().as_slice(), &public_key, &hash); + Ok(NativeResult::ok( + cost, + smallvec![Value::bool(result.is_ok())], + )) +} diff --git a/sui-execution/replay_cut/sui-move-natives/src/crypto/ed25519.rs b/sui-execution/replay_cut/sui-move-natives/src/crypto/ed25519.rs new file mode 100644 index 0000000000000..a49bdab7db56c --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/crypto/ed25519.rs @@ -0,0 +1,88 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 +use crate::{NativesCostTable, get_extension}; +use fastcrypto::{ + ed25519::{Ed25519PublicKey, Ed25519Signature}, + traits::{ToFromBytes, VerifyingKey}, +}; +use move_binary_format::errors::PartialVMResult; +use move_core_types::gas_algebra::InternalGas; +use move_vm_runtime::{native_charge_gas_early_exit, native_functions::NativeContext}; +use move_vm_types::{ + loaded_data::runtime_types::Type, + natives::function::NativeResult, + pop_arg, + values::{Value, VectorRef}, +}; +use smallvec::smallvec; +use std::collections::VecDeque; + +const ED25519_BLOCK_SIZE: usize = 128; + +#[derive(Clone)] +pub struct Ed25519VerifyCostParams { + /// Base cost for invoking the `ed25519_verify` function + pub ed25519_ed25519_verify_cost_base: InternalGas, + /// Cost per byte of `msg` + pub ed25519_ed25519_verify_msg_cost_per_byte: InternalGas, + /// Cost per block of `msg`, where a block is 128 bytes + pub ed25519_ed25519_verify_msg_cost_per_block: InternalGas, +} +/*************************************************************************************************** + * native fun ed25519_verify + * Implementation of the Move native function `ed25519::ed25519_verify(signature: &vector, public_key: &vector, msg: &vector): bool;` + * gas cost: ed25519_ed25519_verify_cost_base | base cost for function call and fixed opers + * + ed25519_ed25519_verify_msg_cost_per_byte * msg.len() | cost depends on length of message + * + ed25519_ed25519_verify_msg_cost_per_block * num_blocks(msg) | cost depends on number of blocks in message + * Note: each block is of size `ED25519_BLOCK_SIZE` bytes, and we round up. + * `signature` and `public_key` are fixed size, so their costs are included in the base cost. + **************************************************************************************************/ +pub fn ed25519_verify( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 3); + + // Load the cost parameters from the protocol config + let ed25519_verify_cost_params = get_extension!(context, NativesCostTable)? + .ed25519_verify_cost_params + .clone(); + // Charge the base cost for this oper + native_charge_gas_early_exit!( + context, + ed25519_verify_cost_params.ed25519_ed25519_verify_cost_base + ); + + let msg = pop_arg!(args, VectorRef); + let msg_ref = msg.as_bytes_ref(); + let public_key_bytes = pop_arg!(args, VectorRef); + let public_key_bytes_ref = public_key_bytes.as_bytes_ref(); + let signature_bytes = pop_arg!(args, VectorRef); + let signature_bytes_ref = signature_bytes.as_bytes_ref(); + + // Charge the arg size dependent costs + native_charge_gas_early_exit!( + context, + ed25519_verify_cost_params.ed25519_ed25519_verify_msg_cost_per_byte + * (msg_ref.len() as u64).into() + + ed25519_verify_cost_params.ed25519_ed25519_verify_msg_cost_per_block + * (msg_ref.len().div_ceil(ED25519_BLOCK_SIZE) as u64).into() + ); + let cost = context.gas_used(); + + let Ok(signature) = ::from_bytes(&signature_bytes_ref) else { + return Ok(NativeResult::ok(cost, smallvec![Value::bool(false)])); + }; + + let Ok(public_key) = ::from_bytes(&public_key_bytes_ref) + else { + return Ok(NativeResult::ok(cost, smallvec![Value::bool(false)])); + }; + + Ok(NativeResult::ok( + cost, + smallvec![Value::bool(public_key.verify(&msg_ref, &signature).is_ok())], + )) +} diff --git a/sui-execution/replay_cut/sui-move-natives/src/crypto/groth16.rs b/sui-execution/replay_cut/sui-move-natives/src/crypto/groth16.rs new file mode 100644 index 0000000000000..b6c2abeb799eb --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/crypto/groth16.rs @@ -0,0 +1,247 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 +use crate::{NativesCostTable, get_extension, object_runtime::ObjectRuntime}; +use move_binary_format::errors::PartialVMResult; +use move_core_types::gas_algebra::InternalGas; +use move_vm_runtime::{native_charge_gas_early_exit, native_functions::NativeContext}; +use move_vm_types::{ + loaded_data::runtime_types::Type, + natives::function::NativeResult, + pop_arg, + values::{self, Value, VectorRef}, +}; +use smallvec::smallvec; +use std::collections::VecDeque; + +pub const INVALID_VERIFYING_KEY: u64 = 0; +pub const INVALID_CURVE: u64 = 1; +pub const TOO_MANY_PUBLIC_INPUTS: u64 = 2; + +// These must match the corresponding values in sui::groth16::Curve. +pub const BLS12381: u8 = 0; +pub const BN254: u8 = 1; + +// We need to set an upper bound on the number of public inputs to avoid a DoS attack +pub const MAX_PUBLIC_INPUTS: usize = 8; + +#[derive(Clone)] +pub struct Groth16PrepareVerifyingKeyCostParams { + pub groth16_prepare_verifying_key_bls12381_cost_base: InternalGas, + pub groth16_prepare_verifying_key_bn254_cost_base: InternalGas, +} +/*************************************************************************************************** + * native fun prepare_verifying_key_internal + * Implementation of the Move native function `prepare_verifying_key_internal(curve: u8, verifying_key: &vector): PreparedVerifyingKey` + * This function has two cost modes depending on the curve being set to `BLS12381` or `BN254`. The core formula is same but constants differ. + * If curve = 0, we use the `bls12381` cost constants, otherwise we use the `bn254` cost constants. + * gas cost: groth16_prepare_verifying_key_cost_base | covers various fixed costs in the oper + * Note: `curve` and `verifying_key` are fixed size, so their costs are included in the base cost. + **************************************************************************************************/ +pub fn prepare_verifying_key_internal( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 2); + + // Load the cost parameters from the protocol config + let (groth16_prepare_verifying_key_cost_params, crypto_invalid_arguments_cost) = { + let cost_table: &NativesCostTable = get_extension!(context)?; + ( + cost_table.groth16_prepare_verifying_key_cost_params.clone(), + cost_table.crypto_invalid_arguments_cost, + ) + }; + let bytes = pop_arg!(args, VectorRef); + let verifying_key = bytes.as_bytes_ref(); + + let curve = pop_arg!(args, u8); + + // Load the cost parameters from the protocol config + let base_cost = match curve { + BLS12381 => { + groth16_prepare_verifying_key_cost_params + .groth16_prepare_verifying_key_bls12381_cost_base + } + BN254 => { + groth16_prepare_verifying_key_cost_params.groth16_prepare_verifying_key_bn254_cost_base + } + _ => { + // Charge for failure but dont fail if we run out of gas otherwise the actual error is masked by OUT_OF_GAS error + context.charge_gas(crypto_invalid_arguments_cost); + return Ok(NativeResult::err(context.gas_used(), INVALID_CURVE)); + } + }; + // Charge the base cost for this oper + native_charge_gas_early_exit!(context, base_cost); + let cost = context.gas_used(); + + let result; + if curve == BLS12381 { + result = fastcrypto_zkp::bls12381::api::prepare_pvk_bytes(&verifying_key); + } else if curve == BN254 { + result = fastcrypto_zkp::bn254::api::prepare_pvk_bytes(&verifying_key); + } else { + return Ok(NativeResult::err(cost, INVALID_CURVE)); + } + + match result { + Ok(pvk) => Ok(NativeResult::ok( + cost, + smallvec![Value::struct_(values::Struct::pack(vec![ + Value::vector_u8(pvk[0].to_vec()), + Value::vector_u8(pvk[1].to_vec()), + Value::vector_u8(pvk[2].to_vec()), + Value::vector_u8(pvk[3].to_vec()) + ]))], + )), + Err(_) => Ok(NativeResult::err(cost, INVALID_VERIFYING_KEY)), + } +} + +#[derive(Clone)] +pub struct Groth16VerifyGroth16ProofInternalCostParams { + pub groth16_verify_groth16_proof_internal_bls12381_cost_base: InternalGas, + pub groth16_verify_groth16_proof_internal_bls12381_cost_per_public_input: InternalGas, + + pub groth16_verify_groth16_proof_internal_bn254_cost_base: InternalGas, + pub groth16_verify_groth16_proof_internal_bn254_cost_per_public_input: InternalGas, + + pub groth16_verify_groth16_proof_internal_public_input_cost_per_byte: InternalGas, +} +/*************************************************************************************************** + * native fun verify_groth16_proof_internal + * Implementation of the Move native function `verify_groth16_proof_internal(curve: u8, vk_gamma_abc_g1_bytes: &vector, + * alpha_g1_beta_g2_bytes: &vector, gamma_g2_neg_pc_bytes: &vector, delta_g2_neg_pc_bytes: &vector, + * public_proof_inputs: &vector, proof_points: &vector): bool` + * + * This function has two cost modes depending on the curve being set to `BLS12381` or `BN254`. The core formula is same but constants differ. + * If curve = 0, we use the `bls12381` cost constants, otherwise we use the `bn254` cost constants. + * gas cost: groth16_prepare_verifying_key_cost_base | covers various fixed costs in the oper + * + groth16_verify_groth16_proof_internal_public_input_cost_per_byte + * * size_of(public_proof_inputs) | covers the cost of verifying each public input per byte + * + groth16_verify_groth16_proof_internal_cost_per_public_input + * * num_public_inputs) | covers the cost of verifying each public input per input + * Note: every other arg is fixed size, so their costs are included in the base cost. + **************************************************************************************************/ +pub fn verify_groth16_proof_internal( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 7); + + // Load the cost parameters from the protocol config + let (groth16_verify_groth16_proof_internal_cost_params, crypto_invalid_arguments_cost) = { + let cost_table: &NativesCostTable = get_extension!(context)?; + ( + cost_table + .groth16_verify_groth16_proof_internal_cost_params + .clone(), + cost_table.crypto_invalid_arguments_cost, + ) + }; + let bytes5 = pop_arg!(args, VectorRef); + let proof_points = bytes5.as_bytes_ref(); + + let bytes4 = pop_arg!(args, VectorRef); + let public_proof_inputs = bytes4.as_bytes_ref(); + + let bytes3 = pop_arg!(args, VectorRef); + let delta_g2_neg_pc = bytes3.as_bytes_ref(); + + let bytes2 = pop_arg!(args, VectorRef); + let gamma_g2_neg_pc = bytes2.as_bytes_ref(); + + let byte1 = pop_arg!(args, VectorRef); + let alpha_g1_beta_g2 = byte1.as_bytes_ref(); + + let bytes = pop_arg!(args, VectorRef); + let vk_gamma_abc_g1 = bytes.as_bytes_ref(); + + let curve = pop_arg!(args, u8); + + let (base_cost, cost_per_public_input, num_public_inputs) = match curve { + BLS12381 => ( + groth16_verify_groth16_proof_internal_cost_params + .groth16_verify_groth16_proof_internal_bls12381_cost_base, + groth16_verify_groth16_proof_internal_cost_params + .groth16_verify_groth16_proof_internal_bls12381_cost_per_public_input, + public_proof_inputs + .len() + .div_ceil(fastcrypto::groups::bls12381::SCALAR_LENGTH), + ), + BN254 => ( + groth16_verify_groth16_proof_internal_cost_params + .groth16_verify_groth16_proof_internal_bn254_cost_base, + groth16_verify_groth16_proof_internal_cost_params + .groth16_verify_groth16_proof_internal_bn254_cost_per_public_input, + public_proof_inputs + .len() + .div_ceil(fastcrypto_zkp::bn254::api::SCALAR_SIZE), + ), + _ => { + // Charge for failure but dont fail if we run out of gas otherwise the actual error is masked by OUT_OF_GAS error + context.charge_gas(crypto_invalid_arguments_cost); + let cost = if get_extension!(context, ObjectRuntime)? + .protocol_config + .native_charging_v2() + { + context.gas_used() + } else { + context.gas_budget() + }; + return Ok(NativeResult::err(cost, INVALID_CURVE)); + } + }; + // Charge the base cost for this oper + native_charge_gas_early_exit!(context, base_cost); + // Charge the arg size dependent costs + native_charge_gas_early_exit!( + context, + cost_per_public_input * (num_public_inputs as u64).into() + + groth16_verify_groth16_proof_internal_cost_params + .groth16_verify_groth16_proof_internal_public_input_cost_per_byte + * (public_proof_inputs.len() as u64).into() + ); + + let cost = context.gas_used(); + + let result; + if curve == BLS12381 { + if public_proof_inputs.len() + > fastcrypto::groups::bls12381::SCALAR_LENGTH * MAX_PUBLIC_INPUTS + { + return Ok(NativeResult::err(cost, TOO_MANY_PUBLIC_INPUTS)); + } + result = fastcrypto_zkp::bls12381::api::verify_groth16_in_bytes( + &vk_gamma_abc_g1, + &alpha_g1_beta_g2, + &gamma_g2_neg_pc, + &delta_g2_neg_pc, + &public_proof_inputs, + &proof_points, + ); + } else if curve == BN254 { + if public_proof_inputs.len() > fastcrypto_zkp::bn254::api::SCALAR_SIZE * MAX_PUBLIC_INPUTS { + return Ok(NativeResult::err(cost, TOO_MANY_PUBLIC_INPUTS)); + } + result = fastcrypto_zkp::bn254::api::verify_groth16_in_bytes( + &vk_gamma_abc_g1, + &alpha_g1_beta_g2, + &gamma_g2_neg_pc, + &delta_g2_neg_pc, + &public_proof_inputs, + &proof_points, + ); + } else { + return Ok(NativeResult::err(cost, INVALID_CURVE)); + } + + Ok(NativeResult::ok( + cost, + smallvec![Value::bool(result.unwrap_or(false))], + )) +} diff --git a/sui-execution/replay_cut/sui-move-natives/src/crypto/group_ops.rs b/sui-execution/replay_cut/sui-move-natives/src/crypto/group_ops.rs new file mode 100644 index 0000000000000..e201f12775b4b --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/crypto/group_ops.rs @@ -0,0 +1,897 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 +use crate::object_runtime::ObjectRuntime; +use crate::{NativesCostTable, get_extension}; +use fastcrypto::error::{FastCryptoError, FastCryptoResult}; +use fastcrypto::groups::{ + FromTrustedByteArray, GroupElement, HashToGroupElement, MultiScalarMul, Pairing, + bls12381 as bls, +}; +use fastcrypto::serde_helpers::ToFromByteArray; +use move_binary_format::errors::{PartialVMError, PartialVMResult}; +use move_core_types::gas_algebra::InternalGas; +use move_core_types::vm_status::StatusCode; +use move_vm_runtime::native_charge_gas_early_exit; +use move_vm_runtime::native_functions::NativeContext; +use move_vm_types::{ + loaded_data::runtime_types::Type, + natives::function::NativeResult, + pop_arg, + values::{Value, VectorRef}, +}; +use smallvec::smallvec; +use std::collections::VecDeque; + +pub const NOT_SUPPORTED_ERROR: u64 = 0; +pub const INVALID_INPUT_ERROR: u64 = 1; +pub const INPUT_TOO_LONG_ERROR: u64 = 2; + +fn is_supported(context: &NativeContext) -> PartialVMResult { + Ok(get_extension!(context, ObjectRuntime)? + .protocol_config + .enable_group_ops_native_functions()) +} + +fn is_msm_supported(context: &NativeContext) -> PartialVMResult { + Ok(get_extension!(context, ObjectRuntime)? + .protocol_config + .enable_group_ops_native_function_msm()) +} + +fn is_uncompressed_g1_supported(context: &NativeContext) -> PartialVMResult { + Ok(get_extension!(context, ObjectRuntime)? + .protocol_config + .uncompressed_g1_group_elements()) +} + +fn v2_native_charge(context: &NativeContext, cost: InternalGas) -> PartialVMResult { + Ok( + if get_extension!(context, ObjectRuntime)? + .protocol_config + .native_charging_v2() + { + context.gas_used() + } else { + cost + }, + ) +} + +fn map_op_result( + context: &NativeContext, + cost: InternalGas, + result: FastCryptoResult>, +) -> PartialVMResult { + match result { + Ok(bytes) => Ok(NativeResult::ok( + v2_native_charge(context, cost)?, + smallvec![Value::vector_u8(bytes)], + )), + // Since all Element are validated on construction, this error should never happen unless the requested type is wrong or inputs are invalid. + Err(_) => Ok(NativeResult::err( + v2_native_charge(context, cost)?, + INVALID_INPUT_ERROR, + )), + } +} + +// Gas related structs and functions. + +#[derive(Clone)] +pub struct GroupOpsCostParams { + // costs for decode and validate + pub bls12381_decode_scalar_cost: Option, + pub bls12381_decode_g1_cost: Option, + pub bls12381_decode_g2_cost: Option, + pub bls12381_decode_gt_cost: Option, + // costs for decode, add, and encode output + pub bls12381_scalar_add_cost: Option, + pub bls12381_g1_add_cost: Option, + pub bls12381_g2_add_cost: Option, + pub bls12381_gt_add_cost: Option, + // costs for decode, sub, and encode output + pub bls12381_scalar_sub_cost: Option, + pub bls12381_g1_sub_cost: Option, + pub bls12381_g2_sub_cost: Option, + pub bls12381_gt_sub_cost: Option, + // costs for decode, mul, and encode output + pub bls12381_scalar_mul_cost: Option, + pub bls12381_g1_mul_cost: Option, + pub bls12381_g2_mul_cost: Option, + pub bls12381_gt_mul_cost: Option, + // costs for decode, div, and encode output + pub bls12381_scalar_div_cost: Option, + pub bls12381_g1_div_cost: Option, + pub bls12381_g2_div_cost: Option, + pub bls12381_gt_div_cost: Option, + // costs for hashing + pub bls12381_g1_hash_to_base_cost: Option, + pub bls12381_g2_hash_to_base_cost: Option, + pub bls12381_g1_hash_to_cost_per_byte: Option, + pub bls12381_g2_hash_to_cost_per_byte: Option, + // costs for encoding the output + base cost for MSM (the |q| doublings) but not decoding + pub bls12381_g1_msm_base_cost: Option, + pub bls12381_g2_msm_base_cost: Option, + // cost that is multiplied with the approximated number of additions + pub bls12381_g1_msm_base_cost_per_input: Option, + pub bls12381_g2_msm_base_cost_per_input: Option, + // limit the length of the input vectors for MSM + pub bls12381_msm_max_len: Option, + // costs for decode, pairing, and encode output + pub bls12381_pairing_cost: Option, + // costs for conversion to and from uncompressed form + pub bls12381_g1_to_uncompressed_g1_cost: Option, + pub bls12381_uncompressed_g1_to_g1_cost: Option, + // costs for sum of elements uncompressed form + pub bls12381_uncompressed_g1_sum_base_cost: Option, + pub bls12381_uncompressed_g1_sum_cost_per_term: Option, + // limit the number of terms in a sum + pub bls12381_uncompressed_g1_sum_max_terms: Option, +} + +macro_rules! native_charge_gas_early_exit_option { + ($native_context:ident, $cost:expr) => {{ + use move_binary_format::errors::PartialVMError; + use move_core_types::vm_status::StatusCode; + native_charge_gas_early_exit!( + $native_context, + $cost.ok_or_else(|| { + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("Gas cost for group ops is missing".to_string()) + })? + ); + }}; +} + +// Next should be aligned with the related Move modules. +#[repr(u8)] +enum Groups { + BLS12381Scalar = 0, + BLS12381G1 = 1, + BLS12381G2 = 2, + BLS12381GT = 3, + BLS12381UncompressedG1 = 4, +} + +impl Groups { + fn from_u8(value: u8) -> Option { + match value { + 0 => Some(Groups::BLS12381Scalar), + 1 => Some(Groups::BLS12381G1), + 2 => Some(Groups::BLS12381G2), + 3 => Some(Groups::BLS12381GT), + 4 => Some(Groups::BLS12381UncompressedG1), + _ => None, + } + } +} + +fn parse_untrusted + FromTrustedByteArray, const S: usize>( + e: &[u8], +) -> FastCryptoResult { + G::from_byte_array(e.try_into().map_err(|_| FastCryptoError::InvalidInput)?) +} + +fn parse_trusted + FromTrustedByteArray, const S: usize>( + e: &[u8], +) -> FastCryptoResult { + G::from_trusted_byte_array(e.try_into().map_err(|_| FastCryptoError::InvalidInput)?) +} + +// Binary operations with 2 different types. +fn binary_op_diff< + G1: ToFromByteArray + FromTrustedByteArray, + G2: ToFromByteArray + FromTrustedByteArray, + const S1: usize, + const S2: usize, +>( + op: impl Fn(G1, G2) -> FastCryptoResult, + a1: &[u8], + a2: &[u8], +) -> FastCryptoResult> { + let e1 = parse_trusted::(a1)?; + let e2 = parse_trusted::(a2)?; + let result = op(e1, e2)?; + Ok(result.to_byte_array().to_vec()) +} + +// Binary operations with the same type. +fn binary_op + FromTrustedByteArray, const S: usize>( + op: impl Fn(G, G) -> FastCryptoResult, + a1: &[u8], + a2: &[u8], +) -> FastCryptoResult> { + binary_op_diff::(op, a1, a2) +} + +// TODO: Since in many cases more than one group operation will be performed in a single +// transaction, it might be worth caching the affine representation of the group elements and use +// them to save conversions. + +/*************************************************************************************************** + * native fun internal_validate + * Implementation of the Move native function `internal_validate(type: u8, bytes: &vector): bool` + * gas cost: group_ops_decode_bls12381_X_cost where X is the requested type + **************************************************************************************************/ + +pub fn internal_validate( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 2); + + let cost = context.gas_used(); + if !is_supported(context)? { + return Ok(NativeResult::err(cost, NOT_SUPPORTED_ERROR)); + } + + let bytes_ref = pop_arg!(args, VectorRef); + let bytes = bytes_ref.as_bytes_ref(); + let group_type = pop_arg!(args, u8); + + let cost_params = get_extension!(context, NativesCostTable)? + .group_ops_cost_params + .clone(); + + let result = match Groups::from_u8(group_type) { + Some(Groups::BLS12381Scalar) => { + native_charge_gas_early_exit_option!(context, cost_params.bls12381_decode_scalar_cost); + parse_untrusted::(&bytes).is_ok() + } + Some(Groups::BLS12381G1) => { + native_charge_gas_early_exit_option!(context, cost_params.bls12381_decode_g1_cost); + parse_untrusted::(&bytes).is_ok() + } + Some(Groups::BLS12381G2) => { + native_charge_gas_early_exit_option!(context, cost_params.bls12381_decode_g2_cost); + parse_untrusted::(&bytes).is_ok() + } + _ => false, + }; + + Ok(NativeResult::ok( + v2_native_charge(context, cost)?, + smallvec![Value::bool(result)], + )) +} + +/*************************************************************************************************** + * native fun internal_add + * Implementation of the Move native function `internal_add(type: u8, e1: &vector, e2: &vector): vector` + * gas cost: group_ops_bls12381_X_add_cost where X is the requested type + **************************************************************************************************/ +pub fn internal_add( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 3); + + let cost = context.gas_used(); + if !is_supported(context)? { + return Ok(NativeResult::err(cost, NOT_SUPPORTED_ERROR)); + } + + let e2_ref = pop_arg!(args, VectorRef); + let e2 = e2_ref.as_bytes_ref(); + let e1_ref = pop_arg!(args, VectorRef); + let e1 = e1_ref.as_bytes_ref(); + let group_type = pop_arg!(args, u8); + + let cost_params = get_extension!(context, NativesCostTable)? + .group_ops_cost_params + .clone(); + + let result = match Groups::from_u8(group_type) { + Some(Groups::BLS12381Scalar) => { + native_charge_gas_early_exit_option!(context, cost_params.bls12381_scalar_add_cost); + binary_op::(|a, b| Ok(a + b), &e1, &e2) + } + Some(Groups::BLS12381G1) => { + native_charge_gas_early_exit_option!(context, cost_params.bls12381_g1_add_cost); + binary_op::(|a, b| Ok(a + b), &e1, &e2) + } + Some(Groups::BLS12381G2) => { + native_charge_gas_early_exit_option!(context, cost_params.bls12381_g2_add_cost); + binary_op::(|a, b| Ok(a + b), &e1, &e2) + } + Some(Groups::BLS12381GT) => { + native_charge_gas_early_exit_option!(context, cost_params.bls12381_gt_add_cost); + binary_op::(|a, b| Ok(a + b), &e1, &e2) + } + _ => Err(FastCryptoError::InvalidInput), + }; + + map_op_result(context, cost, result) +} + +/*************************************************************************************************** + * native fun internal_sub + * Implementation of the Move native function `internal_sub(type: u8, e1: &vector, e2: &vector): vector` + * gas cost: group_ops_bls12381_X_sub_cost where X is the requested type + **************************************************************************************************/ +pub fn internal_sub( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 3); + + let cost = context.gas_used(); + if !is_supported(context)? { + return Ok(NativeResult::err(cost, NOT_SUPPORTED_ERROR)); + } + + let e2_ref = pop_arg!(args, VectorRef); + let e2 = e2_ref.as_bytes_ref(); + let e1_ref = pop_arg!(args, VectorRef); + let e1 = e1_ref.as_bytes_ref(); + let group_type = pop_arg!(args, u8); + + let cost_params = get_extension!(context, NativesCostTable)? + .group_ops_cost_params + .clone(); + + let result = match Groups::from_u8(group_type) { + Some(Groups::BLS12381Scalar) => { + native_charge_gas_early_exit_option!(context, cost_params.bls12381_scalar_sub_cost); + binary_op::(|a, b| Ok(a - b), &e1, &e2) + } + Some(Groups::BLS12381G1) => { + native_charge_gas_early_exit_option!(context, cost_params.bls12381_g1_sub_cost); + binary_op::(|a, b| Ok(a - b), &e1, &e2) + } + Some(Groups::BLS12381G2) => { + native_charge_gas_early_exit_option!(context, cost_params.bls12381_g2_sub_cost); + binary_op::(|a, b| Ok(a - b), &e1, &e2) + } + Some(Groups::BLS12381GT) => { + native_charge_gas_early_exit_option!(context, cost_params.bls12381_gt_sub_cost); + binary_op::(|a, b| Ok(a - b), &e1, &e2) + } + _ => Err(FastCryptoError::InvalidInput), + }; + + map_op_result(context, cost, result) +} + +/*************************************************************************************************** + * native fun internal_mul + * Implementation of the Move native function `internal_mul(type: u8, e1: &vector, e2: &vector): vector` + * gas cost: group_ops_bls12381_X_mul_cost where X is the requested type + **************************************************************************************************/ +pub fn internal_mul( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 3); + + let cost = context.gas_used(); + if !is_supported(context)? { + return Ok(NativeResult::err(cost, NOT_SUPPORTED_ERROR)); + } + + let e2_ref = pop_arg!(args, VectorRef); + let e2 = e2_ref.as_bytes_ref(); + let e1_ref = pop_arg!(args, VectorRef); + let e1 = e1_ref.as_bytes_ref(); + let group_type = pop_arg!(args, u8); + + let cost_params = get_extension!(context, NativesCostTable)? + .group_ops_cost_params + .clone(); + + let result = match Groups::from_u8(group_type) { + Some(Groups::BLS12381Scalar) => { + native_charge_gas_early_exit_option!(context, cost_params.bls12381_scalar_mul_cost); + binary_op::(|a, b| Ok(b * a), &e1, &e2) + } + Some(Groups::BLS12381G1) => { + native_charge_gas_early_exit_option!(context, cost_params.bls12381_g1_mul_cost); + binary_op_diff::< + bls::Scalar, + bls::G1Element, + { bls::Scalar::BYTE_LENGTH }, + { bls::G1Element::BYTE_LENGTH }, + >(|a, b| Ok(b * a), &e1, &e2) + } + Some(Groups::BLS12381G2) => { + native_charge_gas_early_exit_option!(context, cost_params.bls12381_g2_mul_cost); + binary_op_diff::< + bls::Scalar, + bls::G2Element, + { bls::Scalar::BYTE_LENGTH }, + { bls::G2Element::BYTE_LENGTH }, + >(|a, b| Ok(b * a), &e1, &e2) + } + Some(Groups::BLS12381GT) => { + native_charge_gas_early_exit_option!(context, cost_params.bls12381_gt_mul_cost); + binary_op_diff::< + bls::Scalar, + bls::GTElement, + { bls::Scalar::BYTE_LENGTH }, + { bls::GTElement::BYTE_LENGTH }, + >(|a, b| Ok(b * a), &e1, &e2) + } + _ => Err(FastCryptoError::InvalidInput), + }; + + map_op_result(context, cost, result) +} + +/*************************************************************************************************** + * native fun internal_div + * Implementation of the Move native function `internal_div(type: u8, e1: &vector, e2: &vector): vector` + * gas cost: group_ops_bls12381_X_div_cost where X is the requested type + **************************************************************************************************/ +pub fn internal_div( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 3); + + let cost = context.gas_used(); + if !is_supported(context)? { + return Ok(NativeResult::err(cost, NOT_SUPPORTED_ERROR)); + } + + let e2_ref = pop_arg!(args, VectorRef); + let e2 = e2_ref.as_bytes_ref(); + let e1_ref = pop_arg!(args, VectorRef); + let e1 = e1_ref.as_bytes_ref(); + let group_type = pop_arg!(args, u8); + + let cost_params = get_extension!(context, NativesCostTable)? + .group_ops_cost_params + .clone(); + + let result = match Groups::from_u8(group_type) { + Some(Groups::BLS12381Scalar) => { + native_charge_gas_early_exit_option!(context, cost_params.bls12381_scalar_div_cost); + binary_op::(|a, b| b / a, &e1, &e2) + } + Some(Groups::BLS12381G1) => { + native_charge_gas_early_exit_option!(context, cost_params.bls12381_g1_div_cost); + binary_op_diff::< + bls::Scalar, + bls::G1Element, + { bls::Scalar::BYTE_LENGTH }, + { bls::G1Element::BYTE_LENGTH }, + >(|a, b| b / a, &e1, &e2) + } + Some(Groups::BLS12381G2) => { + native_charge_gas_early_exit_option!(context, cost_params.bls12381_g2_div_cost); + binary_op_diff::< + bls::Scalar, + bls::G2Element, + { bls::Scalar::BYTE_LENGTH }, + { bls::G2Element::BYTE_LENGTH }, + >(|a, b| b / a, &e1, &e2) + } + Some(Groups::BLS12381GT) => { + native_charge_gas_early_exit_option!(context, cost_params.bls12381_gt_div_cost); + binary_op_diff::< + bls::Scalar, + bls::GTElement, + { bls::Scalar::BYTE_LENGTH }, + { bls::GTElement::BYTE_LENGTH }, + >(|a, b| b / a, &e1, &e2) + } + _ => Err(FastCryptoError::InvalidInput), + }; + + map_op_result(context, cost, result) +} + +/*************************************************************************************************** + * native fun internal_hash_to + * Implementation of the Move native function `internal_hash_to(type: u8, m: &vector): vector` + * gas cost: group_ops_bls12381_X_hash_to_base_cost + group_ops_bls12381_X_hash_to_cost_per_byte * |input| + * where X is the requested type + **************************************************************************************************/ +pub fn internal_hash_to( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 2); + + let cost = context.gas_used(); + if !is_supported(context)? { + return Ok(NativeResult::err(cost, NOT_SUPPORTED_ERROR)); + } + + let m_ref = pop_arg!(args, VectorRef); + let m = m_ref.as_bytes_ref(); + let group_type = pop_arg!(args, u8); + + if m.is_empty() { + return Ok(NativeResult::err(cost, INVALID_INPUT_ERROR)); + } + + let cost_params = get_extension!(context, NativesCostTable)? + .group_ops_cost_params + .clone(); + + let result = match Groups::from_u8(group_type) { + Some(Groups::BLS12381G1) => { + native_charge_gas_early_exit_option!( + context, + cost_params + .bls12381_g1_hash_to_base_cost + .and_then(|base_cost| cost_params + .bls12381_g1_hash_to_cost_per_byte + .map(|per_byte| base_cost + per_byte * (m.len() as u64).into())) + ); + Ok(bls::G1Element::hash_to_group_element(&m) + .to_byte_array() + .to_vec()) + } + Some(Groups::BLS12381G2) => { + native_charge_gas_early_exit_option!( + context, + cost_params + .bls12381_g2_hash_to_base_cost + .and_then(|base_cost| cost_params + .bls12381_g2_hash_to_cost_per_byte + .map(|per_byte| base_cost + per_byte * (m.len() as u64).into())) + ); + Ok(bls::G2Element::hash_to_group_element(&m) + .to_byte_array() + .to_vec()) + } + _ => Err(FastCryptoError::InvalidInput), + }; + + map_op_result(context, cost, result) +} + +// Based on calculation from https://github.com/supranational/blst/blob/master/src/multi_scalar.c#L270 +fn msm_num_of_additions(n: u64) -> u64 { + debug_assert!(n > 0); + let wbits = (64 - n.leading_zeros() - 1) as u64; + let window_size = match wbits { + 0 => 1, + 1..=4 => 2, + 5..=12 => wbits - 2, + _ => wbits - 3, + }; + let num_of_windows = 255 / window_size + if 255 % window_size == 0 { 0 } else { 1 }; + num_of_windows * (n + (1 << window_size) + 1) +} + +#[test] +fn test_msm_factor() { + assert_eq!(msm_num_of_additions(1), 1020); + assert_eq!(msm_num_of_additions(2), 896); + assert_eq!(msm_num_of_additions(3), 1024); + assert_eq!(msm_num_of_additions(4), 1152); + assert_eq!(msm_num_of_additions(32), 3485); +} + +fn multi_scalar_mul( + context: &mut NativeContext, + scalar_decode_cost: Option, + point_decode_cost: Option, + base_cost: Option, + base_cost_per_addition: Option, + max_len: u32, + scalars: &[u8], + points: &[u8], +) -> PartialVMResult +where + G: GroupElement + + ToFromByteArray + + FromTrustedByteArray + + MultiScalarMul, + G::ScalarType: ToFromByteArray + FromTrustedByteArray, +{ + if points.is_empty() + || scalars.is_empty() + || !scalars.len().is_multiple_of(SCALAR_SIZE) + || !points.len().is_multiple_of(POINT_SIZE) + || points.len() / POINT_SIZE != scalars.len() / SCALAR_SIZE + { + return Ok(NativeResult::err(context.gas_used(), INVALID_INPUT_ERROR)); + } + + if points.len() / POINT_SIZE > max_len as usize { + return Ok(NativeResult::err(context.gas_used(), INPUT_TOO_LONG_ERROR)); + } + + native_charge_gas_early_exit_option!( + context, + scalar_decode_cost.map(|cost| cost * ((scalars.len() / SCALAR_SIZE) as u64).into()) + ); + let scalars = scalars + .chunks(SCALAR_SIZE) + .map(parse_trusted::) + .collect::, _>>(); + + native_charge_gas_early_exit_option!( + context, + point_decode_cost.map(|cost| cost * ((points.len() / POINT_SIZE) as u64).into()) + ); + let points = points + .chunks(POINT_SIZE) + .map(parse_trusted::) + .collect::, _>>(); + + if let (Ok(scalars), Ok(points)) = (scalars, points) { + // Checked above that len()>0 + let num_of_additions = msm_num_of_additions(scalars.len() as u64); + native_charge_gas_early_exit_option!( + context, + base_cost.and_then(|base| base_cost_per_addition + .map(|per_addition| base + per_addition * num_of_additions.into())) + ); + + let r = G::multi_scalar_mul(&scalars, &points) + .expect("Already checked the lengths of the vectors"); + Ok(NativeResult::ok( + context.gas_used(), + smallvec![Value::vector_u8(r.to_byte_array().to_vec())], + )) + } else { + Ok(NativeResult::err(context.gas_used(), INVALID_INPUT_ERROR)) + } +} + +/*************************************************************************************************** + * native fun internal_multi_scalar_mul + * Implementation of the Move native function `internal_multi_scalar_mul(type: u8, scalars: &vector, elements: &vector): vector` + * gas cost: (bls12381_decode_scalar_cost + bls12381_decode_X_cost) * N + bls12381_X_msm_base_cost + + * bls12381_X_msm_base_cost_per_input * num_of_additions(N) + **************************************************************************************************/ +pub fn internal_multi_scalar_mul( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 3); + + let cost = context.gas_used(); + if !is_msm_supported(context)? { + return Ok(NativeResult::err(cost, NOT_SUPPORTED_ERROR)); + } + + let elements_ref = pop_arg!(args, VectorRef); + let elements = elements_ref.as_bytes_ref(); + let scalars_ref = pop_arg!(args, VectorRef); + let scalars = scalars_ref.as_bytes_ref(); + let group_type = pop_arg!(args, u8); + + let cost_params = get_extension!(context, NativesCostTable)? + .group_ops_cost_params + .clone(); + + let max_len = cost_params.bls12381_msm_max_len.ok_or_else(|| { + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("Max len for MSM is not set".to_string()) + })?; + + // TODO: can potentially improve performance when some of the points are the generator. + match Groups::from_u8(group_type) { + Some(Groups::BLS12381G1) => multi_scalar_mul::< + bls::G1Element, + { bls::Scalar::BYTE_LENGTH }, + { bls::G1Element::BYTE_LENGTH }, + >( + context, + cost_params.bls12381_decode_scalar_cost, + cost_params.bls12381_decode_g1_cost, + cost_params.bls12381_g1_msm_base_cost, + cost_params.bls12381_g1_msm_base_cost_per_input, + max_len, + scalars.as_ref(), + elements.as_ref(), + ), + Some(Groups::BLS12381G2) => multi_scalar_mul::< + bls::G2Element, + { bls::Scalar::BYTE_LENGTH }, + { bls::G2Element::BYTE_LENGTH }, + >( + context, + cost_params.bls12381_decode_scalar_cost, + cost_params.bls12381_decode_g2_cost, + cost_params.bls12381_g2_msm_base_cost, + cost_params.bls12381_g2_msm_base_cost_per_input, + max_len, + scalars.as_ref(), + elements.as_ref(), + ), + _ => Ok(NativeResult::err( + v2_native_charge(context, cost)?, + INVALID_INPUT_ERROR, + )), + } +} + +/*************************************************************************************************** + * native fun internal_pairing + * Implementation of the Move native function `internal_pairing(type:u8, e1: &vector, e2: &vector): vector` + * gas cost: group_ops_bls12381_pairing_cost + **************************************************************************************************/ +pub fn internal_pairing( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 3); + + let cost = context.gas_used(); + if !is_supported(context)? { + return Ok(NativeResult::err(cost, NOT_SUPPORTED_ERROR)); + } + + let e2_ref = pop_arg!(args, VectorRef); + let e2 = e2_ref.as_bytes_ref(); + let e1_ref = pop_arg!(args, VectorRef); + let e1 = e1_ref.as_bytes_ref(); + let group_type = pop_arg!(args, u8); + + let cost_params = get_extension!(context, NativesCostTable)? + .group_ops_cost_params + .clone(); + + let result = match Groups::from_u8(group_type) { + Some(Groups::BLS12381G1) => { + native_charge_gas_early_exit_option!(context, cost_params.bls12381_pairing_cost); + parse_trusted::(&e1).and_then(|e1| { + parse_trusted::(&e2).map(|e2| { + let e3 = e1.pairing(&e2); + e3.to_byte_array().to_vec() + }) + }) + } + _ => Err(FastCryptoError::InvalidInput), + }; + + map_op_result(context, cost, result) +} + +/*************************************************************************************************** + * native fun internal_convert + * Implementation of the Move native function `internal_convert(from_type:u8, to_type: u8, e: &vector): vector` + * gas cost: group_ops_bls12381_g1_from_uncompressed_cost / group_ops_bls12381_g1_from_compressed_cost + **************************************************************************************************/ +pub fn internal_convert( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 3); + + let cost = context.gas_used(); + + if !(is_uncompressed_g1_supported(context))? { + return Ok(NativeResult::err(cost, NOT_SUPPORTED_ERROR)); + } + + let e_ref = pop_arg!(args, VectorRef); + let e = e_ref.as_bytes_ref(); + let to_type = pop_arg!(args, u8); + let from_type = pop_arg!(args, u8); + + let cost_params = get_extension!(context, NativesCostTable)? + .group_ops_cost_params + .clone(); + + let result = match (Groups::from_u8(from_type), Groups::from_u8(to_type)) { + (Some(Groups::BLS12381UncompressedG1), Some(Groups::BLS12381G1)) => { + native_charge_gas_early_exit_option!( + context, + cost_params.bls12381_uncompressed_g1_to_g1_cost + ); + e.to_vec() + .try_into() + .map_err(|_| FastCryptoError::InvalidInput) + .map(bls::G1ElementUncompressed::from_trusted_byte_array) + .and_then(|e| bls::G1Element::try_from(&e)) + .map(|e| e.to_byte_array().to_vec()) + } + (Some(Groups::BLS12381G1), Some(Groups::BLS12381UncompressedG1)) => { + native_charge_gas_early_exit_option!( + context, + cost_params.bls12381_g1_to_uncompressed_g1_cost + ); + parse_trusted::(&e) + .map(|e| bls::G1ElementUncompressed::from(&e)) + .map(|e| e.into_byte_array().to_vec()) + } + _ => Err(FastCryptoError::InvalidInput), + }; + + map_op_result(context, cost, result) +} + +/*************************************************************************************************** + * native fun internal_sum + * Implementation of the Move native function `internal_sum(type:u8, terms: &vector>): vector` + * gas cost: group_ops_bls12381_g1_sum_of_uncompressed_base_cost + len(terms) * group_ops_bls12381_g1_sum_of_uncompressed_cost_per_term + **************************************************************************************************/ +pub fn internal_sum( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 2); + + let cost = context.gas_used(); + + if !(is_uncompressed_g1_supported(context))? { + return Ok(NativeResult::err(cost, NOT_SUPPORTED_ERROR)); + } + + let cost_params = get_extension!(context, NativesCostTable)? + .group_ops_cost_params + .clone(); + + // The input is a reference to a vector of vector's + let inputs = pop_arg!(args, VectorRef); + let group_type = pop_arg!(args, u8); + + let length = inputs + .len(&Type::Vector(Box::new(Type::U8)))? + .value_as::()?; + + let result = match Groups::from_u8(group_type) { + Some(Groups::BLS12381UncompressedG1) => { + let max_terms = cost_params + .bls12381_uncompressed_g1_sum_max_terms + .ok_or_else(|| { + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("Max number of terms is not set".to_string()) + })?; + + if length > max_terms { + return Ok(NativeResult::err(cost, INPUT_TOO_LONG_ERROR)); + } + + native_charge_gas_early_exit_option!( + context, + cost_params + .bls12381_uncompressed_g1_sum_base_cost + .and_then(|base| cost_params + .bls12381_uncompressed_g1_sum_cost_per_term + .map(|per_term| base + per_term * length.into())) + ); + + // Read the input vector + (0..length) + .map(|i| { + inputs + .borrow_elem(i as usize, &Type::Vector(Box::new(Type::U8))) + .and_then(Value::value_as::) + .map_err(|_| FastCryptoError::InvalidInput) + .and_then(|v| { + v.as_bytes_ref() + .to_vec() + .try_into() + .map_err(|_| FastCryptoError::InvalidInput) + }) + .map(bls::G1ElementUncompressed::from_trusted_byte_array) + }) + .collect::>>() + .and_then(|e| bls::G1ElementUncompressed::sum(&e)) + .map(|e| bls::G1ElementUncompressed::from(&e)) + .map(|e| e.into_byte_array().to_vec()) + } + _ => Err(FastCryptoError::InvalidInput), + }; + + map_op_result(context, cost, result) +} diff --git a/sui-execution/replay_cut/sui-move-natives/src/crypto/hash.rs b/sui-execution/replay_cut/sui-move-natives/src/crypto/hash.rs new file mode 100644 index 0000000000000..2e8802589e8e2 --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/crypto/hash.rs @@ -0,0 +1,134 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 +use crate::{NativesCostTable, get_extension}; +use fastcrypto::hash::{Blake2b256, HashFunction, Keccak256}; +use move_binary_format::errors::PartialVMResult; +use move_core_types::gas_algebra::InternalGas; +use move_vm_runtime::{native_charge_gas_early_exit, native_functions::NativeContext}; +use move_vm_types::{ + loaded_data::runtime_types::Type, + natives::function::NativeResult, + pop_arg, + values::{Value, VectorRef}, +}; +use smallvec::smallvec; +use std::{collections::VecDeque, ops::Mul}; + +const BLAKE_2B256_BLOCK_SIZE: u16 = 128; +const KECCAK_256_BLOCK_SIZE: u16 = 136; + +fn hash, const DIGEST_SIZE: usize>( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, + // The caller provides the cost per byte + msg_cost_per_byte: InternalGas, + // The caller provides the cost per block + msg_cost_per_block: InternalGas, + // The caller specifies the block size + block_size: u16, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 1); + + let msg = pop_arg!(args, VectorRef); + let msg_ref = msg.as_bytes_ref(); + + let block_size = block_size as usize; + + // Charge the msg dependent costs + native_charge_gas_early_exit!( + context, + msg_cost_per_byte.mul((msg_ref.len() as u64).into()) + // Round up the blocks + + msg_cost_per_block + .mul((msg_ref.len().div_ceil(block_size) as u64).into()) + ); + + Ok(NativeResult::ok( + context.gas_used(), + smallvec![Value::vector_u8( + H::digest(msg.as_bytes_ref().as_slice()).digest + )], + )) +} + +#[derive(Clone)] +pub struct HashKeccak256CostParams { + /// Base cost for invoking the `blake2b256` function + pub hash_keccak256_cost_base: InternalGas, + /// Cost per byte of `data` + pub hash_keccak256_data_cost_per_byte: InternalGas, + /// Cost per block of `data`, where a block is 136 bytes + pub hash_keccak256_data_cost_per_block: InternalGas, +} + +/*************************************************************************************************** + * native fun keccak256 + * Implementation of the Move native function `hash::keccak256(data: &vector): vector` + * gas cost: hash_keccak256_cost_base | base cost for function call and fixed opers + * + hash_keccak256_data_cost_per_byte * msg.len() | cost depends on length of message + * + hash_keccak256_data_cost_per_block * num_blocks | cost depends on number of blocks in message + **************************************************************************************************/ +pub fn keccak256( + context: &mut NativeContext, + ty_args: Vec, + args: VecDeque, +) -> PartialVMResult { + // Load the cost parameters from the protocol config + let hash_keccak256_cost_params = get_extension!(context, NativesCostTable)? + .hash_keccak256_cost_params + .clone(); + // Charge the base cost for this oper + native_charge_gas_early_exit!(context, hash_keccak256_cost_params.hash_keccak256_cost_base); + + hash::( + context, + ty_args, + args, + hash_keccak256_cost_params.hash_keccak256_data_cost_per_byte, + hash_keccak256_cost_params.hash_keccak256_data_cost_per_block, + KECCAK_256_BLOCK_SIZE, + ) +} + +#[derive(Clone)] +pub struct HashBlake2b256CostParams { + /// Base cost for invoking the `blake2b256` function + pub hash_blake2b256_cost_base: InternalGas, + /// Cost per byte of `data` + pub hash_blake2b256_data_cost_per_byte: InternalGas, + /// Cost per block of `data`, where a block is 128 bytes + pub hash_blake2b256_data_cost_per_block: InternalGas, +} +/*************************************************************************************************** + * native fun blake2b256 + * Implementation of the Move native function `hash::blake2b256(data: &vector): vector` + * gas cost: hash_blake2b256_cost_base | base cost for function call and fixed opers + * + hash_blake2b256_data_cost_per_byte * msg.len() | cost depends on length of message + * + hash_blake2b256_data_cost_per_block * num_blocks | cost depends on number of blocks in message + **************************************************************************************************/ +pub fn blake2b256( + context: &mut NativeContext, + ty_args: Vec, + args: VecDeque, +) -> PartialVMResult { + // Load the cost parameters from the protocol config + let hash_blake2b256_cost_params = get_extension!(context, NativesCostTable)? + .hash_blake2b256_cost_params + .clone(); + // Charge the base cost for this oper + native_charge_gas_early_exit!( + context, + hash_blake2b256_cost_params.hash_blake2b256_cost_base + ); + + hash::( + context, + ty_args, + args, + hash_blake2b256_cost_params.hash_blake2b256_data_cost_per_byte, + hash_blake2b256_cost_params.hash_blake2b256_data_cost_per_block, + BLAKE_2B256_BLOCK_SIZE, + ) +} diff --git a/sui-execution/replay_cut/sui-move-natives/src/crypto/hmac.rs b/sui-execution/replay_cut/sui-move-natives/src/crypto/hmac.rs new file mode 100644 index 0000000000000..fa136b6ee23b6 --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/crypto/hmac.rs @@ -0,0 +1,83 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 +use crate::{NativesCostTable, get_extension}; +use fastcrypto::{hmac, traits::ToFromBytes}; +use move_binary_format::errors::PartialVMResult; +use move_core_types::gas_algebra::InternalGas; +use move_vm_runtime::{native_charge_gas_early_exit, native_functions::NativeContext}; +use move_vm_types::{ + loaded_data::runtime_types::Type, + natives::function::NativeResult, + pop_arg, + values::{Value, VectorRef}, +}; +use smallvec::smallvec; +use std::collections::VecDeque; + +const HMAC_SHA3_256_BLOCK_SIZE: usize = 136; + +#[derive(Clone)] +pub struct HmacHmacSha3256CostParams { + /// Base cost for invoking the `hmac_sha3_256` function + pub hmac_hmac_sha3_256_cost_base: InternalGas, + /// Cost per byte of `msg` and `key` + pub hmac_hmac_sha3_256_input_cost_per_byte: InternalGas, + /// Cost per block of `msg` and `key`, with block size = 136 + pub hmac_hmac_sha3_256_input_cost_per_block: InternalGas, +} +/*************************************************************************************************** + * native fun ed25519_verify + * Implementation of the Move native function `hmac_sha3_256(key: &vector, msg: &vector): vector;` + * gas cost: hmac_hmac_sha3_256_cost_base | base cost for function call and fixed opers + * + hmac_hmac_sha3_256_input_cost_per_byte * msg.len() | cost depends on length of message + * + hmac_hmac_sha3_256_input_cost_per_block * num_blocks(msg) | cost depends on number of blocks in message + * Note: each block is of size `HMAC_SHA3_256_BLOCK_SIZE` bytes, and we round up. + * `key` is fixed size, so the cost is included in the base cost. + **************************************************************************************************/ +pub fn hmac_sha3_256( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 2); + + // Load the cost parameters from the protocol config + let hmac_hmac_sha3_256_cost_params = get_extension!(context, NativesCostTable)? + .hmac_hmac_sha3_256_cost_params + .clone(); + + // Charge the base cost for this operation + native_charge_gas_early_exit!( + context, + hmac_hmac_sha3_256_cost_params.hmac_hmac_sha3_256_cost_base + ); + + let message = pop_arg!(args, VectorRef); + let key = pop_arg!(args, VectorRef); + + let msg_len = message.as_bytes_ref().len(); + let key_len = key.as_bytes_ref().len(); + // Charge the arg size dependent costs + native_charge_gas_early_exit!( + context, + hmac_hmac_sha3_256_cost_params.hmac_hmac_sha3_256_input_cost_per_byte + // same cost for msg and key + * ((msg_len + key_len) as u64).into() + + hmac_hmac_sha3_256_cost_params.hmac_hmac_sha3_256_input_cost_per_block + * ((((msg_len + key_len) + (2 * HMAC_SHA3_256_BLOCK_SIZE - 2)) + / HMAC_SHA3_256_BLOCK_SIZE) as u64) + .into() + ); + + let hmac_key = hmac::HmacKey::from_bytes(&key.as_bytes_ref()) + .expect("HMAC key can be of any length and from_bytes should always succeed"); + let cost = context.gas_used(); + + Ok(NativeResult::ok( + cost, + smallvec![Value::vector_u8( + hmac::hmac_sha3_256(&hmac_key, &message.as_bytes_ref()).to_vec() + )], + )) +} diff --git a/sui-execution/replay_cut/sui-move-natives/src/crypto/mod.rs b/sui-execution/replay_cut/sui-move-natives/src/crypto/mod.rs new file mode 100644 index 0000000000000..6303f68c5a05b --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/crypto/mod.rs @@ -0,0 +1,16 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +pub mod bls12381; +pub mod ecdsa_k1; +pub mod ecdsa_r1; +pub mod ecvrf; +pub mod ed25519; +pub mod groth16; +pub mod group_ops; +pub mod hash; +pub mod hmac; +pub mod nitro_attestation; +pub mod poseidon; +pub mod vdf; +pub mod zklogin; diff --git a/sui-execution/replay_cut/sui-move-natives/src/crypto/nitro_attestation.rs b/sui-execution/replay_cut/sui-move-natives/src/crypto/nitro_attestation.rs new file mode 100644 index 0000000000000..875e43d19d688 --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/crypto/nitro_attestation.rs @@ -0,0 +1,174 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use move_binary_format::errors::{PartialVMError, PartialVMResult}; +use move_core_types::{gas_algebra::InternalGas, vm_status::StatusCode}; +use move_vm_runtime::native_functions::NativeContext; +use move_vm_types::{ + loaded_data::runtime_types::Type, + natives::function::NativeResult, + pop_arg, + values::{Struct, Value, Vector, VectorRef, VectorSpecialization}, +}; +use std::collections::{BTreeMap, VecDeque}; +use sui_types::nitro_attestation::{parse_nitro_attestation, verify_nitro_attestation}; + +use crate::{NativesCostTable, get_extension, object_runtime::ObjectRuntime}; +use move_vm_runtime::native_charge_gas_early_exit; + +pub const NOT_SUPPORTED_ERROR: u64 = 0; +pub const PARSE_ERROR: u64 = 1; +pub const VERIFY_ERROR: u64 = 2; +pub const INVALID_PCRS_ERROR: u64 = 3; + +// Gas related structs and functions. +#[derive(Clone)] +pub struct NitroAttestationCostParams { + pub parse_base_cost: Option, + pub parse_cost_per_byte: Option, + pub verify_base_cost: Option, + pub verify_cost_per_cert: Option, +} + +macro_rules! native_charge_gas_early_exit_option { + ($native_context:ident, $cost:expr) => {{ + use move_binary_format::errors::PartialVMError; + use move_core_types::vm_status::StatusCode; + native_charge_gas_early_exit!( + $native_context, + $cost.ok_or_else(|| { + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("Gas cost for nitro attestation is missing".to_string()) + })? + ); + }}; +} + +fn is_supported(context: &NativeContext) -> PartialVMResult { + Ok(get_extension!(context, ObjectRuntime)? + .protocol_config + .enable_nitro_attestation()) +} + +fn is_upgraded(context: &NativeContext) -> PartialVMResult { + Ok(get_extension!(context, ObjectRuntime)? + .protocol_config + .enable_nitro_attestation_upgraded_parsing()) +} + +pub fn load_nitro_attestation_internal( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 2); + + let cost = context.gas_used(); + if !is_supported(context)? { + return Ok(NativeResult::err(cost, NOT_SUPPORTED_ERROR)); + } + + let current_timestamp = pop_arg!(args, u64); + let attestation_ref = pop_arg!(args, VectorRef); + let attestation_bytes = attestation_ref.as_bytes_ref(); + + let cost_params = get_extension!(context, NativesCostTable)? + .nitro_attestation_cost_params + .clone(); + + native_charge_gas_early_exit_option!( + context, + cost_params.parse_base_cost.and_then(|base_cost| cost_params + .parse_cost_per_byte + .map(|per_byte| base_cost + per_byte * (attestation_bytes.len() as u64).into())) + ); + + match parse_nitro_attestation(&attestation_bytes, is_upgraded(context)?) { + Ok((signature, signed_message, payload)) => { + let cert_chain_length = payload.get_cert_chain_length(); + native_charge_gas_early_exit_option!( + context, + cost_params + .verify_base_cost + .and_then(|base_cost| cost_params + .verify_cost_per_cert + .map(|per_cert| base_cost + per_cert * (cert_chain_length as u64).into())) + ); + match verify_nitro_attestation(&signature, &signed_message, &payload, current_timestamp) + { + Ok(()) => { + let pcrs = if is_upgraded(context)? { + to_indexed_struct(payload.pcr_map)? + } else { + to_indexed_struct_legacy(payload.pcr_vec)? + }; + // Encapsulate as a lambda and call to allow us to capture any `Err` returns. + // Could do this with `and_then` as well if desired. + let result = || { + Ok(Value::struct_(Struct::pack(vec![ + Value::vector_u8(payload.module_id.as_bytes().to_vec()), + Value::u64(payload.timestamp), + Value::vector_u8(payload.digest.as_bytes().to_vec()), + pcrs, + to_option_vector_u8(payload.public_key)?, + to_option_vector_u8(payload.user_data)?, + to_option_vector_u8(payload.nonce)?, + ]))) + }; + NativeResult::map_partial_vm_result_one(context.gas_used(), result()) + } + Err(_) => Ok(NativeResult::err(context.gas_used(), VERIFY_ERROR)), + } + } + Err(_) => Ok(NativeResult::err(context.gas_used(), PARSE_ERROR)), + } +} +// Build an Option> value +fn to_option_vector_u8(value: Option>) -> PartialVMResult { + match value { + // Some(>) = { vector[ > ] } + Some(vec) => Ok(Value::struct_(Struct::pack(vec![Vector::pack( + VectorSpecialization::Container, + vec![Value::vector_u8(vec)], + )?]))), + // None = { vector[ ] } + None => Ok(Value::struct_(Struct::pack(vec![Vector::empty( + VectorSpecialization::Container, + )?]))), + } +} + +// Convert a map of index -> PCR to a vector of PCREntry struct with index +// and value where the indices are [0, 1, 2, 3, 4, 8] since AWS currently +// supports PCR0, PCR1, PCR2, PCR3, PCR4, PCR8. +fn to_indexed_struct(pcrs: BTreeMap>) -> PartialVMResult { + let mut sorted = pcrs.iter().collect::>(); + sorted.sort_by_key(|(key, _)| *key); + let mut indexed_struct = vec![]; + for (index, pcr) in sorted.into_iter() { + indexed_struct.push(Value::struct_(Struct::pack(vec![ + Value::u8(*index), + Value::vector_u8(pcr.to_vec()), + ]))); + } + Vector::pack(VectorSpecialization::Container, indexed_struct) +} + +// Convert a list of PCRs into a vector of PCREntry struct with index and value, +// where the indices are [0, 1, 2, 3, 4, 8] since AWS currently supports PCR0, +// PCR1, PCR2, PCR3, PCR4, PCR8. +fn to_indexed_struct_legacy(pcrs: Vec>) -> PartialVMResult { + let indices = [0, 1, 2, 3, 4, 8]; + if pcrs.len() != indices.len() { + return Err(PartialVMError::new(StatusCode::ABORTED).with_sub_status(INVALID_PCRS_ERROR)); + }; + let mut indexed_struct = vec![]; + for (index, pcr) in pcrs.iter().enumerate() { + indexed_struct.push(Value::struct_(Struct::pack(vec![ + Value::u8(indices[index]), + Value::vector_u8(pcr.to_vec()), + ]))); + } + Vector::pack(VectorSpecialization::Container, indexed_struct) +} diff --git a/sui-execution/replay_cut/sui-move-natives/src/crypto/poseidon.rs b/sui-execution/replay_cut/sui-move-natives/src/crypto/poseidon.rs new file mode 100644 index 0000000000000..fe7e481977a45 --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/crypto/poseidon.rs @@ -0,0 +1,116 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 +use crate::object_runtime::ObjectRuntime; +use crate::{NativesCostTable, get_extension}; +use fastcrypto_zkp::bn254::poseidon::poseidon_bytes; +use move_binary_format::errors::PartialVMResult; +use move_core_types::gas_algebra::InternalGas; +use move_core_types::vm_status::StatusCode; +use move_vm_runtime::{native_charge_gas_early_exit, native_functions::NativeContext}; +use move_vm_types::natives::function::PartialVMError; +use move_vm_types::{ + loaded_data::runtime_types::Type, + natives::function::NativeResult, + pop_arg, + values::{Value, VectorRef}, +}; +use smallvec::smallvec; +use std::collections::VecDeque; +use std::ops::Mul; + +pub const NON_CANONICAL_INPUT: u64 = 0; +pub const NOT_SUPPORTED_ERROR: u64 = 1; +pub const TOO_MANY_INPUTS: u64 = 2; + +pub const MAX_POSEIDON_INPUTS: u64 = 16; + +fn is_supported(context: &NativeContext) -> PartialVMResult { + Ok(get_extension!(context, ObjectRuntime)? + .protocol_config + .enable_poseidon()) +} + +#[derive(Clone)] +pub struct PoseidonBN254CostParams { + /// Base cost for invoking the `poseidon_bn254` function + pub poseidon_bn254_cost_base: Option, + /// Cost per block of `data`, where a block is 32 bytes + pub poseidon_bn254_data_cost_per_block: Option, +} + +/*************************************************************************************************** + * native fun poseidon_bn254 + * Implementation of the Move native function `poseidon::poseidon_bn254_internal(data: &vector>): vector + * gas cost: poseidon_bn254_cost_base | base cost for function call and fixed opers + * + poseidon_bn254_data_cost_per_block * num_inputs | cost depends on number of inputs + **************************************************************************************************/ +pub fn poseidon_bn254_internal( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + let cost = context.gas_used(); + if !is_supported(context)? { + return Ok(NativeResult::err(cost, NOT_SUPPORTED_ERROR)); + } + + // Load the cost parameters from the protocol config + let cost_params = get_extension!(context, NativesCostTable)? + .poseidon_bn254_cost_params + .clone(); + + // Charge the base cost for this operation + native_charge_gas_early_exit!( + context, + cost_params + .poseidon_bn254_cost_base + .ok_or_else( + || PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("Gas cost for poseidon_bn254 not available".to_string()) + )? + ); + + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 1); + + // The input is a reference to a vector of vector's + let inputs = pop_arg!(args, VectorRef); + + let length = inputs + .len(&Type::Vector(Box::new(Type::U8)))? + .value_as::()?; + + if length > MAX_POSEIDON_INPUTS { + return Ok(NativeResult::err(context.gas_used(), TOO_MANY_INPUTS)); + } + + // Charge the msg dependent costs + native_charge_gas_early_exit!( + context, + cost_params + .poseidon_bn254_data_cost_per_block + .ok_or_else( + || PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("Gas cost for poseidon_bn254 not available".to_string()) + )? + .mul(length.into()) + ); + + // Read the input vector + let field_elements = (0..length) + .map(|i| { + let reference = inputs.borrow_elem(i as usize, &Type::Vector(Box::new(Type::U8)))?; + let value = reference.value_as::()?.as_bytes_ref().clone(); + Ok(value) + }) + .collect::>>()?; + + match poseidon_bytes(&field_elements) { + Ok(result) => Ok(NativeResult::ok( + context.gas_used(), + smallvec![Value::vector_u8(result)], + )), + // This is also checked in the poseidon_bn254 move function but to be sure we handle it here also. + Err(_) => Ok(NativeResult::err(context.gas_used(), NON_CANONICAL_INPUT)), + } +} diff --git a/sui-execution/replay_cut/sui-move-natives/src/crypto/vdf.rs b/sui-execution/replay_cut/sui-move-natives/src/crypto/vdf.rs new file mode 100644 index 0000000000000..be8588994dbfc --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/crypto/vdf.rs @@ -0,0 +1,166 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 +use crate::object_runtime::ObjectRuntime; +use crate::{NativesCostTable, get_extension}; +use fastcrypto_vdf::class_group::QuadraticForm; +use fastcrypto_vdf::class_group::discriminant::DISCRIMINANT_3072; +use fastcrypto_vdf::vdf::VDF; +use fastcrypto_vdf::vdf::wesolowski::DefaultVDF; +use move_binary_format::errors::PartialVMResult; +use move_core_types::gas_algebra::InternalGas; +use move_core_types::vm_status::StatusCode; +use move_vm_runtime::{native_charge_gas_early_exit, native_functions::NativeContext}; +use move_vm_types::natives::function::PartialVMError; +use move_vm_types::{ + loaded_data::runtime_types::Type, + natives::function::NativeResult, + pop_arg, + values::{Value, VectorRef}, +}; +use smallvec::smallvec; +use std::collections::VecDeque; + +pub const INVALID_INPUT_ERROR: u64 = 0; +pub const NOT_SUPPORTED_ERROR: u64 = 1; + +fn is_supported(context: &NativeContext) -> PartialVMResult { + Ok(get_extension!(context, ObjectRuntime)? + .protocol_config + .enable_vdf()) +} + +#[derive(Clone)] +pub struct VDFCostParams { + pub vdf_verify_cost: Option, + pub hash_to_input_cost: Option, +} + +/*************************************************************************************************** + * native fun vdf_verify_internal + * + * Implementation of the Move native function `vdf::verify_vdf_internal( + * input: &vector, + * output: &vector, + * proof: &vector, + * iterations: u64): bool` + * + * Gas cost: verify_vdf_cost + **************************************************************************************************/ +pub fn vdf_verify_internal( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + let cost = context.gas_used(); + if !is_supported(context)? { + return Ok(NativeResult::err(cost, NOT_SUPPORTED_ERROR)); + } + + // Load the cost parameters from the protocol config + let cost_params = get_extension!(context, NativesCostTable)? + .vdf_cost_params + .clone(); + + // Charge the base cost for this operation + native_charge_gas_early_exit!( + context, + cost_params + .vdf_verify_cost + .ok_or_else( + || PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("Gas cost for vdf_verify not available".to_string()) + )? + ); + + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 4); + + // The input is a reference to a vector of vector's + let iterations = pop_arg!(args, u64); + let proof_bytes = pop_arg!(args, VectorRef); + let output_bytes = pop_arg!(args, VectorRef); + let input_bytes = pop_arg!(args, VectorRef); + + let input = match bcs::from_bytes::(&input_bytes.as_bytes_ref()) { + Ok(input) => input, + Err(_) => return Ok(NativeResult::err(context.gas_used(), INVALID_INPUT_ERROR)), + }; + + let proof = match bcs::from_bytes::(&proof_bytes.as_bytes_ref()) { + Ok(proof) => proof, + Err(_) => return Ok(NativeResult::err(context.gas_used(), INVALID_INPUT_ERROR)), + }; + + let output = match bcs::from_bytes::(&output_bytes.as_bytes_ref()) { + Ok(output) => output, + Err(_) => return Ok(NativeResult::err(context.gas_used(), INVALID_INPUT_ERROR)), + }; + + // We use the default VDF construction: Wesolowski's construction using a strong Fiat-Shamir + // construction and a windowed scalar multiplier to speed up the proof verification. + let vdf = DefaultVDF::new(DISCRIMINANT_3072.clone(), iterations); + let verified = vdf.verify(&input, &output, &proof).is_ok(); + + Ok(NativeResult::ok( + context.gas_used(), + smallvec![Value::bool(verified)], + )) +} + +/*************************************************************************************************** + * native fun hash_to_input_internal + * + * Implementation of the Move native function `vdf::hash_to_input_internal(message: &vector): vector` + * + * Gas cost: hash_to_input_cost + **************************************************************************************************/ +pub fn hash_to_input_internal( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + let cost = context.gas_used(); + if !is_supported(context)? { + return Ok(NativeResult::err(cost, NOT_SUPPORTED_ERROR)); + } + + // Load the cost parameters from the protocol config + let cost_params = get_extension!(context, NativesCostTable)? + .vdf_cost_params + .clone(); + + // Charge the base cost for this operation + native_charge_gas_early_exit!( + context, + cost_params + .hash_to_input_cost + .ok_or_else( + || PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("Gas cost for hash_to_input not available".to_string()) + )? + ); + + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 1); + + let message = pop_arg!(args, VectorRef); + + let output = match QuadraticForm::hash_to_group_with_default_parameters( + &message.as_bytes_ref(), + &DISCRIMINANT_3072, + ) { + Ok(output) => output, + Err(_) => return Ok(NativeResult::err(context.gas_used(), INVALID_INPUT_ERROR)), + }; + + let output_bytes = match bcs::to_bytes(&output) { + Ok(bytes) => bytes, + // This should only fail on extremely large inputs, so we treat it as an invalid input error + Err(_) => return Ok(NativeResult::err(context.gas_used(), INVALID_INPUT_ERROR)), + }; + + Ok(NativeResult::ok( + context.gas_used(), + smallvec![Value::vector_u8(output_bytes)], + )) +} diff --git a/sui-execution/replay_cut/sui-move-natives/src/crypto/zklogin.rs b/sui-execution/replay_cut/sui-move-natives/src/crypto/zklogin.rs new file mode 100644 index 0000000000000..77c628c9541b5 --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/crypto/zklogin.rs @@ -0,0 +1,203 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 +use crate::{NativesCostTable, get_extension}; +use fastcrypto::error::FastCryptoError; +use move_binary_format::errors::PartialVMResult; +use move_core_types::account_address::AccountAddress; +use move_core_types::gas_algebra::InternalGas; +use move_core_types::u256::U256; +use move_core_types::vm_status::StatusCode; +use move_vm_runtime::{native_charge_gas_early_exit, native_functions::NativeContext}; +use move_vm_types::natives::function::PartialVMError; +use move_vm_types::values::VectorRef; +use move_vm_types::{ + loaded_data::runtime_types::Type, natives::function::NativeResult, pop_arg, values::Value, +}; +use smallvec::smallvec; +use std::collections::VecDeque; + +pub const INVALID_INPUT: u64 = 0; + +#[derive(Clone)] +pub struct CheckZkloginIdCostParams { + /// Base cost for invoking the `check_zklogin_id` function + pub check_zklogin_id_cost_base: Option, +} + +/*************************************************************************************************** + * native fun check_zklogin_id_internal + * + * Implementation of the Move native function `zklogin_verified_id::check_zklogin_id_internal( + * address: address, + * key_claim_name: &vector, + * key_claim_value: &vector, + * issuer: &vector, + * audience: &vector, + * pin_hash: u256 + * ): bool;` + * + * Gas cost: check_zklogin_id_cost | The values name, value, iss and aud are hashed as part of this + * function, but their sizes are bounded from above, so we may assume that the cost is constant. + **************************************************************************************************/ +pub fn check_zklogin_id_internal( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + // Load the cost parameters from the protocol config + let check_zklogin_id_cost_params = get_extension!(context, NativesCostTable)? + .check_zklogin_id_cost_params + .clone(); + + // Charge the base cost for this operation + native_charge_gas_early_exit!( + context, + check_zklogin_id_cost_params + .check_zklogin_id_cost_base + .ok_or_else( + || PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("Gas cost for check_zklogin_id not available".to_string()) + )? + ); + + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 6); + + // Poseidon hash of the user's pin code + let pin_hash = pop_arg!(args, U256); + + // The audience (wallet) id + let audience = pop_arg!(args, VectorRef); + + // The issuer (identity provider) id + let issuer = pop_arg!(args, VectorRef); + + // The claim value (sub, email, etc) + let key_claim_value = pop_arg!(args, VectorRef); + + // The claim name (sub, email, etc) + let key_claim_name = pop_arg!(args, VectorRef); + + // The address to check + let address = pop_arg!(args, AccountAddress); + + let result = check_id_internal( + &address, + &key_claim_name.as_bytes_ref(), + &key_claim_value.as_bytes_ref(), + &audience.as_bytes_ref(), + &issuer.as_bytes_ref(), + &pin_hash, + ); + + match result { + Ok(result) => Ok(NativeResult::ok( + context.gas_used(), + smallvec![Value::bool(result)], + )), + Err(_) => Ok(NativeResult::err(context.gas_used(), INVALID_INPUT)), + } +} + +fn check_id_internal( + address: &AccountAddress, + key_claim_name: &[u8], + key_claim_value: &[u8], + audience: &[u8], + issuer: &[u8], + pin_hash: &U256, +) -> Result { + match fastcrypto_zkp::bn254::zk_login_api::verify_zk_login_id( + &address.into_bytes(), + std::str::from_utf8(key_claim_name).map_err(|_| FastCryptoError::InvalidInput)?, + std::str::from_utf8(key_claim_value).map_err(|_| FastCryptoError::InvalidInput)?, + std::str::from_utf8(audience).map_err(|_| FastCryptoError::InvalidInput)?, + std::str::from_utf8(issuer).map_err(|_| FastCryptoError::InvalidInput)?, + &pin_hash.to_string(), + ) { + Ok(_) => Ok(true), + Err(FastCryptoError::InvalidProof) => Ok(false), + Err(_) => Err(FastCryptoError::InvalidInput), + } +} + +#[derive(Clone)] +pub struct CheckZkloginIssuerCostParams { + /// Base cost for invoking the `check_zklogin_issuer` function + pub check_zklogin_issuer_cost_base: Option, +} + +/*************************************************************************************************** + * native fun check_zklogin_issuer_internal + * + * Implementation of the Move native function `zklogin_verified_issuer::check_zklogin_issuer_internal( + * address: address, + * address_seed: u256, + * issuer: &vector, + * ): bool;` + * + * Gas cost: check_zklogin_issuer_cost | The iss value is hashed as part of this function, but its size + * is bounded from above so we may assume that the cost is constant. + **************************************************************************************************/ +pub fn check_zklogin_issuer_internal( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + // Load the cost parameters from the protocol config + let check_zklogin_issuer_cost_params = get_extension!(context, NativesCostTable)? + .check_zklogin_issuer_cost_params + .clone(); + + // Charge the base cost for this operation + native_charge_gas_early_exit!( + context, + check_zklogin_issuer_cost_params + .check_zklogin_issuer_cost_base + .ok_or_else( + || PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("Gas cost for check_zklogin_issuer not available".to_string()) + )? + ); + + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 3); + + // The issuer (identity provider) id + let issuer = pop_arg!(args, VectorRef); + + // The audience (wallet) id + let address_seed = pop_arg!(args, U256); + + // The address to check + let address = pop_arg!(args, AccountAddress); + + let result = check_issuer_internal(&address, &address_seed, &issuer.as_bytes_ref()); + + match result { + Ok(result) => Ok(NativeResult::ok( + context.gas_used(), + smallvec![Value::bool(result)], + )), + Err(_) => Ok(NativeResult::err(context.gas_used(), INVALID_INPUT)), + } +} + +fn check_issuer_internal( + address: &AccountAddress, + address_seed: &U256, + issuer: &[u8], +) -> Result { + match fastcrypto_zkp::bn254::zk_login_api::verify_zk_login_iss( + &address.into_bytes(), + &address_seed.to_string(), + std::str::from_utf8(issuer).map_err(|_| FastCryptoError::InvalidInput)?, + ) { + Ok(_) => Ok(true), + Err(FastCryptoError::InvalidProof) => Ok(false), + // This will only happen if the address_seed as a string cannot be converted to a BigInt in + // fastcrypto. This should not happen, so an InvalidInput error from `check_iss_internal` + // implies that the `iss` bytes array could not be parsed as an UTF-8 string. + Err(_) => Err(FastCryptoError::InvalidInput), + } +} diff --git a/sui-execution/replay_cut/sui-move-natives/src/dynamic_field.rs b/sui-execution/replay_cut/sui-move-natives/src/dynamic_field.rs new file mode 100644 index 0000000000000..04c14f4c3fd2f --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/dynamic_field.rs @@ -0,0 +1,542 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + NativesCostTable, abstract_size, charge_cache_or_load_gas, get_extension, get_extension_mut, + get_nested_struct_field, get_object_id, + object_runtime::{ + ObjectRuntime, + object_store::{CacheInfo, ObjectResult}, + }, +}; +use move_binary_format::errors::{PartialVMError, PartialVMResult}; +use move_core_types::{ + account_address::AccountAddress, + gas_algebra::InternalGas, + language_storage::{StructTag, TypeTag}, + vm_status::StatusCode, +}; +use move_vm_runtime::native_charge_gas_early_exit; +use move_vm_runtime::native_functions::NativeContext; +use move_vm_types::{ + loaded_data::runtime_types::Type, + natives::function::NativeResult, + pop_arg, + values::{StructRef, Value}, + views::{SizeConfig, ValueView}, +}; +use smallvec::smallvec; +use std::collections::VecDeque; +use sui_types::{base_types::MoveObjectType, dynamic_field::derive_dynamic_field_id}; +use tracing::instrument; + +const E_KEY_DOES_NOT_EXIST: u64 = 1; +const E_FIELD_TYPE_MISMATCH: u64 = 2; +const E_BCS_SERIALIZATION_FAILURE: u64 = 3; + +// Used for pre-existing values +const PRE_EXISTING_ABSTRACT_SIZE: u64 = 2; +// Used for borrowing pre-existing values +const BORROW_ABSTRACT_SIZE: u64 = 8; + +macro_rules! get_or_fetch_object { + ($context:ident, $ty_args:ident, $parent:ident, $child_id:ident, $ty_cost_per_byte:expr) => {{ + let child_ty = $ty_args.pop().unwrap(); + native_charge_gas_early_exit!( + $context, + $ty_cost_per_byte * u64::from(child_ty.size()).into() + ); + + assert!($ty_args.is_empty()); + let (tag, layout, annotated_layout) = match crate::get_tag_and_layouts($context, &child_ty)? + { + Some(res) => res, + None => { + return Ok(NativeResult::err( + $context.gas_used(), + E_BCS_SERIALIZATION_FAILURE, + )); + } + }; + + let object_runtime: &mut ObjectRuntime = $crate::get_extension_mut!($context)?; + object_runtime.get_or_fetch_child_object( + $parent, + $child_id, + &layout, + &annotated_layout, + MoveObjectType::from(tag), + )? + }}; +} + +#[derive(Clone)] +pub struct DynamicFieldHashTypeAndKeyCostParams { + pub dynamic_field_hash_type_and_key_cost_base: InternalGas, + pub dynamic_field_hash_type_and_key_type_cost_per_byte: InternalGas, + pub dynamic_field_hash_type_and_key_value_cost_per_byte: InternalGas, + pub dynamic_field_hash_type_and_key_type_tag_cost_per_byte: InternalGas, +} + +/*************************************************************************************************** + * native fun hash_type_and_key + * Implementation of the Move native function `hash_type_and_key(parent: address, k: K): address` + * gas cost: dynamic_field_hash_type_and_key_cost_base | covers various fixed costs in the oper + * + dynamic_field_hash_type_and_key_type_cost_per_byte * size_of(K) | covers cost of operating on the type `K` + * + dynamic_field_hash_type_and_key_value_cost_per_byte * size_of(k) | covers cost of operating on the value `k` + * + dynamic_field_hash_type_and_key_type_tag_cost_per_byte * size_of(type_tag(k)) | covers cost of operating on the type tag of `K` + **************************************************************************************************/ +#[instrument(level = "trace", skip_all)] +pub fn hash_type_and_key( + context: &mut NativeContext, + mut ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + assert_eq!(ty_args.len(), 1); + assert_eq!(args.len(), 2); + + let dynamic_field_hash_type_and_key_cost_params = get_extension!(context, NativesCostTable)? + .dynamic_field_hash_type_and_key_cost_params + .clone(); + + // Charge base fee + native_charge_gas_early_exit!( + context, + dynamic_field_hash_type_and_key_cost_params.dynamic_field_hash_type_and_key_cost_base + ); + + let k_ty = ty_args.pop().unwrap(); + let k: Value = args.pop_back().unwrap(); + let parent = pop_arg!(args, AccountAddress); + + // Get size info for costing for derivations, serializations, etc + let k_ty_size = u64::from(k_ty.size()); + let k_value_size = u64::from(abstract_size( + get_extension!(context, ObjectRuntime)?.protocol_config, + &k, + )); + native_charge_gas_early_exit!( + context, + dynamic_field_hash_type_and_key_cost_params + .dynamic_field_hash_type_and_key_type_cost_per_byte + * k_ty_size.into() + + dynamic_field_hash_type_and_key_cost_params + .dynamic_field_hash_type_and_key_value_cost_per_byte + * k_value_size.into() + ); + + let k_tag = context.type_to_type_tag(&k_ty)?; + let k_tag_size = u64::from(k_tag.abstract_size_for_gas_metering()); + + native_charge_gas_early_exit!( + context, + dynamic_field_hash_type_and_key_cost_params + .dynamic_field_hash_type_and_key_type_tag_cost_per_byte + * k_tag_size.into() + ); + + let cost = context.gas_used(); + + let k_layout = match context.type_to_type_layout(&k_ty) { + Ok(Some(layout)) => layout, + _ => return Ok(NativeResult::err(cost, E_BCS_SERIALIZATION_FAILURE)), + }; + let Some(k_bytes) = k.typed_serialize(&k_layout) else { + return Ok(NativeResult::err(cost, E_BCS_SERIALIZATION_FAILURE)); + }; + let Ok(id) = derive_dynamic_field_id(parent, &k_tag, &k_bytes) else { + return Ok(NativeResult::err(cost, E_BCS_SERIALIZATION_FAILURE)); + }; + + Ok(NativeResult::ok(cost, smallvec![Value::address(id.into())])) +} + +#[derive(Clone)] +pub struct DynamicFieldAddChildObjectCostParams { + pub dynamic_field_add_child_object_cost_base: InternalGas, + pub dynamic_field_add_child_object_type_cost_per_byte: InternalGas, + pub dynamic_field_add_child_object_value_cost_per_byte: InternalGas, + pub dynamic_field_add_child_object_struct_tag_cost_per_byte: InternalGas, +} + +/*************************************************************************************************** + * native fun add_child_object + * throws `E_KEY_ALREADY_EXISTS` if a child already exists with that ID + * Implementation of the Move native function `add_child_object(parent: address, child: Child)` + * gas cost: dynamic_field_add_child_object_cost_base | covers various fixed costs in the oper + * + dynamic_field_add_child_object_type_cost_per_byte * size_of(Child) | covers cost of operating on the type `Child` + * + dynamic_field_add_child_object_value_cost_per_byte * size_of(child) | covers cost of operating on the value `child` + * + dynamic_field_add_child_object_struct_tag_cost_per_byte * size_of(struct)tag(Child)) | covers cost of operating on the struct tag of `Child` + **************************************************************************************************/ +#[instrument(level = "trace", skip_all)] +pub fn add_child_object( + context: &mut NativeContext, + mut ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + assert!(ty_args.len() == 1); + assert!(args.len() == 2); + + let dynamic_field_add_child_object_cost_params = get_extension!(context, NativesCostTable)? + .dynamic_field_add_child_object_cost_params + .clone(); + + // Charge base fee + native_charge_gas_early_exit!( + context, + dynamic_field_add_child_object_cost_params.dynamic_field_add_child_object_cost_base + ); + + let child = args.pop_back().unwrap(); + let parent = pop_arg!(args, AccountAddress).into(); + assert!(args.is_empty()); + + let protocol_config = get_extension!(context, ObjectRuntime)?.protocol_config; + let child_value_size = if protocol_config.abstract_size_in_object_runtime() { + // The value already exists, the size of the value is irrelevant + PRE_EXISTING_ABSTRACT_SIZE + } else { + child.legacy_size().into() + }; + // ID extraction step + native_charge_gas_early_exit!( + context, + dynamic_field_add_child_object_cost_params + .dynamic_field_add_child_object_value_cost_per_byte + * child_value_size.into() + ); + + // TODO remove this copy_value, which will require VM changes + let child_id = get_object_id(child.copy_value().unwrap()) + .unwrap() + .value_as::() + .unwrap() + .into(); + let child_ty = ty_args.pop().unwrap(); + let child_type_size = u64::from(child_ty.size()); + + native_charge_gas_early_exit!( + context, + dynamic_field_add_child_object_cost_params + .dynamic_field_add_child_object_type_cost_per_byte + * child_type_size.into() + ); + + assert!(ty_args.is_empty()); + let tag = match context.type_to_type_tag(&child_ty)? { + TypeTag::Struct(s) => *s, + _ => { + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("Sui verifier guarantees this is a struct".to_string()), + ); + } + }; + + let struct_tag_size = u64::from(tag.abstract_size_for_gas_metering()); + native_charge_gas_early_exit!( + context, + dynamic_field_add_child_object_cost_params + .dynamic_field_add_child_object_struct_tag_cost_per_byte + * struct_tag_size.into() + ); + + if get_extension!(context, ObjectRuntime)? + .protocol_config + .generate_df_type_layouts() + { + context.type_to_type_layout(&child_ty)?; + } + + let object_runtime: &mut ObjectRuntime = get_extension_mut!(context)?; + object_runtime.add_child_object(parent, child_id, MoveObjectType::from(tag), child)?; + Ok(NativeResult::ok(context.gas_used(), smallvec![])) +} + +#[derive(Clone)] +pub struct DynamicFieldBorrowChildObjectCostParams { + pub dynamic_field_borrow_child_object_cost_base: InternalGas, + pub dynamic_field_borrow_child_object_child_ref_cost_per_byte: InternalGas, + pub dynamic_field_borrow_child_object_type_cost_per_byte: InternalGas, +} + +/*************************************************************************************************** + * native fun borrow_child_object + * throws `E_KEY_DOES_NOT_EXIST` if a child does not exist with that ID at that type + * or throws `E_FIELD_TYPE_MISMATCH` if the type does not match (as the runtime does not distinguish different reference types) + * Implementation of the Move native function `borrow_child_object_mut(parent: &mut UID, id: address): &mut Child` + * gas cost: dynamic_field_borrow_child_object_cost_base | covers various fixed costs in the oper + * + dynamic_field_borrow_child_object_child_ref_cost_per_byte * size_of(&Child) | covers cost of fetching and returning `&Child` + * + dynamic_field_borrow_child_object_type_cost_per_byte * size_of(Child) | covers cost of operating on type `Child` + **************************************************************************************************/ +#[instrument(level = "trace", skip_all)] +pub fn borrow_child_object( + context: &mut NativeContext, + mut ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + assert!(ty_args.len() == 1); + assert!(args.len() == 2); + + let dynamic_field_borrow_child_object_cost_params = get_extension!(context, NativesCostTable)? + .dynamic_field_borrow_child_object_cost_params + .clone(); + native_charge_gas_early_exit!( + context, + dynamic_field_borrow_child_object_cost_params.dynamic_field_borrow_child_object_cost_base + ); + + let child_id = pop_arg!(args, AccountAddress).into(); + + let parent_uid = pop_arg!(args, StructRef).read_ref().unwrap(); + // UID { id: ID { bytes: address } } + let parent = get_nested_struct_field(parent_uid, &[0, 0]) + .unwrap() + .value_as::() + .unwrap() + .into(); + + assert!(args.is_empty()); + let global_value_result = get_or_fetch_object!( + context, + ty_args, + parent, + child_id, + dynamic_field_borrow_child_object_cost_params + .dynamic_field_borrow_child_object_type_cost_per_byte + ); + let (cache_info, global_value) = match global_value_result { + ObjectResult::MismatchedType => { + return Ok(NativeResult::err(context.gas_used(), E_FIELD_TYPE_MISMATCH)); + } + ObjectResult::Loaded(gv) => gv, + }; + if !global_value.exists()? { + return Ok(NativeResult::err(context.gas_used(), E_KEY_DOES_NOT_EXIST)); + } + let child_ref = global_value.borrow_global().inspect_err(|err| { + assert!(err.major_status() != StatusCode::MISSING_DATA); + })?; + + charge_cache_or_load_gas!(context, cache_info); + let protocol_config = get_extension!(context, ObjectRuntime)?.protocol_config; + let child_ref_size = match cache_info { + _ if !protocol_config.abstract_size_in_object_runtime() => child_ref.legacy_size(), + CacheInfo::CachedValue => { + // The value already existed + BORROW_ABSTRACT_SIZE.into() + } + // The Move value had to be created. We traverse references to get the full size of the + // borrowed value + CacheInfo::CachedObject | CacheInfo::Loaded(_) => { + child_ref.abstract_memory_size(&SizeConfig { + include_vector_size: true, + traverse_references: true, + }) + } + }; + + native_charge_gas_early_exit!( + context, + dynamic_field_borrow_child_object_cost_params + .dynamic_field_borrow_child_object_child_ref_cost_per_byte + * u64::from(child_ref_size).into() + ); + + Ok(NativeResult::ok(context.gas_used(), smallvec![child_ref])) +} + +#[derive(Clone)] +pub struct DynamicFieldRemoveChildObjectCostParams { + pub dynamic_field_remove_child_object_cost_base: InternalGas, + pub dynamic_field_remove_child_object_child_cost_per_byte: InternalGas, + pub dynamic_field_remove_child_object_type_cost_per_byte: InternalGas, +} +/*************************************************************************************************** + * native fun remove_child_object + * throws `E_KEY_DOES_NOT_EXIST` if a child does not exist with that ID at that type + * or throws `E_FIELD_TYPE_MISMATCH` if the type does not match + * Implementation of the Move native function `remove_child_object(parent: address, id: address): Child` + * gas cost: dynamic_field_remove_child_object_cost_base | covers various fixed costs in the oper + * + dynamic_field_remove_child_object_type_cost_per_byte * size_of(Child) | covers cost of operating on type `Child` + * + dynamic_field_remove_child_object_child_cost_per_byte * size_of(child) | covers cost of fetching and returning value of type `Child` + **************************************************************************************************/ +#[instrument(level = "trace", skip_all)] +pub fn remove_child_object( + context: &mut NativeContext, + mut ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + assert!(ty_args.len() == 1); + assert!(args.len() == 2); + + let dynamic_field_remove_child_object_cost_params = get_extension!(context, NativesCostTable)? + .dynamic_field_remove_child_object_cost_params + .clone(); + native_charge_gas_early_exit!( + context, + dynamic_field_remove_child_object_cost_params.dynamic_field_remove_child_object_cost_base + ); + + let child_id = pop_arg!(args, AccountAddress).into(); + let parent = pop_arg!(args, AccountAddress).into(); + assert!(args.is_empty()); + let global_value_result = get_or_fetch_object!( + context, + ty_args, + parent, + child_id, + dynamic_field_remove_child_object_cost_params + .dynamic_field_remove_child_object_type_cost_per_byte + ); + let (cache_info, global_value) = match global_value_result { + ObjectResult::MismatchedType => { + return Ok(NativeResult::err(context.gas_used(), E_FIELD_TYPE_MISMATCH)); + } + ObjectResult::Loaded(gv) => gv, + }; + + if !global_value.exists()? { + return Ok(NativeResult::err(context.gas_used(), E_KEY_DOES_NOT_EXIST)); + } + let child = global_value.move_from().inspect_err(|err| { + assert!(err.major_status() != StatusCode::MISSING_DATA); + })?; + + charge_cache_or_load_gas!(context, cache_info); + + let protocol_config = get_extension!(context, ObjectRuntime)?.protocol_config; + let child_size = match cache_info { + _ if !protocol_config.abstract_size_in_object_runtime() => child.legacy_size(), + CacheInfo::CachedValue => { + // The value already existed + PRE_EXISTING_ABSTRACT_SIZE.into() + } + // The Move value had to be created. The value isn't a reference so traverse_references + // doesn't matter + CacheInfo::CachedObject | CacheInfo::Loaded(_) => child.abstract_memory_size(&SizeConfig { + include_vector_size: true, + traverse_references: false, + }), + }; + native_charge_gas_early_exit!( + context, + dynamic_field_remove_child_object_cost_params + .dynamic_field_remove_child_object_child_cost_per_byte + * u64::from(child_size).into() + ); + + Ok(NativeResult::ok(context.gas_used(), smallvec![child])) +} + +#[derive(Clone)] +pub struct DynamicFieldHasChildObjectCostParams { + // All inputs are constant same size. No need for special costing as this is a lookup + pub dynamic_field_has_child_object_cost_base: InternalGas, +} +/*************************************************************************************************** + * native fun has_child_object + * Implementation of the Move native function `has_child_object(parent: address, id: address): bool` + * gas cost: dynamic_field_has_child_object_cost_base | covers various fixed costs in the oper + **************************************************************************************************/ +#[instrument(level = "trace", skip_all)] +pub fn has_child_object( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + assert!(ty_args.is_empty()); + assert!(args.len() == 2); + + let dynamic_field_has_child_object_cost_params = get_extension!(context, NativesCostTable)? + .dynamic_field_has_child_object_cost_params + .clone(); + native_charge_gas_early_exit!( + context, + dynamic_field_has_child_object_cost_params.dynamic_field_has_child_object_cost_base + ); + + let child_id = pop_arg!(args, AccountAddress).into(); + let parent = pop_arg!(args, AccountAddress).into(); + let object_runtime: &mut ObjectRuntime = get_extension_mut!(context)?; + let (cache_info, has_child) = object_runtime.child_object_exists(parent, child_id)?; + charge_cache_or_load_gas!(context, cache_info); + Ok(NativeResult::ok( + context.gas_used(), + smallvec![Value::bool(has_child)], + )) +} + +#[derive(Clone)] +pub struct DynamicFieldHasChildObjectWithTyCostParams { + pub dynamic_field_has_child_object_with_ty_cost_base: InternalGas, + pub dynamic_field_has_child_object_with_ty_type_cost_per_byte: InternalGas, + pub dynamic_field_has_child_object_with_ty_type_tag_cost_per_byte: InternalGas, +} +/*************************************************************************************************** + * native fun has_child_object_with_ty + * Implementation of the Move native function `has_child_object_with_ty(parent: address, id: address): bool` + * gas cost: dynamic_field_has_child_object_with_ty_cost_base | covers various fixed costs in the oper + * + dynamic_field_has_child_object_with_ty_type_cost_per_byte * size_of(Child) | covers cost of operating on type `Child` + * + dynamic_field_has_child_object_with_ty_type_tag_cost_per_byte * size_of(Child) | covers cost of fetching and returning value of type tag for `Child` + **************************************************************************************************/ +#[instrument(level = "trace", skip_all)] +pub fn has_child_object_with_ty( + context: &mut NativeContext, + mut ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + assert!(ty_args.len() == 1); + assert!(args.len() == 2); + + let dynamic_field_has_child_object_with_ty_cost_params = + get_extension!(context, NativesCostTable)? + .dynamic_field_has_child_object_with_ty_cost_params + .clone(); + native_charge_gas_early_exit!( + context, + dynamic_field_has_child_object_with_ty_cost_params + .dynamic_field_has_child_object_with_ty_cost_base + ); + + let child_id = pop_arg!(args, AccountAddress).into(); + let parent = pop_arg!(args, AccountAddress).into(); + assert!(args.is_empty()); + let ty = ty_args.pop().unwrap(); + + native_charge_gas_early_exit!( + context, + dynamic_field_has_child_object_with_ty_cost_params + .dynamic_field_has_child_object_with_ty_type_cost_per_byte + * u64::from(ty.size()).into() + ); + + let tag: StructTag = match context.type_to_type_tag(&ty)? { + TypeTag::Struct(s) => *s, + _ => { + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("Sui verifier guarantees this is a struct".to_string()), + ); + } + }; + + native_charge_gas_early_exit!( + context, + dynamic_field_has_child_object_with_ty_cost_params + .dynamic_field_has_child_object_with_ty_type_tag_cost_per_byte + * u64::from(tag.abstract_size_for_gas_metering()).into() + ); + + let object_runtime: &mut ObjectRuntime = get_extension_mut!(context)?; + let (cache_info, has_child) = object_runtime.child_object_exists_and_has_type( + parent, + child_id, + &MoveObjectType::from(tag), + )?; + charge_cache_or_load_gas!(context, cache_info); + Ok(NativeResult::ok( + context.gas_used(), + smallvec![Value::bool(has_child)], + )) +} diff --git a/sui-execution/replay_cut/sui-move-natives/src/event.rs b/sui-execution/replay_cut/sui-move-natives/src/event.rs new file mode 100644 index 0000000000000..17e6052d362cc --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/event.rs @@ -0,0 +1,280 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + NativesCostTable, abstract_size, get_extension, get_extension_mut, legacy_test_cost, + object_runtime::{MoveAccumulatorAction, MoveAccumulatorValue, ObjectRuntime}, +}; +use move_binary_format::errors::{PartialVMError, PartialVMResult}; +use move_core_types::{ + account_address::AccountAddress, gas_algebra::InternalGas, language_storage::TypeTag, + vm_status::StatusCode, +}; +use move_vm_runtime::{native_charge_gas_early_exit, native_functions::NativeContext}; +use move_vm_types::{ + loaded_data::runtime_types::Type, + natives::function::NativeResult, + values::{Value, VectorSpecialization}, +}; +use smallvec::smallvec; +use std::collections::VecDeque; +use sui_types::{base_types::ObjectID, error::VMMemoryLimitExceededSubStatusCode}; + +pub const NOT_SUPPORTED: u64 = 0; + +#[derive(Clone, Debug)] +pub struct EventEmitCostParams { + pub event_emit_cost_base: InternalGas, + pub event_emit_value_size_derivation_cost_per_byte: InternalGas, + pub event_emit_tag_size_derivation_cost_per_byte: InternalGas, + pub event_emit_output_cost_per_byte: InternalGas, + pub event_emit_auth_stream_cost: Option, +} + +/*************************************************************************************************** + * native fun emit + * Implementation of the Move native function `event::emit(event: T)` + * Adds an event to the transaction's event log + * gas cost: event_emit_cost_base | covers various fixed costs in the oper + * + event_emit_value_size_derivation_cost_per_byte * event_size | derivation of size + * + event_emit_tag_size_derivation_cost_per_byte * tag_size | converting type + * + event_emit_output_cost_per_byte * (tag_size + event_size) | emitting the actual event + **************************************************************************************************/ +pub fn emit( + context: &mut NativeContext, + mut ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.len() == 1); + debug_assert!(args.len() == 1); + + let ty = ty_args.pop().unwrap(); + let event_value = args.pop_back().unwrap(); + emit_impl(context, ty, event_value, None) +} + +pub fn emit_authenticated_impl( + context: &mut NativeContext, + mut ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.len() == 2); + debug_assert!(args.len() == 3); + + let cost = context.gas_used(); + if !get_extension!(context, ObjectRuntime)? + .protocol_config + .enable_authenticated_event_streams() + { + return Ok(NativeResult::err(cost, NOT_SUPPORTED)); + } + + let event_ty = ty_args.pop().unwrap(); + // This type is always sui::event::EventStreamHead + let stream_head_ty = ty_args.pop().unwrap(); + + let event_value = args.pop_back().unwrap(); + let stream_id = args.pop_back().unwrap(); + let accumulator_id = args.pop_back().unwrap(); + + emit_impl( + context, + event_ty, + event_value, + Some(StreamRef { + accumulator_id, + stream_id, + stream_head_ty, + }), + ) +} + +struct StreamRef { + // The pre-computed id of the accumulator object. This is a hash of + // stream_id + ty + accumulator_id: Value, + // The stream ID (the `stream_id` field of some EventStreamCap) + stream_id: Value, + // The type of the stream head. Should always be `sui::event::EventStreamHead` + stream_head_ty: Type, +} + +fn emit_impl( + context: &mut NativeContext, + ty: Type, + event_value: Value, + stream_ref: Option, +) -> PartialVMResult { + let event_emit_cost_params = get_extension!(context, NativesCostTable)? + .event_emit_cost_params + .clone(); + + native_charge_gas_early_exit!(context, event_emit_cost_params.event_emit_cost_base); + + let event_value_size = abstract_size( + get_extension!(context, ObjectRuntime)?.protocol_config, + &event_value, + ); + + // Deriving event value size can be expensive due to recursion overhead + native_charge_gas_early_exit!( + context, + event_emit_cost_params.event_emit_value_size_derivation_cost_per_byte + * u64::from(event_value_size).into() + ); + + let tag = match context.type_to_type_tag(&ty)? { + TypeTag::Struct(s) => s, + _ => { + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("Sui verifier guarantees this is a struct".to_string()), + ); + } + }; + let tag_size = tag.abstract_size_for_gas_metering(); + + // Converting type to typetag be expensive due to recursion overhead + native_charge_gas_early_exit!( + context, + event_emit_cost_params.event_emit_tag_size_derivation_cost_per_byte + * u64::from(tag_size).into() + ); + + if stream_ref.is_some() { + native_charge_gas_early_exit!( + context, + // this code cannot be reached in protocol versions which don't define + // event_emit_auth_stream_cost + event_emit_cost_params.event_emit_auth_stream_cost.unwrap() + ); + } + + // Get the type tag before getting the mutable reference to avoid borrowing issues + let stream_head_type_tag = if stream_ref.is_some() { + Some(context.type_to_type_tag(&stream_ref.as_ref().unwrap().stream_head_ty)?) + } else { + None + }; + + let obj_runtime: &mut ObjectRuntime = get_extension_mut!(context)?; + let max_event_emit_size = obj_runtime.protocol_config.max_event_emit_size(); + let ev_size = u64::from(tag_size + event_value_size); + // Check if the event size is within the limit + if ev_size > max_event_emit_size { + return Err(PartialVMError::new(StatusCode::MEMORY_LIMIT_EXCEEDED) + .with_message(format!( + "Emitting event of size {ev_size} bytes. Limit is {max_event_emit_size} bytes." + )) + .with_sub_status( + VMMemoryLimitExceededSubStatusCode::EVENT_SIZE_LIMIT_EXCEEDED as u64, + )); + } + + // Check that the size contribution of the event is within the total size limit + // This feature is guarded as its only present in some versions + if let Some(max_event_emit_size_total) = obj_runtime + .protocol_config + .max_event_emit_size_total_as_option() + { + let total_events_size = obj_runtime.state.total_events_size() + ev_size; + if total_events_size > max_event_emit_size_total { + return Err(PartialVMError::new(StatusCode::MEMORY_LIMIT_EXCEEDED) + .with_message(format!( + "Reached total event size of size {total_events_size} bytes. Limit is {max_event_emit_size_total} bytes." + )) + .with_sub_status( + VMMemoryLimitExceededSubStatusCode::TOTAL_EVENT_SIZE_LIMIT_EXCEEDED as u64, + )); + } + obj_runtime.state.incr_total_events_size(ev_size); + } + // Emitting an event is cheap since its a vector push + native_charge_gas_early_exit!( + context, + event_emit_cost_params.event_emit_output_cost_per_byte * ev_size.into() + ); + + let obj_runtime: &mut ObjectRuntime = get_extension_mut!(context)?; + + obj_runtime.emit_event(*tag, event_value)?; + + if let Some(StreamRef { + accumulator_id, + stream_id, + stream_head_ty: _, + }) = stream_ref + { + let stream_id_addr: AccountAddress = stream_id.value_as::().unwrap(); + let accumulator_id: ObjectID = accumulator_id.value_as::().unwrap().into(); + let events_len = obj_runtime.state.events().len(); + if events_len == 0 { + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("No events found after emitting authenticated event".to_string()), + ); + } + let event_idx = events_len - 1; + obj_runtime.emit_accumulator_event( + accumulator_id, + MoveAccumulatorAction::Merge, + stream_id_addr, + stream_head_type_tag.unwrap(), + MoveAccumulatorValue::EventRef(event_idx as u64), + )?; + } + + Ok(NativeResult::ok(context.gas_used(), smallvec![])) +} + +/// Get the all emitted events of type `T`, starting at the specified index +pub fn num_events( + context: &mut NativeContext, + ty_args: Vec, + args: VecDeque, +) -> PartialVMResult { + assert!(ty_args.is_empty()); + assert!(args.is_empty()); + let object_runtime_ref: &ObjectRuntime = get_extension!(context)?; + let num_events = object_runtime_ref.state.events().len(); + Ok(NativeResult::ok( + legacy_test_cost(), + smallvec![Value::u32(num_events as u32)], + )) +} + +/// Get the all emitted events of type `T`, starting at the specified index +pub fn get_events_by_type( + context: &mut NativeContext, + mut ty_args: Vec, + args: VecDeque, +) -> PartialVMResult { + assert_eq!(ty_args.len(), 1); + let specified_ty = ty_args.pop().unwrap(); + let specialization: VectorSpecialization = (&specified_ty).try_into()?; + assert!(args.is_empty()); + let object_runtime_ref: &ObjectRuntime = get_extension!(context)?; + let specified_type_tag = match context.type_to_type_tag(&specified_ty)? { + TypeTag::Struct(s) => *s, + _ => return Ok(NativeResult::ok(legacy_test_cost(), smallvec![])), + }; + let matched_events = object_runtime_ref + .state + .events() + .iter() + .filter_map(|(tag, event)| { + if &specified_type_tag == tag { + Some(event.copy_value().unwrap()) + } else { + None + } + }) + .collect::>(); + Ok(NativeResult::ok( + legacy_test_cost(), + smallvec![move_vm_types::values::Vector::pack( + specialization, + matched_events + )?], + )) +} diff --git a/sui-execution/replay_cut/sui-move-natives/src/funds_accumulator.rs b/sui-execution/replay_cut/sui-move-natives/src/funds_accumulator.rs new file mode 100644 index 0000000000000..a2752de302dca --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/funds_accumulator.rs @@ -0,0 +1,152 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::VecDeque; + +use move_binary_format::errors::{PartialVMError, PartialVMResult}; +use move_core_types::{account_address::AccountAddress, u256::U256, vm_status::StatusCode}; +use move_vm_runtime::{native_charge_gas_early_exit, native_functions::NativeContext}; +use move_vm_types::{ + loaded_data::runtime_types::Type, + natives::function::NativeResult, + values::{Struct, Value}, +}; +use smallvec::smallvec; +use sui_types::base_types::ObjectID; + +use crate::{ + NativesCostTable, + object_runtime::{MoveAccumulatorAction, MoveAccumulatorValue, ObjectRuntime}, +}; + +const E_OVERFLOW: u64 = 0; +const E_ADDRESS_BALANCE_NOT_ENABLED: u64 = 1; + +pub fn add_to_accumulator_address( + context: &mut NativeContext, + mut ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.len() == 1); + debug_assert!(args.len() == 3); + + // TODO(address-balances): add specific cost for this + let event_emit_cost_params = context + .extensions_mut() + .get::()? + .event_emit_cost_params + .clone(); + native_charge_gas_early_exit!(context, event_emit_cost_params.event_emit_cost_base); + + let ty_tag = context.type_to_type_tag(&ty_args.pop().unwrap())?; + + let Some(value) = args.pop_back().unwrap().value_as::().ok() else { + // TODO in the future this is guaranteed/checked via a custom verifier rule + debug_assert!(false); + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR).with_message( + "Balance should be guaranteed under current implementation".to_owned(), + ), + ); + }; + let recipient = args + .pop_back() + .unwrap() + .value_as::() + .unwrap(); + let accumulator: ObjectID = args + .pop_back() + .unwrap() + .value_as::() + .unwrap() + .into(); + + // TODO this will need to look at the layout of T when this is not guaranteed to be a Balance + let Some([amount]): Option<[Value; 1]> = value + .unpack() + .ok() + .and_then(|vs| vs.collect::>().try_into().ok()) + else { + debug_assert!(false); + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR).with_message( + "Balance should be guaranteed under current implementation".to_owned(), + ), + ); + }; + let Some(amount) = amount.value_as::().ok() else { + debug_assert!(false); + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR).with_message( + "Balance should be guaranteed under current implementation".to_owned(), + ), + ); + }; + + let cost = context.gas_used(); + + let obj_runtime: &mut ObjectRuntime = context.extensions_mut().get_mut()?; + + if !obj_runtime.protocol_config.enable_accumulators() { + return Ok(NativeResult::err(cost, E_ADDRESS_BALANCE_NOT_ENABLED)); + } + + obj_runtime.emit_accumulator_event( + accumulator, + MoveAccumulatorAction::Merge, + recipient, + ty_tag, + MoveAccumulatorValue::U64(amount), + )?; + Ok(NativeResult::ok(context.gas_used(), smallvec![])) +} + +pub fn withdraw_from_accumulator_address( + context: &mut NativeContext, + mut ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.len() == 1); + debug_assert!(args.len() == 3); + + // TODO(address-balances): add specific cost for this + // TODO(address-balances): determine storage cost for "Merge" + let event_emit_cost_params = context + .extensions_mut() + .get::()? + .event_emit_cost_params + .clone(); + native_charge_gas_early_exit!(context, event_emit_cost_params.event_emit_cost_base); + + let ty_tag = context.type_to_type_tag(&ty_args.pop().unwrap())?; + + let value = args.pop_back().unwrap().value_as::().unwrap(); + let recipient = args + .pop_back() + .unwrap() + .value_as::() + .unwrap(); + let accumulator: ObjectID = args + .pop_back() + .unwrap() + .value_as::() + .unwrap() + .into(); + + // TODO this will need to look at the layout of T when this is not guaranteed to be a Balance + let Ok(amount): Result = value.try_into() else { + return Ok(NativeResult::err(context.gas_used(), E_OVERFLOW)); + }; + + let obj_runtime: &mut ObjectRuntime = context.extensions_mut().get_mut()?; + obj_runtime.emit_accumulator_event( + accumulator, + MoveAccumulatorAction::Split, + recipient, + ty_tag, + MoveAccumulatorValue::U64(amount), + )?; + // TODO this will need to look at the layout of T when this is not guaranteed to be a Balance + let withdrawn = Value::struct_(Struct::pack(vec![Value::u64(amount)])); + Ok(NativeResult::ok(context.gas_used(), smallvec![withdrawn])) +} diff --git a/sui-execution/replay_cut/sui-move-natives/src/lib.rs b/sui-execution/replay_cut/sui-move-natives/src/lib.rs new file mode 100644 index 0000000000000..73688a19ead02 --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/lib.rs @@ -0,0 +1,1424 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use self::{ + address::{AddressFromBytesCostParams, AddressFromU256CostParams, AddressToU256CostParams}, + config::ConfigReadSettingImplCostParams, + crypto::{bls12381, ecdsa_k1, ecdsa_r1, ecvrf, ed25519, groth16, hash, hmac}, + crypto::{ + bls12381::{Bls12381Bls12381MinPkVerifyCostParams, Bls12381Bls12381MinSigVerifyCostParams}, + ecdsa_k1::{ + EcdsaK1DecompressPubkeyCostParams, EcdsaK1EcrecoverCostParams, + EcdsaK1Secp256k1VerifyCostParams, + }, + ecdsa_r1::{EcdsaR1EcrecoverCostParams, EcdsaR1Secp256R1VerifyCostParams}, + ecvrf::EcvrfEcvrfVerifyCostParams, + ed25519::Ed25519VerifyCostParams, + groth16::{ + Groth16PrepareVerifyingKeyCostParams, Groth16VerifyGroth16ProofInternalCostParams, + }, + hash::{HashBlake2b256CostParams, HashKeccak256CostParams}, + hmac::HmacHmacSha3256CostParams, + poseidon, + }, + dynamic_field::{ + DynamicFieldAddChildObjectCostParams, DynamicFieldBorrowChildObjectCostParams, + DynamicFieldHasChildObjectCostParams, DynamicFieldHasChildObjectWithTyCostParams, + DynamicFieldHashTypeAndKeyCostParams, DynamicFieldRemoveChildObjectCostParams, + }, + event::EventEmitCostParams, + object::{BorrowUidCostParams, DeleteImplCostParams, RecordNewIdCostParams}, + transfer::{ + TransferFreezeObjectCostParams, TransferInternalCostParams, TransferShareObjectCostParams, + }, + tx_context::{ + TxContextDeriveIdCostParams, TxContextEpochCostParams, TxContextEpochTimestampMsCostParams, + TxContextFreshIdCostParams, TxContextGasBudgetCostParams, TxContextGasPriceCostParams, + TxContextIdsCreatedCostParams, TxContextRGPCostParams, TxContextReplaceCostParams, + TxContextSenderCostParams, TxContextSponsorCostParams, + }, + types::TypesIsOneTimeWitnessCostParams, + validator::ValidatorValidateMetadataBcsCostParams, +}; +use crate::crypto::group_ops::GroupOpsCostParams; +use crate::crypto::poseidon::PoseidonBN254CostParams; +use crate::crypto::zklogin; +use crate::crypto::zklogin::{CheckZkloginIdCostParams, CheckZkloginIssuerCostParams}; +use crate::{crypto::group_ops, transfer::PartyTransferInternalCostParams}; +use better_any::{Tid, TidAble}; +use crypto::nitro_attestation::{self, NitroAttestationCostParams}; +use crypto::vdf::{self, VDFCostParams}; +use move_binary_format::errors::{PartialVMError, PartialVMResult}; +use move_core_types::{ + annotated_value as A, + gas_algebra::{AbstractMemorySize, InternalGas}, + identifier::Identifier, + language_storage::{StructTag, TypeTag}, + runtime_value as R, + vm_status::StatusCode, +}; +use move_stdlib_natives::{self as MSN, GasParameters}; +use move_vm_runtime::{ + native_extensions::NativeExtensionMarker, + native_functions::{NativeContext, NativeFunction, NativeFunctionTable}, +}; +use move_vm_types::{ + loaded_data::runtime_types::Type, + natives::function::NativeResult, + values::{Struct, Value}, + views::{SizeConfig, ValueView}, +}; +use std::sync::Arc; +use sui_protocol_config::ProtocolConfig; +use sui_types::{MOVE_STDLIB_ADDRESS, SUI_FRAMEWORK_ADDRESS, SUI_SYSTEM_ADDRESS}; +use transfer::TransferReceiveObjectInternalCostParams; + +mod accumulator; +mod address; +mod config; +mod crypto; +mod dynamic_field; +pub mod event; +mod funds_accumulator; +mod object; +pub mod object_runtime; +mod random; +pub mod test_scenario; +mod test_utils; +pub mod transaction_context; +mod transfer; +mod tx_context; +mod types; +mod validator; + +// TODO: remove in later PRs once we define the proper cost of native functions +const DEFAULT_UNUSED_TX_CONTEXT_ENTRY_COST: u64 = 10; +#[derive(Tid)] +pub struct NativesCostTable { + // Address natives + pub address_from_bytes_cost_params: AddressFromBytesCostParams, + pub address_to_u256_cost_params: AddressToU256CostParams, + pub address_from_u256_cost_params: AddressFromU256CostParams, + + // Config + pub config_read_setting_impl_cost_params: ConfigReadSettingImplCostParams, + + // Dynamic field natives + pub dynamic_field_hash_type_and_key_cost_params: DynamicFieldHashTypeAndKeyCostParams, + pub dynamic_field_add_child_object_cost_params: DynamicFieldAddChildObjectCostParams, + pub dynamic_field_borrow_child_object_cost_params: DynamicFieldBorrowChildObjectCostParams, + pub dynamic_field_remove_child_object_cost_params: DynamicFieldRemoveChildObjectCostParams, + pub dynamic_field_has_child_object_cost_params: DynamicFieldHasChildObjectCostParams, + pub dynamic_field_has_child_object_with_ty_cost_params: + DynamicFieldHasChildObjectWithTyCostParams, + + // Event natives + pub event_emit_cost_params: EventEmitCostParams, + + // Object + pub borrow_uid_cost_params: BorrowUidCostParams, + pub delete_impl_cost_params: DeleteImplCostParams, + pub record_new_id_cost_params: RecordNewIdCostParams, + + // Transfer + pub transfer_transfer_internal_cost_params: TransferInternalCostParams, + pub transfer_party_transfer_internal_cost_params: PartyTransferInternalCostParams, + pub transfer_freeze_object_cost_params: TransferFreezeObjectCostParams, + pub transfer_share_object_cost_params: TransferShareObjectCostParams, + + // TxContext + pub tx_context_derive_id_cost_params: TxContextDeriveIdCostParams, + pub tx_context_fresh_id_cost_params: TxContextFreshIdCostParams, + pub tx_context_sender_cost_params: TxContextSenderCostParams, + pub tx_context_epoch_cost_params: TxContextEpochCostParams, + pub tx_context_epoch_timestamp_ms_cost_params: TxContextEpochTimestampMsCostParams, + pub tx_context_sponsor_cost_params: TxContextSponsorCostParams, + pub tx_context_rgp_cost_params: TxContextRGPCostParams, + pub tx_context_gas_price_cost_params: TxContextGasPriceCostParams, + pub tx_context_gas_budget_cost_params: TxContextGasBudgetCostParams, + pub tx_context_ids_created_cost_params: TxContextIdsCreatedCostParams, + pub tx_context_replace_cost_params: TxContextReplaceCostParams, + + // Type + pub type_is_one_time_witness_cost_params: TypesIsOneTimeWitnessCostParams, + + // Validator + pub validator_validate_metadata_bcs_cost_params: ValidatorValidateMetadataBcsCostParams, + + // Crypto natives + pub crypto_invalid_arguments_cost: InternalGas, + // bls12381 + pub bls12381_bls12381_min_sig_verify_cost_params: Bls12381Bls12381MinSigVerifyCostParams, + pub bls12381_bls12381_min_pk_verify_cost_params: Bls12381Bls12381MinPkVerifyCostParams, + + // ecdsak1 + pub ecdsa_k1_ecrecover_cost_params: EcdsaK1EcrecoverCostParams, + pub ecdsa_k1_decompress_pubkey_cost_params: EcdsaK1DecompressPubkeyCostParams, + pub ecdsa_k1_secp256k1_verify_cost_params: EcdsaK1Secp256k1VerifyCostParams, + + // ecdsar1 + pub ecdsa_r1_ecrecover_cost_params: EcdsaR1EcrecoverCostParams, + pub ecdsa_r1_secp256_r1_verify_cost_params: EcdsaR1Secp256R1VerifyCostParams, + + // ecvrf + pub ecvrf_ecvrf_verify_cost_params: EcvrfEcvrfVerifyCostParams, + + // ed25519 + pub ed25519_verify_cost_params: Ed25519VerifyCostParams, + + // groth16 + pub groth16_prepare_verifying_key_cost_params: Groth16PrepareVerifyingKeyCostParams, + pub groth16_verify_groth16_proof_internal_cost_params: + Groth16VerifyGroth16ProofInternalCostParams, + + // hash + pub hash_blake2b256_cost_params: HashBlake2b256CostParams, + pub hash_keccak256_cost_params: HashKeccak256CostParams, + + // poseidon + pub poseidon_bn254_cost_params: PoseidonBN254CostParams, + + // hmac + pub hmac_hmac_sha3_256_cost_params: HmacHmacSha3256CostParams, + + // group ops + pub group_ops_cost_params: GroupOpsCostParams, + + // vdf + pub vdf_cost_params: VDFCostParams, + + // zklogin + pub check_zklogin_id_cost_params: CheckZkloginIdCostParams, + pub check_zklogin_issuer_cost_params: CheckZkloginIssuerCostParams, + + // Receive object + pub transfer_receive_object_internal_cost_params: TransferReceiveObjectInternalCostParams, + + // nitro attestation + pub nitro_attestation_cost_params: NitroAttestationCostParams, +} + +impl NativeExtensionMarker<'_> for NativesCostTable {} + +impl NativesCostTable { + pub fn from_protocol_config(protocol_config: &ProtocolConfig) -> NativesCostTable { + Self { + address_from_bytes_cost_params: AddressFromBytesCostParams { + address_from_bytes_cost_base: protocol_config.address_from_bytes_cost_base().into(), + }, + address_to_u256_cost_params: AddressToU256CostParams { + address_to_u256_cost_base: protocol_config.address_to_u256_cost_base().into(), + }, + address_from_u256_cost_params: AddressFromU256CostParams { + address_from_u256_cost_base: protocol_config.address_from_u256_cost_base().into(), + }, + + config_read_setting_impl_cost_params: ConfigReadSettingImplCostParams { + config_read_setting_impl_cost_base: protocol_config + .config_read_setting_impl_cost_base_as_option() + .map(Into::into), + config_read_setting_impl_cost_per_byte: protocol_config + .config_read_setting_impl_cost_per_byte_as_option() + .map(Into::into), + }, + + dynamic_field_hash_type_and_key_cost_params: DynamicFieldHashTypeAndKeyCostParams { + dynamic_field_hash_type_and_key_cost_base: protocol_config + .dynamic_field_hash_type_and_key_cost_base() + .into(), + dynamic_field_hash_type_and_key_type_cost_per_byte: protocol_config + .dynamic_field_hash_type_and_key_type_cost_per_byte() + .into(), + dynamic_field_hash_type_and_key_value_cost_per_byte: protocol_config + .dynamic_field_hash_type_and_key_value_cost_per_byte() + .into(), + dynamic_field_hash_type_and_key_type_tag_cost_per_byte: protocol_config + .dynamic_field_hash_type_and_key_type_tag_cost_per_byte() + .into(), + }, + dynamic_field_add_child_object_cost_params: DynamicFieldAddChildObjectCostParams { + dynamic_field_add_child_object_cost_base: protocol_config + .dynamic_field_add_child_object_cost_base() + .into(), + dynamic_field_add_child_object_type_cost_per_byte: protocol_config + .dynamic_field_add_child_object_type_cost_per_byte() + .into(), + dynamic_field_add_child_object_value_cost_per_byte: protocol_config + .dynamic_field_add_child_object_value_cost_per_byte() + .into(), + dynamic_field_add_child_object_struct_tag_cost_per_byte: protocol_config + .dynamic_field_add_child_object_struct_tag_cost_per_byte() + .into(), + }, + dynamic_field_borrow_child_object_cost_params: + DynamicFieldBorrowChildObjectCostParams { + dynamic_field_borrow_child_object_cost_base: protocol_config + .dynamic_field_borrow_child_object_cost_base() + .into(), + dynamic_field_borrow_child_object_child_ref_cost_per_byte: protocol_config + .dynamic_field_borrow_child_object_child_ref_cost_per_byte() + .into(), + dynamic_field_borrow_child_object_type_cost_per_byte: protocol_config + .dynamic_field_borrow_child_object_type_cost_per_byte() + .into(), + }, + dynamic_field_remove_child_object_cost_params: + DynamicFieldRemoveChildObjectCostParams { + dynamic_field_remove_child_object_cost_base: protocol_config + .dynamic_field_remove_child_object_cost_base() + .into(), + dynamic_field_remove_child_object_child_cost_per_byte: protocol_config + .dynamic_field_remove_child_object_child_cost_per_byte() + .into(), + dynamic_field_remove_child_object_type_cost_per_byte: protocol_config + .dynamic_field_remove_child_object_type_cost_per_byte() + .into(), + }, + dynamic_field_has_child_object_cost_params: DynamicFieldHasChildObjectCostParams { + dynamic_field_has_child_object_cost_base: protocol_config + .dynamic_field_has_child_object_cost_base() + .into(), + }, + dynamic_field_has_child_object_with_ty_cost_params: + DynamicFieldHasChildObjectWithTyCostParams { + dynamic_field_has_child_object_with_ty_cost_base: protocol_config + .dynamic_field_has_child_object_with_ty_cost_base() + .into(), + dynamic_field_has_child_object_with_ty_type_cost_per_byte: protocol_config + .dynamic_field_has_child_object_with_ty_type_cost_per_byte() + .into(), + dynamic_field_has_child_object_with_ty_type_tag_cost_per_byte: protocol_config + .dynamic_field_has_child_object_with_ty_type_tag_cost_per_byte() + .into(), + }, + + event_emit_cost_params: EventEmitCostParams { + event_emit_value_size_derivation_cost_per_byte: protocol_config + .event_emit_value_size_derivation_cost_per_byte() + .into(), + event_emit_tag_size_derivation_cost_per_byte: protocol_config + .event_emit_tag_size_derivation_cost_per_byte() + .into(), + event_emit_output_cost_per_byte: protocol_config + .event_emit_output_cost_per_byte() + .into(), + event_emit_cost_base: protocol_config.event_emit_cost_base().into(), + event_emit_auth_stream_cost: protocol_config + .event_emit_auth_stream_cost_as_option() + .map(Into::into), + }, + + borrow_uid_cost_params: BorrowUidCostParams { + object_borrow_uid_cost_base: protocol_config.object_borrow_uid_cost_base().into(), + }, + delete_impl_cost_params: DeleteImplCostParams { + object_delete_impl_cost_base: protocol_config.object_delete_impl_cost_base().into(), + }, + record_new_id_cost_params: RecordNewIdCostParams { + object_record_new_uid_cost_base: protocol_config + .object_record_new_uid_cost_base() + .into(), + }, + + // Crypto + crypto_invalid_arguments_cost: protocol_config.crypto_invalid_arguments_cost().into(), + // ed25519 + ed25519_verify_cost_params: Ed25519VerifyCostParams { + ed25519_ed25519_verify_cost_base: protocol_config + .ed25519_ed25519_verify_cost_base() + .into(), + ed25519_ed25519_verify_msg_cost_per_byte: protocol_config + .ed25519_ed25519_verify_msg_cost_per_byte() + .into(), + ed25519_ed25519_verify_msg_cost_per_block: protocol_config + .ed25519_ed25519_verify_msg_cost_per_block() + .into(), + }, + // hash + hash_blake2b256_cost_params: HashBlake2b256CostParams { + hash_blake2b256_cost_base: protocol_config.hash_blake2b256_cost_base().into(), + hash_blake2b256_data_cost_per_byte: protocol_config + .hash_blake2b256_data_cost_per_byte() + .into(), + hash_blake2b256_data_cost_per_block: protocol_config + .hash_blake2b256_data_cost_per_block() + .into(), + }, + hash_keccak256_cost_params: HashKeccak256CostParams { + hash_keccak256_cost_base: protocol_config.hash_keccak256_cost_base().into(), + hash_keccak256_data_cost_per_byte: protocol_config + .hash_keccak256_data_cost_per_byte() + .into(), + hash_keccak256_data_cost_per_block: protocol_config + .hash_keccak256_data_cost_per_block() + .into(), + }, + transfer_transfer_internal_cost_params: TransferInternalCostParams { + transfer_transfer_internal_cost_base: protocol_config + .transfer_transfer_internal_cost_base() + .into(), + }, + transfer_party_transfer_internal_cost_params: PartyTransferInternalCostParams { + transfer_party_transfer_internal_cost_base: protocol_config + .transfer_party_transfer_internal_cost_base_as_option() + .map(Into::into), + }, + transfer_freeze_object_cost_params: TransferFreezeObjectCostParams { + transfer_freeze_object_cost_base: protocol_config + .transfer_freeze_object_cost_base() + .into(), + }, + transfer_share_object_cost_params: TransferShareObjectCostParams { + transfer_share_object_cost_base: protocol_config + .transfer_share_object_cost_base() + .into(), + }, + // tx_context + tx_context_derive_id_cost_params: TxContextDeriveIdCostParams { + tx_context_derive_id_cost_base: protocol_config + .tx_context_derive_id_cost_base() + .into(), + }, + tx_context_fresh_id_cost_params: TxContextFreshIdCostParams { + tx_context_fresh_id_cost_base: if protocol_config.move_native_context() { + protocol_config.tx_context_fresh_id_cost_base().into() + } else { + DEFAULT_UNUSED_TX_CONTEXT_ENTRY_COST.into() + }, + }, + tx_context_sender_cost_params: TxContextSenderCostParams { + tx_context_sender_cost_base: if protocol_config.move_native_context() { + protocol_config.tx_context_sender_cost_base().into() + } else { + DEFAULT_UNUSED_TX_CONTEXT_ENTRY_COST.into() + }, + }, + tx_context_epoch_cost_params: TxContextEpochCostParams { + tx_context_epoch_cost_base: if protocol_config.move_native_context() { + protocol_config.tx_context_epoch_cost_base().into() + } else { + DEFAULT_UNUSED_TX_CONTEXT_ENTRY_COST.into() + }, + }, + tx_context_epoch_timestamp_ms_cost_params: TxContextEpochTimestampMsCostParams { + tx_context_epoch_timestamp_ms_cost_base: if protocol_config.move_native_context() { + protocol_config + .tx_context_epoch_timestamp_ms_cost_base() + .into() + } else { + DEFAULT_UNUSED_TX_CONTEXT_ENTRY_COST.into() + }, + }, + tx_context_sponsor_cost_params: TxContextSponsorCostParams { + tx_context_sponsor_cost_base: if protocol_config.move_native_context() { + protocol_config.tx_context_sponsor_cost_base().into() + } else { + DEFAULT_UNUSED_TX_CONTEXT_ENTRY_COST.into() + }, + }, + tx_context_rgp_cost_params: TxContextRGPCostParams { + tx_context_rgp_cost_base: protocol_config + .tx_context_rgp_cost_base_as_option() + .unwrap_or(DEFAULT_UNUSED_TX_CONTEXT_ENTRY_COST) + .into(), + }, + tx_context_gas_price_cost_params: TxContextGasPriceCostParams { + tx_context_gas_price_cost_base: if protocol_config.move_native_context() { + protocol_config.tx_context_gas_price_cost_base().into() + } else { + DEFAULT_UNUSED_TX_CONTEXT_ENTRY_COST.into() + }, + }, + tx_context_gas_budget_cost_params: TxContextGasBudgetCostParams { + tx_context_gas_budget_cost_base: if protocol_config.move_native_context() { + protocol_config.tx_context_gas_budget_cost_base().into() + } else { + DEFAULT_UNUSED_TX_CONTEXT_ENTRY_COST.into() + }, + }, + tx_context_ids_created_cost_params: TxContextIdsCreatedCostParams { + tx_context_ids_created_cost_base: if protocol_config.move_native_context() { + protocol_config.tx_context_ids_created_cost_base().into() + } else { + DEFAULT_UNUSED_TX_CONTEXT_ENTRY_COST.into() + }, + }, + tx_context_replace_cost_params: TxContextReplaceCostParams { + tx_context_replace_cost_base: if protocol_config.move_native_context() { + protocol_config.tx_context_replace_cost_base().into() + } else { + DEFAULT_UNUSED_TX_CONTEXT_ENTRY_COST.into() + }, + }, + type_is_one_time_witness_cost_params: TypesIsOneTimeWitnessCostParams { + types_is_one_time_witness_cost_base: protocol_config + .types_is_one_time_witness_cost_base() + .into(), + types_is_one_time_witness_type_tag_cost_per_byte: protocol_config + .types_is_one_time_witness_type_tag_cost_per_byte() + .into(), + types_is_one_time_witness_type_cost_per_byte: protocol_config + .types_is_one_time_witness_type_cost_per_byte() + .into(), + }, + validator_validate_metadata_bcs_cost_params: ValidatorValidateMetadataBcsCostParams { + validator_validate_metadata_cost_base: protocol_config + .validator_validate_metadata_cost_base() + .into(), + validator_validate_metadata_data_cost_per_byte: protocol_config + .validator_validate_metadata_data_cost_per_byte() + .into(), + }, + bls12381_bls12381_min_sig_verify_cost_params: Bls12381Bls12381MinSigVerifyCostParams { + bls12381_bls12381_min_sig_verify_cost_base: protocol_config + .bls12381_bls12381_min_sig_verify_cost_base() + .into(), + bls12381_bls12381_min_sig_verify_msg_cost_per_byte: protocol_config + .bls12381_bls12381_min_sig_verify_msg_cost_per_byte() + .into(), + bls12381_bls12381_min_sig_verify_msg_cost_per_block: protocol_config + .bls12381_bls12381_min_sig_verify_msg_cost_per_block() + .into(), + }, + bls12381_bls12381_min_pk_verify_cost_params: Bls12381Bls12381MinPkVerifyCostParams { + bls12381_bls12381_min_pk_verify_cost_base: protocol_config + .bls12381_bls12381_min_pk_verify_cost_base() + .into(), + bls12381_bls12381_min_pk_verify_msg_cost_per_byte: protocol_config + .bls12381_bls12381_min_pk_verify_msg_cost_per_byte() + .into(), + bls12381_bls12381_min_pk_verify_msg_cost_per_block: protocol_config + .bls12381_bls12381_min_pk_verify_msg_cost_per_block() + .into(), + }, + ecdsa_k1_ecrecover_cost_params: EcdsaK1EcrecoverCostParams { + ecdsa_k1_ecrecover_keccak256_cost_base: protocol_config + .ecdsa_k1_ecrecover_keccak256_cost_base() + .into(), + ecdsa_k1_ecrecover_keccak256_msg_cost_per_byte: protocol_config + .ecdsa_k1_ecrecover_keccak256_msg_cost_per_byte() + .into(), + ecdsa_k1_ecrecover_keccak256_msg_cost_per_block: protocol_config + .ecdsa_k1_ecrecover_keccak256_msg_cost_per_block() + .into(), + ecdsa_k1_ecrecover_sha256_cost_base: protocol_config + .ecdsa_k1_ecrecover_sha256_cost_base() + .into(), + ecdsa_k1_ecrecover_sha256_msg_cost_per_byte: protocol_config + .ecdsa_k1_ecrecover_sha256_msg_cost_per_byte() + .into(), + ecdsa_k1_ecrecover_sha256_msg_cost_per_block: protocol_config + .ecdsa_k1_ecrecover_sha256_msg_cost_per_block() + .into(), + }, + ecdsa_k1_decompress_pubkey_cost_params: EcdsaK1DecompressPubkeyCostParams { + ecdsa_k1_decompress_pubkey_cost_base: protocol_config + .ecdsa_k1_decompress_pubkey_cost_base() + .into(), + }, + ecdsa_k1_secp256k1_verify_cost_params: EcdsaK1Secp256k1VerifyCostParams { + ecdsa_k1_secp256k1_verify_keccak256_cost_base: protocol_config + .ecdsa_k1_secp256k1_verify_keccak256_cost_base() + .into(), + ecdsa_k1_secp256k1_verify_keccak256_msg_cost_per_byte: protocol_config + .ecdsa_k1_secp256k1_verify_keccak256_msg_cost_per_byte() + .into(), + ecdsa_k1_secp256k1_verify_keccak256_msg_cost_per_block: protocol_config + .ecdsa_k1_secp256k1_verify_keccak256_msg_cost_per_block() + .into(), + ecdsa_k1_secp256k1_verify_sha256_cost_base: protocol_config + .ecdsa_k1_secp256k1_verify_sha256_cost_base() + .into(), + ecdsa_k1_secp256k1_verify_sha256_msg_cost_per_byte: protocol_config + .ecdsa_k1_secp256k1_verify_sha256_msg_cost_per_byte() + .into(), + ecdsa_k1_secp256k1_verify_sha256_msg_cost_per_block: protocol_config + .ecdsa_k1_secp256k1_verify_sha256_msg_cost_per_block() + .into(), + }, + ecdsa_r1_ecrecover_cost_params: EcdsaR1EcrecoverCostParams { + ecdsa_r1_ecrecover_keccak256_cost_base: protocol_config + .ecdsa_r1_ecrecover_keccak256_cost_base() + .into(), + ecdsa_r1_ecrecover_keccak256_msg_cost_per_byte: protocol_config + .ecdsa_r1_ecrecover_keccak256_msg_cost_per_byte() + .into(), + ecdsa_r1_ecrecover_keccak256_msg_cost_per_block: protocol_config + .ecdsa_r1_ecrecover_keccak256_msg_cost_per_block() + .into(), + ecdsa_r1_ecrecover_sha256_cost_base: protocol_config + .ecdsa_r1_ecrecover_sha256_cost_base() + .into(), + ecdsa_r1_ecrecover_sha256_msg_cost_per_byte: protocol_config + .ecdsa_r1_ecrecover_sha256_msg_cost_per_byte() + .into(), + ecdsa_r1_ecrecover_sha256_msg_cost_per_block: protocol_config + .ecdsa_r1_ecrecover_sha256_msg_cost_per_block() + .into(), + }, + ecdsa_r1_secp256_r1_verify_cost_params: EcdsaR1Secp256R1VerifyCostParams { + ecdsa_r1_secp256r1_verify_keccak256_cost_base: protocol_config + .ecdsa_r1_secp256r1_verify_keccak256_cost_base() + .into(), + ecdsa_r1_secp256r1_verify_keccak256_msg_cost_per_byte: protocol_config + .ecdsa_r1_secp256r1_verify_keccak256_msg_cost_per_byte() + .into(), + ecdsa_r1_secp256r1_verify_keccak256_msg_cost_per_block: protocol_config + .ecdsa_r1_secp256r1_verify_keccak256_msg_cost_per_block() + .into(), + ecdsa_r1_secp256r1_verify_sha256_cost_base: protocol_config + .ecdsa_r1_secp256r1_verify_sha256_cost_base() + .into(), + ecdsa_r1_secp256r1_verify_sha256_msg_cost_per_byte: protocol_config + .ecdsa_r1_secp256r1_verify_sha256_msg_cost_per_byte() + .into(), + ecdsa_r1_secp256r1_verify_sha256_msg_cost_per_block: protocol_config + .ecdsa_r1_secp256r1_verify_sha256_msg_cost_per_block() + .into(), + }, + ecvrf_ecvrf_verify_cost_params: EcvrfEcvrfVerifyCostParams { + ecvrf_ecvrf_verify_cost_base: protocol_config.ecvrf_ecvrf_verify_cost_base().into(), + ecvrf_ecvrf_verify_alpha_string_cost_per_byte: protocol_config + .ecvrf_ecvrf_verify_alpha_string_cost_per_byte() + .into(), + ecvrf_ecvrf_verify_alpha_string_cost_per_block: protocol_config + .ecvrf_ecvrf_verify_alpha_string_cost_per_block() + .into(), + }, + groth16_prepare_verifying_key_cost_params: Groth16PrepareVerifyingKeyCostParams { + groth16_prepare_verifying_key_bls12381_cost_base: protocol_config + .groth16_prepare_verifying_key_bls12381_cost_base() + .into(), + groth16_prepare_verifying_key_bn254_cost_base: protocol_config + .groth16_prepare_verifying_key_bn254_cost_base() + .into(), + }, + groth16_verify_groth16_proof_internal_cost_params: + Groth16VerifyGroth16ProofInternalCostParams { + groth16_verify_groth16_proof_internal_bls12381_cost_base: protocol_config + .groth16_verify_groth16_proof_internal_bls12381_cost_base() + .into(), + groth16_verify_groth16_proof_internal_bls12381_cost_per_public_input: + protocol_config + .groth16_verify_groth16_proof_internal_bls12381_cost_per_public_input() + .into(), + groth16_verify_groth16_proof_internal_bn254_cost_base: protocol_config + .groth16_verify_groth16_proof_internal_bn254_cost_base() + .into(), + groth16_verify_groth16_proof_internal_bn254_cost_per_public_input: + protocol_config + .groth16_verify_groth16_proof_internal_bn254_cost_per_public_input() + .into(), + groth16_verify_groth16_proof_internal_public_input_cost_per_byte: + protocol_config + .groth16_verify_groth16_proof_internal_public_input_cost_per_byte() + .into(), + }, + hmac_hmac_sha3_256_cost_params: HmacHmacSha3256CostParams { + hmac_hmac_sha3_256_cost_base: protocol_config.hmac_hmac_sha3_256_cost_base().into(), + hmac_hmac_sha3_256_input_cost_per_byte: protocol_config + .hmac_hmac_sha3_256_input_cost_per_byte() + .into(), + hmac_hmac_sha3_256_input_cost_per_block: protocol_config + .hmac_hmac_sha3_256_input_cost_per_block() + .into(), + }, + transfer_receive_object_internal_cost_params: TransferReceiveObjectInternalCostParams { + transfer_receive_object_internal_cost_base: protocol_config + .transfer_receive_object_cost_base_as_option() + .unwrap_or(0) + .into(), + }, + check_zklogin_id_cost_params: CheckZkloginIdCostParams { + check_zklogin_id_cost_base: protocol_config + .check_zklogin_id_cost_base_as_option() + .map(Into::into), + }, + check_zklogin_issuer_cost_params: CheckZkloginIssuerCostParams { + check_zklogin_issuer_cost_base: protocol_config + .check_zklogin_issuer_cost_base_as_option() + .map(Into::into), + }, + poseidon_bn254_cost_params: PoseidonBN254CostParams { + poseidon_bn254_cost_base: protocol_config + .poseidon_bn254_cost_base_as_option() + .map(Into::into), + poseidon_bn254_data_cost_per_block: protocol_config + .poseidon_bn254_cost_per_block_as_option() + .map(Into::into), + }, + group_ops_cost_params: GroupOpsCostParams { + bls12381_decode_scalar_cost: protocol_config + .group_ops_bls12381_decode_scalar_cost_as_option() + .map(Into::into), + bls12381_decode_g1_cost: protocol_config + .group_ops_bls12381_decode_g1_cost_as_option() + .map(Into::into), + bls12381_decode_g2_cost: protocol_config + .group_ops_bls12381_decode_g2_cost_as_option() + .map(Into::into), + bls12381_decode_gt_cost: protocol_config + .group_ops_bls12381_decode_gt_cost_as_option() + .map(Into::into), + bls12381_scalar_add_cost: protocol_config + .group_ops_bls12381_scalar_add_cost_as_option() + .map(Into::into), + bls12381_g1_add_cost: protocol_config + .group_ops_bls12381_g1_add_cost_as_option() + .map(Into::into), + bls12381_g2_add_cost: protocol_config + .group_ops_bls12381_g2_add_cost_as_option() + .map(Into::into), + bls12381_gt_add_cost: protocol_config + .group_ops_bls12381_gt_add_cost_as_option() + .map(Into::into), + bls12381_scalar_sub_cost: protocol_config + .group_ops_bls12381_scalar_sub_cost_as_option() + .map(Into::into), + bls12381_g1_sub_cost: protocol_config + .group_ops_bls12381_g1_sub_cost_as_option() + .map(Into::into), + bls12381_g2_sub_cost: protocol_config + .group_ops_bls12381_g2_sub_cost_as_option() + .map(Into::into), + bls12381_gt_sub_cost: protocol_config + .group_ops_bls12381_gt_sub_cost_as_option() + .map(Into::into), + bls12381_scalar_mul_cost: protocol_config + .group_ops_bls12381_scalar_mul_cost_as_option() + .map(Into::into), + bls12381_g1_mul_cost: protocol_config + .group_ops_bls12381_g1_mul_cost_as_option() + .map(Into::into), + bls12381_g2_mul_cost: protocol_config + .group_ops_bls12381_g2_mul_cost_as_option() + .map(Into::into), + bls12381_gt_mul_cost: protocol_config + .group_ops_bls12381_gt_mul_cost_as_option() + .map(Into::into), + bls12381_scalar_div_cost: protocol_config + .group_ops_bls12381_scalar_div_cost_as_option() + .map(Into::into), + bls12381_g1_div_cost: protocol_config + .group_ops_bls12381_g1_div_cost_as_option() + .map(Into::into), + bls12381_g2_div_cost: protocol_config + .group_ops_bls12381_g2_div_cost_as_option() + .map(Into::into), + bls12381_gt_div_cost: protocol_config + .group_ops_bls12381_gt_div_cost_as_option() + .map(Into::into), + bls12381_g1_hash_to_base_cost: protocol_config + .group_ops_bls12381_g1_hash_to_base_cost_as_option() + .map(Into::into), + bls12381_g2_hash_to_base_cost: protocol_config + .group_ops_bls12381_g2_hash_to_base_cost_as_option() + .map(Into::into), + bls12381_g1_hash_to_cost_per_byte: protocol_config + .group_ops_bls12381_g1_hash_to_cost_per_byte_as_option() + .map(Into::into), + bls12381_g2_hash_to_cost_per_byte: protocol_config + .group_ops_bls12381_g2_hash_to_cost_per_byte_as_option() + .map(Into::into), + bls12381_g1_msm_base_cost: protocol_config + .group_ops_bls12381_g1_msm_base_cost_as_option() + .map(Into::into), + bls12381_g2_msm_base_cost: protocol_config + .group_ops_bls12381_g2_msm_base_cost_as_option() + .map(Into::into), + bls12381_g1_msm_base_cost_per_input: protocol_config + .group_ops_bls12381_g1_msm_base_cost_per_input_as_option() + .map(Into::into), + bls12381_g2_msm_base_cost_per_input: protocol_config + .group_ops_bls12381_g2_msm_base_cost_per_input_as_option() + .map(Into::into), + bls12381_msm_max_len: protocol_config.group_ops_bls12381_msm_max_len_as_option(), + bls12381_pairing_cost: protocol_config + .group_ops_bls12381_pairing_cost_as_option() + .map(Into::into), + bls12381_g1_to_uncompressed_g1_cost: protocol_config + .group_ops_bls12381_g1_to_uncompressed_g1_cost_as_option() + .map(Into::into), + bls12381_uncompressed_g1_to_g1_cost: protocol_config + .group_ops_bls12381_uncompressed_g1_to_g1_cost_as_option() + .map(Into::into), + bls12381_uncompressed_g1_sum_base_cost: protocol_config + .group_ops_bls12381_uncompressed_g1_sum_base_cost_as_option() + .map(Into::into), + bls12381_uncompressed_g1_sum_cost_per_term: protocol_config + .group_ops_bls12381_uncompressed_g1_sum_cost_per_term_as_option() + .map(Into::into), + bls12381_uncompressed_g1_sum_max_terms: protocol_config + .group_ops_bls12381_uncompressed_g1_sum_max_terms_as_option(), + }, + vdf_cost_params: VDFCostParams { + vdf_verify_cost: protocol_config + .vdf_verify_vdf_cost_as_option() + .map(Into::into), + hash_to_input_cost: protocol_config + .vdf_hash_to_input_cost_as_option() + .map(Into::into), + }, + nitro_attestation_cost_params: NitroAttestationCostParams { + parse_base_cost: protocol_config + .nitro_attestation_parse_base_cost_as_option() + .map(Into::into), + parse_cost_per_byte: protocol_config + .nitro_attestation_parse_cost_per_byte_as_option() + .map(Into::into), + verify_base_cost: protocol_config + .nitro_attestation_verify_base_cost_as_option() + .map(Into::into), + verify_cost_per_cert: protocol_config + .nitro_attestation_verify_cost_per_cert_as_option() + .map(Into::into), + }, + } + } +} + +pub fn make_stdlib_gas_params_for_protocol_config( + protocol_config: &ProtocolConfig, +) -> GasParameters { + macro_rules! get_gas_cost_or_default { + ($name: ident) => {{ + debug_assert!( + protocol_config.version.as_u64() < 53 || protocol_config.$name().is_some() + ); + protocol_config.$name().map(Into::into).unwrap_or(0.into()) + }}; + } + GasParameters::new( + MSN::bcs::GasParameters { + to_bytes: MSN::bcs::ToBytesGasParameters { + per_byte_serialized: get_gas_cost_or_default!( + bcs_per_byte_serialized_cost_as_option + ), + legacy_min_output_size: get_gas_cost_or_default!( + bcs_legacy_min_output_size_cost_as_option + ), + failure: get_gas_cost_or_default!(bcs_failure_cost_as_option), + }, + }, + MSN::debug::GasParameters { + print: MSN::debug::PrintGasParameters { + base_cost: get_gas_cost_or_default!(debug_print_base_cost_as_option), + }, + print_stack_trace: MSN::debug::PrintStackTraceGasParameters { + base_cost: get_gas_cost_or_default!(debug_print_stack_trace_base_cost_as_option), + }, + }, + MSN::hash::GasParameters { + sha2_256: MSN::hash::Sha2_256GasParameters { + base: get_gas_cost_or_default!(hash_sha2_256_base_cost_as_option), + per_byte: get_gas_cost_or_default!(hash_sha2_256_per_byte_cost_as_option), + legacy_min_input_len: get_gas_cost_or_default!( + hash_sha2_256_legacy_min_input_len_cost_as_option + ), + }, + sha3_256: MSN::hash::Sha3_256GasParameters { + base: get_gas_cost_or_default!(hash_sha3_256_base_cost_as_option), + per_byte: get_gas_cost_or_default!(hash_sha3_256_per_byte_cost_as_option), + legacy_min_input_len: get_gas_cost_or_default!( + hash_sha3_256_legacy_min_input_len_cost_as_option + ), + }, + }, + MSN::string::GasParameters { + check_utf8: MSN::string::CheckUtf8GasParameters { + base: get_gas_cost_or_default!(string_check_utf8_base_cost_as_option), + per_byte: get_gas_cost_or_default!(string_check_utf8_per_byte_cost_as_option), + }, + is_char_boundary: MSN::string::IsCharBoundaryGasParameters { + base: get_gas_cost_or_default!(string_is_char_boundary_base_cost_as_option), + }, + sub_string: MSN::string::SubStringGasParameters { + base: get_gas_cost_or_default!(string_sub_string_base_cost_as_option), + per_byte: get_gas_cost_or_default!(string_sub_string_per_byte_cost_as_option), + }, + index_of: MSN::string::IndexOfGasParameters { + base: get_gas_cost_or_default!(string_index_of_base_cost_as_option), + per_byte_pattern: get_gas_cost_or_default!( + string_index_of_per_byte_pattern_cost_as_option + ), + per_byte_searched: get_gas_cost_or_default!( + string_index_of_per_byte_searched_cost_as_option + ), + }, + }, + MSN::type_name::GasParameters { + get: MSN::type_name::GetGasParameters { + base: get_gas_cost_or_default!(type_name_get_base_cost_as_option), + per_byte: get_gas_cost_or_default!(type_name_get_per_byte_cost_as_option), + }, + id: MSN::type_name::IdGasParameters::new( + protocol_config.type_name_id_base_cost_as_option(), + ), + }, + MSN::vector::GasParameters { + empty: MSN::vector::EmptyGasParameters { + base: get_gas_cost_or_default!(vector_empty_base_cost_as_option), + }, + length: MSN::vector::LengthGasParameters { + base: get_gas_cost_or_default!(vector_length_base_cost_as_option), + }, + push_back: MSN::vector::PushBackGasParameters { + base: get_gas_cost_or_default!(vector_push_back_base_cost_as_option), + legacy_per_abstract_memory_unit: get_gas_cost_or_default!( + vector_push_back_legacy_per_abstract_memory_unit_cost_as_option + ), + }, + borrow: MSN::vector::BorrowGasParameters { + base: get_gas_cost_or_default!(vector_borrow_base_cost_as_option), + }, + pop_back: MSN::vector::PopBackGasParameters { + base: get_gas_cost_or_default!(vector_pop_back_base_cost_as_option), + }, + destroy_empty: MSN::vector::DestroyEmptyGasParameters { + base: get_gas_cost_or_default!(vector_destroy_empty_base_cost_as_option), + }, + swap: MSN::vector::SwapGasParameters { + base: get_gas_cost_or_default!(vector_swap_base_cost_as_option), + }, + }, + ) +} + +pub fn all_natives(silent: bool, protocol_config: &ProtocolConfig) -> NativeFunctionTable { + let sui_framework_natives: &[(&str, &str, NativeFunction)] = &[ + ( + "accumulator", + "emit_deposit_event", + make_native!(accumulator::emit_deposit_event), + ), + ( + "accumulator", + "emit_withdraw_event", + make_native!(accumulator::emit_withdraw_event), + ), + ( + "accumulator_settlement", + "record_settlement_sui_conservation", + make_native!(accumulator::record_settlement_sui_conservation), + ), + ("address", "from_bytes", make_native!(address::from_bytes)), + ("address", "to_u256", make_native!(address::to_u256)), + ("address", "from_u256", make_native!(address::from_u256)), + ("hash", "blake2b256", make_native!(hash::blake2b256)), + ( + "bls12381", + "bls12381_min_sig_verify", + make_native!(bls12381::bls12381_min_sig_verify), + ), + ( + "bls12381", + "bls12381_min_pk_verify", + make_native!(bls12381::bls12381_min_pk_verify), + ), + ( + "dynamic_field", + "hash_type_and_key", + make_native!(dynamic_field::hash_type_and_key), + ), + ( + "config", + "read_setting_impl", + make_native!(config::read_setting_impl), + ), + ( + "dynamic_field", + "add_child_object", + make_native!(dynamic_field::add_child_object), + ), + ( + "dynamic_field", + "borrow_child_object", + make_native!(dynamic_field::borrow_child_object), + ), + ( + "dynamic_field", + "borrow_child_object_mut", + make_native!(dynamic_field::borrow_child_object), + ), + ( + "dynamic_field", + "remove_child_object", + make_native!(dynamic_field::remove_child_object), + ), + ( + "dynamic_field", + "has_child_object", + make_native!(dynamic_field::has_child_object), + ), + ( + "dynamic_field", + "has_child_object_with_ty", + make_native!(dynamic_field::has_child_object_with_ty), + ), + ( + "ecdsa_k1", + "secp256k1_ecrecover", + make_native!(ecdsa_k1::ecrecover), + ), + ( + "ecdsa_k1", + "decompress_pubkey", + make_native!(ecdsa_k1::decompress_pubkey), + ), + ( + "ecdsa_k1", + "secp256k1_verify", + make_native!(ecdsa_k1::secp256k1_verify), + ), + ("ecvrf", "ecvrf_verify", make_native!(ecvrf::ecvrf_verify)), + ( + "ecdsa_r1", + "secp256r1_ecrecover", + make_native!(ecdsa_r1::ecrecover), + ), + ( + "ecdsa_r1", + "secp256r1_verify", + make_native!(ecdsa_r1::secp256r1_verify), + ), + ( + "ed25519", + "ed25519_verify", + make_native!(ed25519::ed25519_verify), + ), + ("event", "emit", make_native!(event::emit)), + ( + "event", + "emit_authenticated_impl", + make_native!(event::emit_authenticated_impl), + ), + ( + "event", + "events_by_type", + make_native!(event::get_events_by_type), + ), + ("event", "num_events", make_native!(event::num_events)), + ( + "funds_accumulator", + "add_to_accumulator_address", + make_native!(funds_accumulator::add_to_accumulator_address), + ), + ( + "funds_accumulator", + "withdraw_from_accumulator_address", + make_native!(funds_accumulator::withdraw_from_accumulator_address), + ), + ( + "groth16", + "verify_groth16_proof_internal", + make_native!(groth16::verify_groth16_proof_internal), + ), + ( + "groth16", + "prepare_verifying_key_internal", + make_native!(groth16::prepare_verifying_key_internal), + ), + ("hmac", "hmac_sha3_256", make_native!(hmac::hmac_sha3_256)), + ("hash", "keccak256", make_native!(hash::keccak256)), + ( + "group_ops", + "internal_validate", + make_native!(group_ops::internal_validate), + ), + ( + "group_ops", + "internal_add", + make_native!(group_ops::internal_add), + ), + ( + "group_ops", + "internal_sub", + make_native!(group_ops::internal_sub), + ), + ( + "group_ops", + "internal_mul", + make_native!(group_ops::internal_mul), + ), + ( + "group_ops", + "internal_div", + make_native!(group_ops::internal_div), + ), + ( + "group_ops", + "internal_hash_to", + make_native!(group_ops::internal_hash_to), + ), + ( + "group_ops", + "internal_multi_scalar_mul", + make_native!(group_ops::internal_multi_scalar_mul), + ), + ( + "group_ops", + "internal_pairing", + make_native!(group_ops::internal_pairing), + ), + ( + "group_ops", + "internal_convert", + make_native!(group_ops::internal_convert), + ), + ( + "group_ops", + "internal_sum", + make_native!(group_ops::internal_sum), + ), + ("object", "delete_impl", make_native!(object::delete_impl)), + ("object", "borrow_uid", make_native!(object::borrow_uid)), + ( + "object", + "record_new_uid", + make_native!(object::record_new_uid), + ), + ( + "test_scenario", + "take_from_address_by_id", + make_native!(test_scenario::take_from_address_by_id), + ), + ( + "test_scenario", + "most_recent_id_for_address", + make_native!(test_scenario::most_recent_id_for_address), + ), + ( + "test_scenario", + "was_taken_from_address", + make_native!(test_scenario::was_taken_from_address), + ), + ( + "test_scenario", + "take_immutable_by_id", + make_native!(test_scenario::take_immutable_by_id), + ), + ( + "test_scenario", + "most_recent_immutable_id", + make_native!(test_scenario::most_recent_immutable_id), + ), + ( + "test_scenario", + "was_taken_immutable", + make_native!(test_scenario::was_taken_immutable), + ), + ( + "test_scenario", + "take_shared_by_id", + make_native!(test_scenario::take_shared_by_id), + ), + ( + "test_scenario", + "most_recent_id_shared", + make_native!(test_scenario::most_recent_id_shared), + ), + ( + "test_scenario", + "was_taken_shared", + make_native!(test_scenario::was_taken_shared), + ), + ( + "test_scenario", + "end_transaction", + make_native!(test_scenario::end_transaction), + ), + ( + "test_scenario", + "ids_for_address", + make_native!(test_scenario::ids_for_address), + ), + ( + "test_scenario", + "allocate_receiving_ticket_for_object", + make_native!(test_scenario::allocate_receiving_ticket_for_object), + ), + ( + "test_scenario", + "deallocate_receiving_ticket_for_object", + make_native!(test_scenario::deallocate_receiving_ticket_for_object), + ), + ( + "transfer", + "transfer_impl", + make_native!(transfer::transfer_internal), + ), + ( + "transfer", + "party_transfer_impl", + make_native!(transfer::party_transfer_internal), + ), + ( + "transfer", + "freeze_object_impl", + make_native!(transfer::freeze_object), + ), + ( + "transfer", + "share_object_impl", + make_native!(transfer::share_object), + ), + ( + "transfer", + "receive_impl", + make_native!(transfer::receive_object_internal), + ), + ( + "tx_context", + "last_created_id", + make_native!(tx_context::last_created_id), + ), + ( + "tx_context", + "derive_id", + make_native!(tx_context::derive_id), + ), + ("tx_context", "fresh_id", make_native!(tx_context::fresh_id)), + ( + "tx_context", + "native_sender", + make_native!(tx_context::sender), + ), + ( + "tx_context", + "native_epoch", + make_native!(tx_context::epoch), + ), + ( + "tx_context", + "native_epoch_timestamp_ms", + make_native!(tx_context::epoch_timestamp_ms), + ), + ( + "tx_context", + "native_sponsor", + make_native!(tx_context::sponsor), + ), + ("tx_context", "native_rgp", make_native!(tx_context::rgp)), + ( + "tx_context", + "native_gas_price", + make_native!(tx_context::gas_price), + ), + ( + "tx_context", + "native_gas_budget", + make_native!(tx_context::gas_budget), + ), + ( + "tx_context", + "native_ids_created", + make_native!(tx_context::ids_created), + ), + ("tx_context", "replace", make_native!(tx_context::replace)), + ( + "types", + "is_one_time_witness", + make_native!(types::is_one_time_witness), + ), + ( + "test_utils", + "create_one_time_witness", + make_native!(test_utils::create_one_time_witness), + ), + ( + "random", + "generate_rand_seed_for_testing", + make_native!(random::generate_rand_seed_for_testing), + ), + ( + "zklogin_verified_id", + "check_zklogin_id_internal", + make_native!(zklogin::check_zklogin_id_internal), + ), + ( + "zklogin_verified_issuer", + "check_zklogin_issuer_internal", + make_native!(zklogin::check_zklogin_issuer_internal), + ), + ( + "poseidon", + "poseidon_bn254_internal", + make_native!(poseidon::poseidon_bn254_internal), + ), + ( + "vdf", + "vdf_verify_internal", + make_native!(vdf::vdf_verify_internal), + ), + ( + "vdf", + "hash_to_input_internal", + make_native!(vdf::hash_to_input_internal), + ), + ( + "ecdsa_k1", + "secp256k1_sign", + make_native!(ecdsa_k1::secp256k1_sign), + ), + ( + "ecdsa_k1", + "secp256k1_keypair_from_seed", + make_native!(ecdsa_k1::secp256k1_keypair_from_seed), + ), + ( + "nitro_attestation", + "load_nitro_attestation_internal", + make_native!(nitro_attestation::load_nitro_attestation_internal), + ), + ]; + let sui_framework_natives_iter = + sui_framework_natives + .iter() + .cloned() + .map(|(module_name, func_name, func)| { + ( + SUI_FRAMEWORK_ADDRESS, + Identifier::new(module_name).unwrap(), + Identifier::new(func_name).unwrap(), + func, + ) + }); + let sui_system_natives: &[(&str, &str, NativeFunction)] = &[( + "validator", + "validate_metadata_bcs", + make_native!(validator::validate_metadata_bcs), + )]; + sui_system_natives + .iter() + .cloned() + .map(|(module_name, func_name, func)| { + ( + SUI_SYSTEM_ADDRESS, + Identifier::new(module_name).unwrap(), + Identifier::new(func_name).unwrap(), + func, + ) + }) + .chain(sui_framework_natives_iter) + .chain(move_stdlib_natives::all_natives( + MOVE_STDLIB_ADDRESS, + make_stdlib_gas_params_for_protocol_config(protocol_config), + silent, + )) + .collect() +} + +// ID { bytes: address } +// Extract the first field of the struct to get the address bytes. +pub fn get_receiver_object_id(object: Value) -> Result { + get_nested_struct_field(object, &[0]) +} + +// Object { id: UID { id: ID { bytes: address } } .. } +// Extract the first field of the struct 3 times to get the id bytes. +pub fn get_object_id(object: Value) -> Result { + get_nested_struct_field(object, &[0, 0, 0]) +} + +// Extract a field value that's nested inside value `v`. The offset of each nesting +// is determined by `offsets`. +pub fn get_nested_struct_field(mut v: Value, offsets: &[usize]) -> Result { + for offset in offsets { + v = get_nth_struct_field(v, *offset)?; + } + Ok(v) +} + +pub fn get_nth_struct_field(v: Value, n: usize) -> Result { + let mut itr = v.value_as::()?.unpack()?; + Ok(itr.nth(n).unwrap()) +} + +/// Returns the struct tag, non-annotated type layout, and fully annotated type layout of `ty`. +pub(crate) fn get_tag_and_layouts( + context: &NativeContext, + ty: &Type, +) -> PartialVMResult> { + let tag = match context.type_to_type_tag(ty)? { + TypeTag::Struct(s) => s, + _ => { + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("Sui verifier guarantees this is a struct".to_string()), + ); + } + }; + let Some(layout) = context.type_to_type_layout(ty)? else { + return Ok(None); + }; + let Some(annotated_layout) = context.type_to_fully_annotated_layout(ty)? else { + return Ok(None); + }; + Ok(Some((*tag, layout, annotated_layout))) +} + +#[macro_export] +macro_rules! make_native { + ($native: expr) => { + Arc::new( + move |context, ty_args, args| -> PartialVMResult { + $native(context, ty_args, args) + }, + ) + }; +} + +#[macro_export] +macro_rules! get_extension { + ($context: expr, $ext: ty) => { + $context.extensions().get::<$ext>() + }; + ($context: expr) => { + $context.extensions().get() + }; +} + +#[macro_export] +macro_rules! get_extension_mut { + ($context: expr, $ext: ty) => { + $context.extensions_mut().get_mut::<$ext>() + }; + ($context: expr) => { + $context.extensions_mut().get_mut() + }; +} + +#[macro_export] +macro_rules! charge_cache_or_load_gas { + ($context:ident, $cache_info:expr) => {{ + use sui_types::base_types::SUI_ADDRESS_LENGTH; + use $crate::object_runtime::object_store::CacheInfo; + match $cache_info { + CacheInfo::CachedObject | CacheInfo::CachedValue => (), + CacheInfo::Loaded(bytes_opt) => { + let config = get_extension!($context, ObjectRuntime)?.protocol_config; + if config.object_runtime_charge_cache_load_gas() { + let bytes = bytes_opt.unwrap_or(SUI_ADDRESS_LENGTH as usize); + let cost = 2 * bytes * config.obj_access_cost_read_per_byte() as usize; + native_charge_gas_early_exit!($context, InternalGas::new(cost as u64)); + } + } + } + }}; +} + +pub(crate) fn legacy_test_cost() -> InternalGas { + InternalGas::new(0) +} + +pub(crate) fn abstract_size(protocol_config: &ProtocolConfig, v: &Value) -> AbstractMemorySize { + if protocol_config.abstract_size_in_object_runtime() { + v.abstract_memory_size(&SizeConfig { + include_vector_size: true, + traverse_references: false, + }) + } else { + // TODO: Remove this (with glee!) in the next execution version cut. + v.legacy_size() + } +} diff --git a/sui-execution/replay_cut/sui-move-natives/src/object.rs b/sui-execution/replay_cut/sui-move-natives/src/object.rs new file mode 100644 index 0000000000000..d8133ff3a07f1 --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/object.rs @@ -0,0 +1,115 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::{NativesCostTable, get_extension, get_extension_mut, object_runtime::ObjectRuntime}; +use move_binary_format::errors::PartialVMResult; +use move_core_types::{account_address::AccountAddress, gas_algebra::InternalGas}; +use move_vm_runtime::{native_charge_gas_early_exit, native_functions::NativeContext}; +use move_vm_types::{ + loaded_data::runtime_types::Type, + natives::function::NativeResult, + pop_arg, + values::{StructRef, Value}, +}; +use smallvec::smallvec; +use std::collections::VecDeque; + +#[derive(Clone)] +pub struct BorrowUidCostParams { + pub object_borrow_uid_cost_base: InternalGas, +} +/*************************************************************************************************** + * native fun borrow_uid + * Implementation of the Move native function `borrow_uid(obj: &T): &UID` + * gas cost: object_borrow_uid_cost_base | this is hard to calculate as it's very sensitive to `borrow_field` impl. Making it flat + **************************************************************************************************/ +pub fn borrow_uid( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.len() == 1); + debug_assert!(args.len() == 1); + + let borrow_uid_cost_params = get_extension!(context, NativesCostTable)? + .borrow_uid_cost_params + .clone(); + + // Charge base fee + native_charge_gas_early_exit!(context, borrow_uid_cost_params.object_borrow_uid_cost_base); + + let obj = pop_arg!(args, StructRef); + let id_field = obj.borrow_field(0)?; + + Ok(NativeResult::ok(context.gas_used(), smallvec![id_field])) +} + +#[derive(Clone)] +pub struct DeleteImplCostParams { + pub object_delete_impl_cost_base: InternalGas, +} +/*************************************************************************************************** + * native fun delete_impl + * Implementation of the Move native function `delete_impl(id: address)` + * gas cost: cost_base | this is a simple ID deletion + **************************************************************************************************/ +pub fn delete_impl( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 1); + + let delete_impl_cost_params = get_extension!(context, NativesCostTable)? + .delete_impl_cost_params + .clone(); + + // Charge base fee + native_charge_gas_early_exit!( + context, + delete_impl_cost_params.object_delete_impl_cost_base + ); + + // unwrap safe because the interface of native function guarantees it. + let uid_bytes = pop_arg!(args, AccountAddress); + + let obj_runtime: &mut ObjectRuntime = get_extension_mut!(context)?; + obj_runtime.delete_id(uid_bytes.into())?; + Ok(NativeResult::ok(context.gas_used(), smallvec![])) +} + +#[derive(Clone)] +pub struct RecordNewIdCostParams { + pub object_record_new_uid_cost_base: InternalGas, +} +/*************************************************************************************************** + * native fun record_new_uid + * Implementation of the Move native function `record_new_uid(id: address)` + * gas cost: object_record_new_uid_cost_base | this is a simple ID addition + **************************************************************************************************/ +pub fn record_new_uid( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 1); + + let record_new_id_cost_params = get_extension!(context, NativesCostTable)? + .record_new_id_cost_params + .clone(); + + // Charge base fee + native_charge_gas_early_exit!( + context, + record_new_id_cost_params.object_record_new_uid_cost_base + ); + + // unwrap safe because the interface of native function guarantees it. + let uid_bytes = pop_arg!(args, AccountAddress); + + let obj_runtime: &mut ObjectRuntime = get_extension_mut!(context)?; + obj_runtime.new_id(uid_bytes.into())?; + Ok(NativeResult::ok(context.gas_used(), smallvec![])) +} diff --git a/sui-execution/replay_cut/sui-move-natives/src/object_runtime/accumulator.rs b/sui-execution/replay_cut/sui-move-natives/src/object_runtime/accumulator.rs new file mode 100644 index 0000000000000..63c34e9fa1392 --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/object_runtime/accumulator.rs @@ -0,0 +1,38 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use move_core_types::account_address::AccountAddress; +use sui_types::{TypeTag, base_types::ObjectID, effects::AccumulatorOperation}; + +#[derive(Debug)] +pub enum MoveAccumulatorAction { + Merge, + Split, +} + +impl MoveAccumulatorAction { + pub fn into_sui_accumulator_action(self) -> AccumulatorOperation { + match self { + MoveAccumulatorAction::Merge => AccumulatorOperation::Merge, + MoveAccumulatorAction::Split => AccumulatorOperation::Split, + } + } +} + +#[derive(Debug)] +pub enum MoveAccumulatorValue { + U64(u64), + // commit the nth event emitted by the transaction to an event stream + EventRef(u64), +} + +#[derive(Debug)] +pub struct MoveAccumulatorEvent { + // Note: accumulator_id is derived by hashing target and ty, but we include + // both for simplicity. + pub accumulator_id: ObjectID, + pub action: MoveAccumulatorAction, + pub target_addr: AccountAddress, + pub target_ty: TypeTag, + pub value: MoveAccumulatorValue, +} diff --git a/sui-execution/replay_cut/sui-move-natives/src/object_runtime/fingerprint.rs b/sui-execution/replay_cut/sui-move-natives/src/object_runtime/fingerprint.rs new file mode 100644 index 0000000000000..0e597b12226e8 --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/object_runtime/fingerprint.rs @@ -0,0 +1,100 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use move_binary_format::errors::{PartialVMError, PartialVMResult}; +use move_core_types::vm_status::StatusCode; +use move_vm_types::values::Value; +use sui_protocol_config::ProtocolConfig; +use sui_types::base_types::{MoveObjectType, ObjectID}; + +/// This type is used to track if an object has changed since it was read from storage. Ideally, +/// this would just store the owner ID+type+BCS bytes of the object; however, due to pending +/// rewrites in the adapter, that would be too much code churn. Instead, we use the `Value` since +/// the `RuntimeResults` operate over `Value` and not BCS bytes. +pub struct ObjectFingerprint(Option); + +enum ObjectFingerprint_ { + /// The object did not exist (as a child object) in storage at the start of the transaction. + Empty, + // The object was loaded as a child object from storage. + Preexisting { + owner: ObjectID, + ty: MoveObjectType, + value: Value, + }, +} + +impl ObjectFingerprint { + #[cfg(debug_assertions)] + pub fn is_disabled(&self) -> bool { + self.0.is_none() + } + + /// Creates a new object fingerprint for a child object not found in storage. + /// Will be internally disabled if the feature is not enabled in the protocol config. + pub fn none(protocol_config: &ProtocolConfig) -> Self { + if !protocol_config.minimize_child_object_mutations() { + Self(None) + } else { + Self(Some(ObjectFingerprint_::Empty)) + } + } + + /// Creates a new object fingerprint for a child found in storage. + /// Will be internally disabled if the feature is not enabled in the protocol config. + pub fn preexisting( + protocol_config: &ProtocolConfig, + preexisting_owner: &ObjectID, + preexisting_type: &MoveObjectType, + preexisting_value: &Value, + ) -> PartialVMResult { + Ok(if !protocol_config.minimize_child_object_mutations() { + Self(None) + } else { + Self(Some(ObjectFingerprint_::Preexisting { + owner: *preexisting_owner, + ty: preexisting_type.clone(), + value: preexisting_value.copy_value()?, + })) + }) + } + + /// Checks if the object has changed since it was read from storage. + /// Gives an invariant violation if the fingerprint is disabled. + /// Gives an invariant violation if the values do not have the same layout, but the owner and + /// type are thesame. + pub fn object_has_changed( + &self, + final_owner: &ObjectID, + final_type: &MoveObjectType, + final_value: &Option, + ) -> PartialVMResult { + use ObjectFingerprint_ as F; + let Some(inner) = &self.0 else { + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR).with_message( + "Object fingerprint not enabled, yet we were asked for the changes".to_string(), + ), + ); + }; + Ok(match (inner, final_value) { + (F::Empty, None) => false, + (F::Empty, Some(_)) | (F::Preexisting { .. }, None) => true, + ( + F::Preexisting { + owner: preexisting_owner, + ty: preexisting_type, + value: preexisting_value, + }, + Some(final_value), + ) => { + // owner changed or value changed. + // For the value, we must first check if the types are the same before comparing the + // values + !(preexisting_owner == final_owner + && preexisting_type == final_type + && preexisting_value.equals(final_value)?) + } + }) + } +} diff --git a/sui-execution/replay_cut/sui-move-natives/src/object_runtime/mod.rs b/sui-execution/replay_cut/sui-move-natives/src/object_runtime/mod.rs new file mode 100644 index 0000000000000..0abeb0878bfe7 --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/object_runtime/mod.rs @@ -0,0 +1,1002 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +pub(crate) mod accumulator; +mod fingerprint; +pub(crate) mod object_store; + +use crate::object_runtime::object_store::{CacheMetadata, ChildObjectEffectV1}; + +use self::object_store::{ChildObjectEffectV0, ChildObjectEffects, ObjectResult}; +use super::get_object_id; +use better_any::{Tid, TidAble}; +use indexmap::map::IndexMap; +use indexmap::set::IndexSet; +use move_binary_format::errors::{PartialVMError, PartialVMResult}; +use move_core_types::{ + account_address::AccountAddress, + annotated_value::{MoveTypeLayout, MoveValue}, + annotated_visitor as AV, + language_storage::StructTag, + runtime_value as R, + vm_status::StatusCode, +}; +use move_vm_runtime::native_extensions::NativeExtensionMarker; +use move_vm_types::{ + effects::Op, + values::{GlobalValue, Value}, +}; +use object_store::{ActiveChildObject, ChildObjectStore}; +use std::{ + collections::{BTreeMap, BTreeSet}, + sync::Arc, +}; +use sui_protocol_config::{LimitThresholdCrossed, ProtocolConfig, check_limit_by_meter}; +use sui_types::{ + SUI_ACCUMULATOR_ROOT_OBJECT_ID, SUI_AUTHENTICATOR_STATE_OBJECT_ID, SUI_BRIDGE_OBJECT_ID, + SUI_CLOCK_OBJECT_ID, SUI_COIN_REGISTRY_OBJECT_ID, SUI_DENY_LIST_OBJECT_ID, + SUI_DISPLAY_REGISTRY_OBJECT_ID, SUI_RANDOMNESS_STATE_OBJECT_ID, SUI_SYSTEM_STATE_OBJECT_ID, + TypeTag, + base_types::{MoveObjectType, ObjectID, SequenceNumber, SuiAddress}, + committee::EpochId, + error::{ExecutionError, ExecutionErrorKind, VMMemoryLimitExceededSubStatusCode}, + execution::DynamicallyLoadedObjectMetadata, + id::UID, + metrics::LimitsMetrics, + object::{MoveObject, Owner}, + storage::ChildObjectResolver, +}; +use tracing::error; + +pub use accumulator::*; + +pub enum ObjectEvent { + /// Transfer to a new address or object. Or make it shared or immutable. + Transfer(Owner, MoveObject), + /// An object ID is deleted + DeleteObjectID(ObjectID), +} + +type Set = IndexSet; + +#[derive(Default)] +pub(crate) struct TestInventories { + pub(crate) objects: BTreeMap, + // address inventories. Most recent objects are at the back of the set + pub(crate) address_inventories: BTreeMap>>, + // global inventories.Most recent objects are at the back of the set + pub(crate) shared_inventory: BTreeMap>, + pub(crate) immutable_inventory: BTreeMap>, + pub(crate) taken_immutable_values: BTreeMap>, + // object has been taken from the inventory + pub(crate) taken: BTreeMap, + // allocated receiving tickets + pub(crate) allocated_tickets: BTreeMap, +} + +pub struct LoadedRuntimeObject { + pub version: SequenceNumber, + pub is_modified: bool, +} + +pub struct RuntimeResults { + pub writes: IndexMap, + pub user_events: Vec<(StructTag, Value)>, + pub accumulator_events: Vec, + // Loaded child objects, their loaded version/digest and whether they were modified. + pub loaded_child_objects: BTreeMap, + pub created_object_ids: Set, + pub deleted_object_ids: Set, + pub settlement_input_sui: u64, + pub settlement_output_sui: u64, +} + +#[derive(Default)] +pub(crate) struct ObjectRuntimeState { + pub(crate) input_objects: BTreeMap, + // new ids from object::new. This does not contain any new-and-subsequently-deleted ids + new_ids: Set, + // contains all ids generated in the txn including any new-and-subsequently-deleted ids + generated_ids: Set, + // ids passed to object::delete + deleted_ids: Set, + // transfers to a new owner (shared, immutable, object, or account address) + // TODO these struct tags can be removed if type_to_type_tag was exposed in the session + transfers: IndexMap, + events: Vec<(StructTag, Value)>, + accumulator_events: Vec, + // total size of events emitted so far + total_events_size: u64, + received: IndexMap, + // Used to track SUI conservation in settlement transactions. Settlement transactions + // gather up withdraws and deposits from other transactions, and record them to accumulator + // fields. The settlement transaction records the total amount of SUI being disbursed here, + // so that we can verify that the amount stored in the fields at the end of the transaction + // is correct. + settlement_input_sui: u64, + settlement_output_sui: u64, + accumulator_merge_totals: BTreeMap<(AccountAddress, TypeTag), u128>, + accumulator_split_totals: BTreeMap<(AccountAddress, TypeTag), u128>, +} + +#[derive(Tid)] +pub struct ObjectRuntime<'a> { + child_object_store: ChildObjectStore<'a>, + // inventories for test scenario + pub(crate) test_inventories: TestInventories, + // the internal state + pub(crate) state: ObjectRuntimeState, + // whether or not this TX is gas metered + is_metered: bool, + + pub(crate) protocol_config: &'a ProtocolConfig, + pub(crate) metrics: Arc, +} + +impl<'a> NativeExtensionMarker<'a> for ObjectRuntime<'a> {} + +pub enum TransferResult { + New, + SameOwner, + OwnerChanged, +} + +pub struct InputObject { + pub contained_uids: BTreeSet, + pub version: SequenceNumber, + pub owner: Owner, +} + +impl TestInventories { + fn new() -> Self { + Self::default() + } +} + +impl<'a> ObjectRuntime<'a> { + pub fn new( + object_resolver: &'a dyn ChildObjectResolver, + input_objects: BTreeMap, + is_metered: bool, + protocol_config: &'a ProtocolConfig, + metrics: Arc, + epoch_id: EpochId, + ) -> Self { + let mut input_object_owners = BTreeMap::new(); + let mut root_version = BTreeMap::new(); + let mut wrapped_object_containers = BTreeMap::new(); + for (id, input_object) in input_objects { + let InputObject { + contained_uids, + version, + owner, + } = input_object; + input_object_owners.insert(id, owner); + debug_assert!(contained_uids.contains(&id)); + for contained_uid in contained_uids { + root_version.insert(contained_uid, version); + if contained_uid != id { + let prev = wrapped_object_containers.insert(contained_uid, id); + debug_assert!(prev.is_none()); + } + } + } + Self { + child_object_store: ChildObjectStore::new( + object_resolver, + root_version, + wrapped_object_containers, + is_metered, + protocol_config, + metrics.clone(), + epoch_id, + ), + test_inventories: TestInventories::new(), + state: ObjectRuntimeState { + input_objects: input_object_owners, + new_ids: Set::new(), + generated_ids: Set::new(), + deleted_ids: Set::new(), + transfers: IndexMap::new(), + events: vec![], + accumulator_events: vec![], + total_events_size: 0, + received: IndexMap::new(), + settlement_input_sui: 0, + settlement_output_sui: 0, + accumulator_merge_totals: BTreeMap::new(), + accumulator_split_totals: BTreeMap::new(), + }, + is_metered, + protocol_config, + metrics, + } + } + + pub fn new_id(&mut self, id: ObjectID) -> PartialVMResult<()> { + // If metered, we use the metered limit (non system tx limit) as the hard limit + // This macro takes care of that + if let LimitThresholdCrossed::Hard(_, lim) = check_limit_by_meter!( + self.is_metered, + self.state.new_ids.len(), + self.protocol_config.max_num_new_move_object_ids(), + self.protocol_config.max_num_new_move_object_ids_system_tx(), + self.metrics.excessive_new_move_object_ids + ) { + return Err(PartialVMError::new(StatusCode::MEMORY_LIMIT_EXCEEDED) + .with_message(format!("Creating more than {} IDs is not allowed", lim)) + .with_sub_status( + VMMemoryLimitExceededSubStatusCode::NEW_ID_COUNT_LIMIT_EXCEEDED as u64, + )); + }; + + // remove from deleted_ids for the case in dynamic fields where the Field object was deleted + // and then re-added in a single transaction. In that case, we also skip adding it + // to new_ids. + let was_present = self.state.deleted_ids.shift_remove(&id); + if !was_present { + // mark the id as new + self.state.generated_ids.insert(id); + self.state.new_ids.insert(id); + } + Ok(()) + } + + pub fn delete_id(&mut self, id: ObjectID) -> PartialVMResult<()> { + // This is defensive because `self.state.deleted_ids` may not indeed + // be called based on the `was_new` flag + // Metered transactions don't have limits for now + + if let LimitThresholdCrossed::Hard(_, lim) = check_limit_by_meter!( + self.is_metered, + self.state.deleted_ids.len(), + self.protocol_config.max_num_deleted_move_object_ids(), + self.protocol_config + .max_num_deleted_move_object_ids_system_tx(), + self.metrics.excessive_deleted_move_object_ids + ) { + return Err(PartialVMError::new(StatusCode::MEMORY_LIMIT_EXCEEDED) + .with_message(format!("Deleting more than {} IDs is not allowed", lim)) + .with_sub_status( + VMMemoryLimitExceededSubStatusCode::DELETED_ID_COUNT_LIMIT_EXCEEDED as u64, + )); + }; + + let was_new = self.state.new_ids.shift_remove(&id); + if !was_new { + self.state.deleted_ids.insert(id); + } + Ok(()) + } + + /// In the new PTB adapter, this function is also used for persisting owners at the end + /// of the transaction. In which case, we don't check the transfer limits. + pub fn transfer( + &mut self, + owner: Owner, + ty: MoveObjectType, + obj: Value, + end_of_transaction: bool, + ) -> PartialVMResult { + let id: ObjectID = get_object_id(obj.copy_value()?)? + .value_as::()? + .into(); + // - An object is new if it is contained in the new ids or if it is one of the objects + // created during genesis (the system state object or clock). + // - Otherwise, check the input objects for the previous owner + // - If it was not in the input objects, it must have been wrapped or must have been a + // child object + let is_framework_obj = [ + SUI_SYSTEM_STATE_OBJECT_ID, + SUI_CLOCK_OBJECT_ID, + SUI_AUTHENTICATOR_STATE_OBJECT_ID, + SUI_RANDOMNESS_STATE_OBJECT_ID, + SUI_DENY_LIST_OBJECT_ID, + SUI_BRIDGE_OBJECT_ID, + SUI_ACCUMULATOR_ROOT_OBJECT_ID, + SUI_COIN_REGISTRY_OBJECT_ID, + SUI_DISPLAY_REGISTRY_OBJECT_ID, + ] + .contains(&id); + let transfer_result = if self.state.new_ids.contains(&id) { + TransferResult::New + } else if let Some(prev_owner) = self.state.input_objects.get(&id) { + match (&owner, prev_owner) { + // don't use == for dummy values in Shared or ConsensusAddressOwner + (Owner::Shared { .. }, Owner::Shared { .. }) => TransferResult::SameOwner, + ( + Owner::ConsensusAddressOwner { + owner: new_owner, .. + }, + Owner::ConsensusAddressOwner { + owner: old_owner, .. + }, + ) if new_owner == old_owner => TransferResult::SameOwner, + (new, old) if new == old => TransferResult::SameOwner, + _ => TransferResult::OwnerChanged, + } + } else if is_framework_obj { + // framework objects are always created when they are transferred, but the id is + // hard-coded so it is not yet in new_ids or generated_ids + self.state.new_ids.insert(id); + self.state.generated_ids.insert(id); + TransferResult::New + } else { + TransferResult::OwnerChanged + }; + // assert!(end of transaction ==> same owner) + if end_of_transaction && !matches!(transfer_result, TransferResult::SameOwner) { + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message(format!("Untransferred object {} had its owner change", id)), + ); + } + + // Metered transactions don't have limits for now + + if let LimitThresholdCrossed::Hard(_, lim) = check_limit_by_meter!( + // TODO: is this not redundant? Metered TX implies framework obj cannot be transferred + // We have higher limits for unmetered transactions and framework obj + // We don't check the limit for objects whose owner is persisted at the end of the + // transaction + self.is_metered && !is_framework_obj && !end_of_transaction, + self.state.transfers.len(), + self.protocol_config.max_num_transferred_move_object_ids(), + self.protocol_config + .max_num_transferred_move_object_ids_system_tx(), + self.metrics.excessive_transferred_move_object_ids + ) { + return Err(PartialVMError::new(StatusCode::MEMORY_LIMIT_EXCEEDED) + .with_message(format!("Transferring more than {} IDs is not allowed", lim)) + .with_sub_status( + VMMemoryLimitExceededSubStatusCode::TRANSFER_ID_COUNT_LIMIT_EXCEEDED as u64, + )); + }; + + self.state.transfers.insert(id, (owner, ty, obj)); + Ok(transfer_result) + } + + pub fn emit_event(&mut self, tag: StructTag, event: Value) -> PartialVMResult<()> { + if self.state.events.len() >= (self.protocol_config.max_num_event_emit() as usize) { + return Err(max_event_error(self.protocol_config.max_num_event_emit())); + } + self.state.events.push((tag, event)); + Ok(()) + } + + pub fn take_user_events(&mut self) -> Vec<(StructTag, Value)> { + std::mem::take(&mut self.state.events) + } + + pub fn emit_accumulator_event( + &mut self, + accumulator_id: ObjectID, + action: MoveAccumulatorAction, + target_addr: AccountAddress, + target_ty: TypeTag, + value: MoveAccumulatorValue, + ) -> PartialVMResult<()> { + if let MoveAccumulatorValue::U64(amount) = value { + let key = (target_addr, target_ty.clone()); + + match action { + MoveAccumulatorAction::Merge => { + let current = self + .state + .accumulator_merge_totals + .get(&key) + .copied() + .unwrap_or(0); + let new_total = current + amount as u128; + if new_total > u64::MAX as u128 { + return Err(PartialVMError::new(StatusCode::ARITHMETIC_ERROR) + .with_message(format!( + "accumulator merge overflow: total merges {} exceed u64::MAX", + new_total + ))); + } + self.state.accumulator_merge_totals.insert(key, new_total); + } + MoveAccumulatorAction::Split => { + let current = self + .state + .accumulator_split_totals + .get(&key) + .copied() + .unwrap_or(0); + let new_total = current + amount as u128; + if new_total > u64::MAX as u128 { + return Err(PartialVMError::new(StatusCode::ARITHMETIC_ERROR) + .with_message(format!( + "accumulator split overflow: total splits {} exceed u64::MAX", + new_total + ))); + } + self.state.accumulator_split_totals.insert(key, new_total); + } + } + } + + let event = MoveAccumulatorEvent { + accumulator_id, + action, + target_addr, + target_ty, + value, + }; + self.state.accumulator_events.push(event); + Ok(()) + } + + pub(crate) fn child_object_exists( + &mut self, + parent: ObjectID, + child: ObjectID, + ) -> PartialVMResult> { + self.child_object_store.object_exists(parent, child) + } + + pub(crate) fn child_object_exists_and_has_type( + &mut self, + parent: ObjectID, + child: ObjectID, + child_type: &MoveObjectType, + ) -> PartialVMResult> { + self.child_object_store + .object_exists_and_has_type(parent, child, child_type) + } + + pub(super) fn receive_object( + &mut self, + parent: ObjectID, + child: ObjectID, + child_version: SequenceNumber, + child_layout: &R::MoveTypeLayout, + child_fully_annotated_layout: &MoveTypeLayout, + child_move_type: MoveObjectType, + ) -> PartialVMResult>>> { + let Some((value, obj_meta)) = self.child_object_store.receive_object( + parent, + child, + child_version, + child_layout, + child_fully_annotated_layout, + child_move_type, + )? + else { + return Ok(None); + }; + // NB: It is important that the object only be added to the received set after it has been + // fully authenticated and loaded. + if self.state.received.insert(child, obj_meta).is_some() { + // We should never hit this -- it means that we have received the same object twice which + // means we have a duplicated a receiving ticket somehow. + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR).with_message(format!( + "Object {child} at version {child_version} already received. This can only happen \ + if multiple `Receiving` arguments exist for the same object in the transaction which is impossible." + )), + ); + } + Ok(Some(value)) + } + + pub(crate) fn get_or_fetch_child_object( + &mut self, + parent: ObjectID, + child: ObjectID, + child_layout: &R::MoveTypeLayout, + child_fully_annotated_layout: &MoveTypeLayout, + child_move_type: MoveObjectType, + ) -> PartialVMResult>> { + let res = self.child_object_store.get_or_fetch_object( + parent, + child, + child_layout, + child_fully_annotated_layout, + child_move_type, + )?; + Ok(match res { + ObjectResult::MismatchedType => ObjectResult::MismatchedType, + ObjectResult::Loaded((cache_info, child_object)) => { + ObjectResult::Loaded((cache_info, &mut child_object.value)) + } + }) + } + + pub(crate) fn add_child_object( + &mut self, + parent: ObjectID, + child: ObjectID, + child_move_type: MoveObjectType, + child_value: Value, + ) -> PartialVMResult<()> { + self.child_object_store + .add_object(parent, child, child_move_type, child_value) + } + + pub(crate) fn config_setting_unsequenced_read( + &mut self, + config_id: ObjectID, + name_df_id: ObjectID, + field_setting_layout: &R::MoveTypeLayout, + field_setting_object_type: &MoveObjectType, + ) -> Option { + match self.child_object_store.config_setting_unsequenced_read( + config_id, + name_df_id, + field_setting_layout, + field_setting_object_type, + ) { + Err(e) => { + error!( + "Failed to read config setting. + config_id: {config_id}, + name_df_id: {name_df_id}, + field_setting_object_type: {field_setting_object_type:?}, + error: {e}" + ); + None + } + Ok(ObjectResult::MismatchedType) | Ok(ObjectResult::Loaded(None)) => None, + Ok(ObjectResult::Loaded(Some(value))) => Some(value), + } + } + + pub(super) fn config_setting_cache_update( + &mut self, + config_id: ObjectID, + name_df_id: ObjectID, + setting_value_object_type: MoveObjectType, + value: Option, + ) { + self.child_object_store.config_setting_cache_update( + config_id, + name_df_id, + setting_value_object_type, + value, + ) + } + + // returns None if a child object is still borrowed + pub(crate) fn take_state(&mut self) -> ObjectRuntimeState { + std::mem::take(&mut self.state) + } + + pub fn is_deleted(&self, id: &ObjectID) -> bool { + self.state.deleted_ids.contains(id) + } + + pub fn is_transferred(&self, id: &ObjectID) -> Option { + self.state + .transfers + .get(id) + .map(|(owner, _, _)| owner.clone()) + } + + pub fn finish(mut self) -> Result { + let loaded_child_objects = self.loaded_runtime_objects(); + let child_effects = self.child_object_store.take_effects().map_err(|e| { + ExecutionError::invariant_violation(format!("Failed to take child object effects: {e}")) + })?; + self.state.finish(loaded_child_objects, child_effects) + } + + pub(crate) fn all_active_child_objects(&self) -> impl Iterator> { + self.child_object_store.all_active_objects() + } + + pub fn loaded_runtime_objects(&self) -> BTreeMap { + // The loaded child objects, and the received objects, should be disjoint. If they are not, + // this is an error since it could lead to incorrect transaction dependency computations. + debug_assert!( + self.child_object_store + .cached_objects() + .keys() + .all(|id| !self.state.received.contains_key(id)) + ); + self.child_object_store + .cached_objects() + .iter() + .filter_map(|(id, obj_opt)| { + obj_opt.as_ref().map(|obj| { + ( + *id, + DynamicallyLoadedObjectMetadata { + version: obj.version(), + digest: obj.digest(), + storage_rebate: obj.storage_rebate, + owner: obj.owner.clone(), + previous_transaction: obj.previous_transaction, + }, + ) + }) + }) + .chain( + self.state + .received + .iter() + .map(|(id, meta)| (*id, meta.clone())), + ) + .collect() + } + + /// A map from wrapped objects to the object that wraps them at the beginning of the + /// transaction. + pub fn wrapped_object_containers(&self) -> BTreeMap { + self.child_object_store.wrapped_object_containers().clone() + } + + pub fn record_settlement_sui_conservation(&mut self, input_sui: u64, output_sui: u64) { + self.state.settlement_input_sui += input_sui; + self.state.settlement_output_sui += output_sui; + } + + /// Return the set of all object IDs that were created during this transaction, including any + /// object IDs that were created and then deleted during the transaction. + pub fn generated_object_ids(&self) -> BTreeSet { + self.state.generated_ids.iter().cloned().collect() + } +} + +pub fn max_event_error(max_events: u64) -> PartialVMError { + PartialVMError::new(StatusCode::MEMORY_LIMIT_EXCEEDED) + .with_message(format!( + "Emitting more than {} events is not allowed", + max_events + )) + .with_sub_status(VMMemoryLimitExceededSubStatusCode::EVENT_COUNT_LIMIT_EXCEEDED as u64) +} + +impl ObjectRuntimeState { + /// Update `state_view` with the effects of successfully executing a transaction: + /// - Given the effects `Op` of child objects, processes the changes in terms of + /// object writes/deletes + /// - Process `transfers` and `input_objects` to determine whether the type of change + /// (WriteKind) to the object + /// - Process `deleted_ids` with previously determined information to determine the + /// DeleteKind + /// - Passes through user events + pub(crate) fn finish( + mut self, + loaded_child_objects: BTreeMap, + child_object_effects: ChildObjectEffects, + ) -> Result { + let mut loaded_child_objects: BTreeMap<_, _> = loaded_child_objects + .into_iter() + .map(|(id, metadata)| { + ( + id, + LoadedRuntimeObject { + version: metadata.version, + is_modified: false, + }, + ) + }) + .collect(); + self.apply_child_object_effects(&mut loaded_child_objects, child_object_effects); + let ObjectRuntimeState { + input_objects: _, + new_ids, + generated_ids, + deleted_ids, + transfers, + events: user_events, + total_events_size: _, + received, + accumulator_events, + settlement_input_sui, + settlement_output_sui, + accumulator_merge_totals: _, + accumulator_split_totals: _, + } = self; + + // The set of new ids is a subset of the generated ids. + debug_assert!(new_ids.is_subset(&generated_ids)); + + // Check new owners from transfers, reports an error on cycles. + // TODO can we have cycles in the new system? + check_circular_ownership( + transfers + .iter() + .map(|(id, (owner, _, _))| (*id, owner.clone())), + )?; + // For both written_objects and deleted_ids, we need to mark the loaded child object as modified. + // These may not be covered in the child object effects if they are taken out in one PT command and then + // transferred/deleted in a different command. Marking them as modified will allow us properly determine their + // mutation category in effects. + // TODO: This could get error-prone quickly: what if we forgot to mark an object as modified? There may be a cleaner + // sulution. + let written_objects: IndexMap<_, _> = transfers + .into_iter() + .map(|(id, (owner, type_, value))| { + if let Some(loaded_child) = loaded_child_objects.get_mut(&id) { + loaded_child.is_modified = true; + } + (id, (owner, type_, value)) + }) + .collect(); + for deleted_id in &deleted_ids { + if let Some(loaded_child) = loaded_child_objects.get_mut(deleted_id) { + loaded_child.is_modified = true; + } + } + + // Any received objects are viewed as modified. They had to be loaded in order to be + // received so they must be in the loaded_child_objects map otherwise it's an invariant + // violation. + for (received_object, _) in received.into_iter() { + match loaded_child_objects.get_mut(&received_object) { + Some(loaded_child) => { + loaded_child.is_modified = true; + } + None => { + return Err(ExecutionError::invariant_violation(format!( + "Failed to find received UID {received_object} in loaded child objects." + ))); + } + } + } + + Ok(RuntimeResults { + writes: written_objects, + user_events, + accumulator_events, + loaded_child_objects, + created_object_ids: new_ids, + deleted_object_ids: deleted_ids, + settlement_input_sui, + settlement_output_sui, + }) + } + + pub fn events(&self) -> &[(StructTag, Value)] { + &self.events + } + + pub fn total_events_size(&self) -> u64 { + self.total_events_size + } + + pub fn incr_total_events_size(&mut self, size: u64) { + self.total_events_size += size; + } + + fn apply_child_object_effects( + &mut self, + loaded_child_objects: &mut BTreeMap, + child_object_effects: ChildObjectEffects, + ) { + match child_object_effects { + ChildObjectEffects::V0(child_object_effects) => { + self.apply_child_object_effects_v0(loaded_child_objects, child_object_effects) + } + ChildObjectEffects::V1(child_object_effects) => { + self.apply_child_object_effects_v1(loaded_child_objects, child_object_effects) + } + } + } + + fn apply_child_object_effects_v0( + &mut self, + loaded_child_objects: &mut BTreeMap, + child_object_effects: BTreeMap, + ) { + for (child, child_object_effect) in child_object_effects { + let ChildObjectEffectV0 { + owner: parent, + ty, + effect, + } = child_object_effect; + + if let Some(loaded_child) = loaded_child_objects.get_mut(&child) { + loaded_child.is_modified = true; + } + + match effect { + // was modified, so mark it as mutated and transferred + Op::Modify(v) => { + debug_assert!(!self.transfers.contains_key(&child)); + debug_assert!(!self.new_ids.contains(&child)); + debug_assert!(loaded_child_objects.contains_key(&child)); + self.transfers + .insert(child, (Owner::ObjectOwner(parent.into()), ty, v)); + } + + Op::New(v) => { + debug_assert!(!self.transfers.contains_key(&child)); + self.transfers + .insert(child, (Owner::ObjectOwner(parent.into()), ty, v)); + } + + Op::Delete => { + // was transferred so not actually deleted + if self.transfers.contains_key(&child) { + debug_assert!(!self.deleted_ids.contains(&child)); + } + // ID was deleted too was deleted so mark as deleted + if self.deleted_ids.contains(&child) { + debug_assert!(!self.transfers.contains_key(&child)); + debug_assert!(!self.new_ids.contains(&child)); + } + } + } + } + } + + fn apply_child_object_effects_v1( + &mut self, + loaded_child_objects: &mut BTreeMap, + child_object_effects: BTreeMap, + ) { + for (child, child_object_effect) in child_object_effects { + let ChildObjectEffectV1 { + owner: parent, + ty, + final_value, + object_changed, + } = child_object_effect; + + if object_changed { + if let Some(loaded_child) = loaded_child_objects.get_mut(&child) { + loaded_child.is_modified = true; + } + + match final_value { + None => { + // Value was changed and is no longer present, it may have been wrapped, + // transferred, or deleted. + + // If it was transferred, it should not have been deleted + // transferred ==> !deleted + debug_assert!( + !self.transfers.contains_key(&child) + || !self.deleted_ids.contains(&child) + ); + // If it was deleted, it should not have been transferred. Additionally, + // if it was deleted, it should no longer be marked as new. + // deleted ==> !transferred and !new + debug_assert!( + !self.deleted_ids.contains(&child) + || (!self.transfers.contains_key(&child) + && !self.new_ids.contains(&child)) + ); + } + Some(v) => { + // Value was changed (or the owner was changed) + + // It is still a dynamic field so it should not be transferred or deleted + debug_assert!( + !self.transfers.contains_key(&child) + && !self.deleted_ids.contains(&child) + ); + // If it was loaded, it must have been new. But keep in mind if it was not + // loaded, it is not necessarily new since it could have been + // input/wrapped/received + // loaded ==> !new + debug_assert!( + !loaded_child_objects.contains_key(&child) + || !self.new_ids.contains(&child) + ); + // Mark the mutation of the new value and/or parent. + self.transfers + .insert(child, (Owner::ObjectOwner(parent.into()), ty, v)); + } + } + } else { + // The object was not changed. + // If it was created, + // it must now have been moved elsewhere (wrapped or transferred). + // If it was deleted or transferred, + // it must have been an input/received/wrapped object. + // In either case, the value must now have been moved elsewhere, giving us: + // (new or deleted or transferred or received) ==> no value + // which is equivalent to: + // has value ==> (!deleted and !transferred and !input) + // If the value is still there, it must have been loaded. + // Combining these to give us the check: + // has value ==> (loaded and !deleted and !transferred and !input and !received) + // which is equivalent to: + // !(no value) ==> (loaded and !deleted and !transferred and !input and !received) + debug_assert!( + final_value.is_none() + || (loaded_child_objects.contains_key(&child) + && !self.deleted_ids.contains(&child) + && !self.transfers.contains_key(&child) + && !self.input_objects.contains_key(&child) + && !self.received.contains_key(&child)) + ); + // In any case, if it was not changed, it should not be marked as modified + debug_assert!( + loaded_child_objects + .get(&child) + .is_none_or(|loaded_child| !loaded_child.is_modified) + ); + } + } + } +} + +fn check_circular_ownership( + transfers: impl IntoIterator, +) -> Result<(), ExecutionError> { + let mut object_owner_map = BTreeMap::new(); + for (id, recipient) in transfers { + object_owner_map.remove(&id); + match recipient { + Owner::AddressOwner(_) + | Owner::Shared { .. } + | Owner::Immutable + | Owner::ConsensusAddressOwner { .. } => (), + Owner::ObjectOwner(new_owner) => { + let new_owner: ObjectID = new_owner.into(); + let mut cur = new_owner; + loop { + if cur == id { + return Err(ExecutionError::from_kind( + ExecutionErrorKind::CircularObjectOwnership { object: cur }, + )); + } + if let Some(parent) = object_owner_map.get(&cur) { + cur = *parent; + } else { + break; + } + } + object_owner_map.insert(id, new_owner); + } + } + } + Ok(()) +} + +/// WARNING! This function assumes that the bcs bytes have already been validated, +/// and it will give an invariant violation otherwise. +/// In short, we are relying on the invariant that the bytes are valid for objects +/// in storage. We do not need this invariant for dev-inspect, as the programmable +/// transaction execution will validate the bytes before we get to this point. +pub fn get_all_uids( + fully_annotated_layout: &MoveTypeLayout, + bcs_bytes: &[u8], +) -> Result, /* invariant violation */ String> { + let mut ids = BTreeSet::new(); + struct UIDTraversal<'i>(&'i mut BTreeSet); + struct UIDCollector<'i>(&'i mut BTreeSet); + + impl<'b, 'l> AV::Traversal<'b, 'l> for UIDTraversal<'_> { + type Error = AV::Error; + + fn traverse_struct( + &mut self, + driver: &mut AV::StructDriver<'_, 'b, 'l>, + ) -> Result<(), Self::Error> { + if driver.struct_layout().type_ == UID::type_() { + while driver.next_field(&mut UIDCollector(self.0))?.is_some() {} + } else { + while driver.next_field(self)?.is_some() {} + } + Ok(()) + } + } + + impl<'b, 'l> AV::Traversal<'b, 'l> for UIDCollector<'_> { + type Error = AV::Error; + fn traverse_address( + &mut self, + _driver: &AV::ValueDriver<'_, 'b, 'l>, + value: AccountAddress, + ) -> Result<(), Self::Error> { + self.0.insert(value.into()); + Ok(()) + } + } + + MoveValue::visit_deserialize( + bcs_bytes, + fully_annotated_layout, + &mut UIDTraversal(&mut ids), + ) + .map_err(|e| format!("Failed to deserialize. {e}"))?; + Ok(ids) +} diff --git a/sui-execution/replay_cut/sui-move-natives/src/object_runtime/object_store.rs b/sui-execution/replay_cut/sui-move-natives/src/object_runtime/object_store.rs new file mode 100644 index 0000000000000..e0e91e1c682d4 --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/object_runtime/object_store.rs @@ -0,0 +1,853 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::object_runtime::{fingerprint::ObjectFingerprint, get_all_uids}; +use move_binary_format::errors::{PartialVMError, PartialVMResult}; +use move_core_types::{annotated_value as A, runtime_value as R, vm_status::StatusCode}; +use move_vm_types::{ + effects::Op, + values::{GlobalValue, StructRef, Value}, +}; +use std::{ + collections::{BTreeMap, btree_map}, + sync::Arc, +}; +use sui_protocol_config::{LimitThresholdCrossed, ProtocolConfig, check_limit_by_meter}; +use sui_types::{ + base_types::{MoveObjectType, ObjectID, SequenceNumber}, + committee::EpochId, + error::VMMemoryLimitExceededSubStatusCode, + execution::DynamicallyLoadedObjectMetadata, + metrics::LimitsMetrics, + object::{Data, MoveObject, Object, Owner}, + storage::ChildObjectResolver, +}; + +pub(super) struct ChildObject { + pub(super) owner: ObjectID, + pub(super) ty: MoveObjectType, + pub(super) value: GlobalValue, + pub(super) fingerprint: ObjectFingerprint, +} + +#[derive(Debug, Clone, Copy)] +pub(crate) enum CacheInfo { + /// Only the object was cached, the value was not loaded into the VM + CachedObject, + /// Both the object and the value were cached + CachedValue, + /// The value was loaded from storage, and the number of BCS bytes loaded + /// (which is None if the object was not found) + Loaded(Option), +} + +pub(crate) type CacheMetadata = (CacheInfo, T); + +pub(crate) struct ActiveChildObject<'a> { + pub(crate) id: &'a ObjectID, + pub(crate) owner: &'a ObjectID, + pub(crate) ty: &'a MoveObjectType, + pub(crate) copied_value: Option, +} + +#[derive(Debug)] +struct ConfigSetting { + config: ObjectID, + ty: MoveObjectType, + value: Value, +} + +#[derive(Debug)] +pub(crate) struct ChildObjectEffectV0 { + pub(super) owner: ObjectID, + pub(super) ty: MoveObjectType, + pub(super) effect: Op, +} + +#[derive(Debug)] +pub(crate) struct ChildObjectEffectV1 { + pub(super) owner: ObjectID, + pub(super) ty: MoveObjectType, + pub(super) final_value: Option, + // True if the value or the owner has changed + pub(super) object_changed: bool, +} + +#[derive(Debug)] +pub(crate) enum ChildObjectEffects { + // In this version, we accurately track mutations via WriteRef to the child object, or + // references rooted in the child object. + V0(BTreeMap), + // In this version, we instead check always return the value, and report if it changed. + V1(BTreeMap), +} + +struct Inner<'a> { + // used for loading child objects + resolver: &'a dyn ChildObjectResolver, + // The version of the root object in ownership at the beginning of the transaction. + // If it was a child object, it resolves to the root parent's sequence number. + // Otherwise, it is just the sequence number at the beginning of the transaction. + root_version: BTreeMap, + // A map from a wrapped object to the object it was contained in at the + // beginning of the transaction. + wrapped_object_containers: BTreeMap, + // cached objects from the resolver. An object might be in this map but not in the store + // if it's existence was queried, but the value was not used. + cached_objects: BTreeMap>, + // whether or not this TX is gas metered + is_metered: bool, + // Protocol config used to enforce limits + protocol_config: &'a ProtocolConfig, + // Metrics for reporting exceeded limits + metrics: Arc, + // Epoch ID for the current transaction. Used for receiving objects. + current_epoch_id: EpochId, +} + +// maintains the runtime GlobalValues for child objects and manages the fetching of objects +// from storage, through the `ChildObjectResolver` +pub(super) struct ChildObjectStore<'a> { + // contains object resolver and object cache + // kept as a separate struct to deal with lifetime issues where the `store` is accessed + // at the same time as the `cached_objects` is populated + inner: Inner<'a>, + // Maps of populated GlobalValues, meaning the child object has been accessed in this + // transaction + store: BTreeMap, + config_setting_cache: BTreeMap, + // whether or not this TX is gas metered + is_metered: bool, +} + +pub(crate) enum ObjectResult { + // object exists but type does not match. Should result in an abort + MismatchedType, + Loaded(V), +} + +type LoadedWithMetadataResult = Option<(V, DynamicallyLoadedObjectMetadata)>; + +macro_rules! fetch_child_object_unbounded { + ($inner:ident, $parent:ident, $child:ident, $parents_root_version:expr, $had_parent_root_version:expr) => {{ + let child_opt = $inner + .resolver + .read_child_object(&$parent, &$child, $parents_root_version) + .map_err(|msg| { + PartialVMError::new(StatusCode::STORAGE_ERROR).with_message(format!("{msg}")) + })?; + if let Some(object) = child_opt { + // if there was no root version, guard against reading a child object. A newly + // created parent should not have a child in storage + if !$had_parent_root_version { + return Err( + PartialVMError::new(StatusCode::STORAGE_ERROR).with_message(format!( + "A new parent {} should not have a child object {}.", + $parent, $child + )), + ); + } + // guard against bugs in `read_child_object`: if it returns a child object such that + // C.parent != parent, we raise an invariant violation + match &object.owner { + Owner::ObjectOwner(id) => { + if ObjectID::from(*id) != $parent { + return Err(PartialVMError::new(StatusCode::STORAGE_ERROR).with_message( + format!( + "Bad owner for {}. Expected owner {} but found owner {}", + $child, $parent, id + ), + )); + } + } + Owner::AddressOwner(_) + | Owner::Immutable + | Owner::Shared { .. } + | Owner::ConsensusAddressOwner { .. } => { + return Err(PartialVMError::new(StatusCode::STORAGE_ERROR).with_message( + format!( + "Bad owner for {}. \ + Expected an id owner {} but found an address, \ + immutable, or shared owner", + $child, $parent + ), + )); + } + }; + match &object.data { + Data::Package(_) => { + return Err(PartialVMError::new(StatusCode::STORAGE_ERROR).with_message( + format!( + "Mismatched object type for {}. \ + Expected a Move object but found a Move package", + $child + ), + )); + } + Data::Move(_) => Some(object), + } + } else { + None + } + }}; +} + +impl Inner<'_> { + fn receive_object_from_store( + &self, + owner: ObjectID, + child: ObjectID, + version: SequenceNumber, + ) -> PartialVMResult>> { + let child_opt = self + .resolver + .get_object_received_at_version(&owner, &child, version, self.current_epoch_id) + .map_err(|msg| { + PartialVMError::new(StatusCode::STORAGE_ERROR).with_message(format!("{msg}")) + })?; + let (cache_info, obj_opt) = if let Some(object) = child_opt { + // guard against bugs in `receive_object_at_version`: if it returns a child object such that + // C.parent != parent, we raise an invariant violation since that should be checked by + // `receive_object_at_version`. + if object.owner != Owner::AddressOwner(owner.into()) { + return Err( + PartialVMError::new(StatusCode::STORAGE_ERROR).with_message(format!( + "Bad owner for {child}. \ + Expected owner {owner} but found owner {}", + object.owner + )), + ); + } + let loaded_metadata = DynamicallyLoadedObjectMetadata { + version, + digest: object.digest(), + storage_rebate: object.storage_rebate, + owner: object.owner.clone(), + previous_transaction: object.previous_transaction, + }; + + // `ChildObjectResolver::receive_object_at_version` should return the object at the + // version or nothing at all. If it returns an object with a different version, we + // should raise an invariant violation since it should be checked by + // `receive_object_at_version`. + if object.version() != version { + return Err( + PartialVMError::new(StatusCode::STORAGE_ERROR).with_message(format!( + "Bad version for {child}. \ + Expected version {version} but found version {}", + object.version() + )), + ); + } + match object.into_inner().data { + Data::Package(_) => { + return Err(PartialVMError::new(StatusCode::STORAGE_ERROR).with_message( + format!( + "Mismatched object type for {child}. \ + Expected a Move object but found a Move package" + ), + )); + } + Data::Move(mo @ MoveObject { .. }) => ( + CacheInfo::Loaded(Some(mo.contents().len())), + Some((mo, loaded_metadata)), + ), + } + } else { + (CacheInfo::Loaded(None), None) + }; + Ok((cache_info, obj_opt)) + } + + #[allow(clippy::map_entry)] + fn get_or_fetch_object_from_store( + &mut self, + parent: ObjectID, + child: ObjectID, + ) -> PartialVMResult>> { + let cached_objects_count = self.cached_objects.len() as u64; + let parents_root_version = self.root_version.get(&parent).copied(); + let had_parent_root_version = parents_root_version.is_some(); + // if not found, it must be new so it won't have any child objects, thus + // we can return SequenceNumber(0) as no child object will be found + let parents_root_version = parents_root_version.unwrap_or(SequenceNumber::new()); + let cache_info = if let btree_map::Entry::Vacant(e) = self.cached_objects.entry(child) { + let obj_opt = fetch_child_object_unbounded!( + self, + parent, + child, + parents_root_version, + had_parent_root_version + ); + + if let LimitThresholdCrossed::Hard(_, lim) = check_limit_by_meter!( + self.is_metered, + cached_objects_count, + self.protocol_config.object_runtime_max_num_cached_objects(), + self.protocol_config + .object_runtime_max_num_cached_objects_system_tx(), + self.metrics.excessive_object_runtime_cached_objects + ) { + return Err(PartialVMError::new(StatusCode::MEMORY_LIMIT_EXCEEDED) + .with_message(format!( + "Object runtime cached objects limit ({} entries) reached", + lim + )) + .with_sub_status( + VMMemoryLimitExceededSubStatusCode::OBJECT_RUNTIME_CACHE_LIMIT_EXCEEDED + as u64, + )); + }; + let num_bytes_opt = match &obj_opt { + Some(obj) => { + // unwrap safe because we only insert Move objects + let move_obj = obj.data.try_as_move().unwrap(); + Some(move_obj.contents().len()) + } + None => None, + }; + + e.insert(obj_opt); + CacheInfo::Loaded(num_bytes_opt) + } else { + CacheInfo::CachedObject + }; + // unwraps are safe because it must be inserted and we only insert Move objects + let move_obj_opt = self + .cached_objects + .get(&child) + .unwrap() + .as_ref() + .map(|obj| obj.data.try_as_move().unwrap()); + Ok((cache_info, move_obj_opt)) + } + + fn fetch_object_impl( + &mut self, + parent: ObjectID, + child: ObjectID, + child_ty_layout: &R::MoveTypeLayout, + child_ty_fully_annotated_layout: &A::MoveTypeLayout, + child_move_type: &MoveObjectType, + ) -> PartialVMResult< + ObjectResult>, + > { + // we copy the reference to the protocol config ahead of time for lifetime reasons + let protocol_config = self.protocol_config; + // retrieve the object from storage if it exists + let (cache_info, obj_opt) = self.get_or_fetch_object_from_store(parent, child)?; + let Some(obj) = obj_opt else { + return Ok(ObjectResult::Loaded(( + cache_info, + ( + child_move_type.clone(), + GlobalValue::none(), + ObjectFingerprint::none(protocol_config), + ), + ))); + }; + // object exists, but the type does not match + if obj.type_() != child_move_type { + return Ok(ObjectResult::MismatchedType); + } + // deserialize the value + let obj_contents = obj.contents(); + let v = match Value::simple_deserialize(obj_contents, child_ty_layout) { + Some(v) => v, + None => return Err( + PartialVMError::new(StatusCode::FAILED_TO_DESERIALIZE_RESOURCE).with_message( + format!("Failed to deserialize object {child} with type {child_move_type}",), + ), + ), + }; + // save a fingerprint + let fingerprint = + ObjectFingerprint::preexisting(protocol_config, &parent, child_move_type, &v).map_err( + |e| { + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR).with_message( + format!("Failed to fingerprint value for object {child}. Error: {e}"), + ) + }, + )?; + // generate a global value + let global_value = + match GlobalValue::cached(v) { + Ok(gv) => gv, + Err(e) => { + return Err(PartialVMError::new(StatusCode::STORAGE_ERROR).with_message( + format!("Object {child} did not deserialize to a struct Value. Error: {e}"), + )); + } + }; + // Find all UIDs inside of the value and update the object parent maps + let contained_uids = + get_all_uids(child_ty_fully_annotated_layout, obj_contents).map_err(|e| { + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message(format!("Failed to find UIDs. ERROR: {e}")) + })?; + let parents_root_version = self.root_version.get(&parent).copied(); + if let Some(v) = parents_root_version { + debug_assert!(contained_uids.contains(&child)); + for id in contained_uids { + self.root_version.insert(id, v); + if id != child { + let prev = self.wrapped_object_containers.insert(id, child); + debug_assert!(prev.is_none()) + } + } + } + Ok(ObjectResult::Loaded(( + cache_info, + (child_move_type.clone(), global_value, fingerprint), + ))) + } +} + +fn deserialize_move_object( + obj: &MoveObject, + child_ty_layout: &R::MoveTypeLayout, + child_move_type: MoveObjectType, +) -> PartialVMResult> { + let child_id = obj.id(); + // object exists, but the type does not match + if obj.type_() != &child_move_type { + return Ok(ObjectResult::MismatchedType); + } + let value = match Value::simple_deserialize(obj.contents(), child_ty_layout) { + Some(v) => v, + None => { + return Err( + PartialVMError::new(StatusCode::FAILED_TO_DESERIALIZE_RESOURCE).with_message( + format!("Failed to deserialize object {child_id} with type {child_move_type}",), + ), + ); + } + }; + Ok(ObjectResult::Loaded((child_move_type, value))) +} + +impl<'a> ChildObjectStore<'a> { + pub(super) fn new( + resolver: &'a dyn ChildObjectResolver, + root_version: BTreeMap, + wrapped_object_containers: BTreeMap, + is_metered: bool, + protocol_config: &'a ProtocolConfig, + metrics: Arc, + current_epoch_id: EpochId, + ) -> Self { + Self { + inner: Inner { + resolver, + root_version, + wrapped_object_containers, + cached_objects: BTreeMap::new(), + is_metered, + protocol_config, + metrics, + current_epoch_id, + }, + store: BTreeMap::new(), + config_setting_cache: BTreeMap::new(), + is_metered, + } + } + + pub(super) fn receive_object( + &mut self, + parent: ObjectID, + child: ObjectID, + child_version: SequenceNumber, + child_layout: &R::MoveTypeLayout, + child_fully_annotated_layout: &A::MoveTypeLayout, + child_move_type: MoveObjectType, + ) -> PartialVMResult>>> { + let (cache_info, Some((obj, obj_meta))) = + self.inner + .receive_object_from_store(parent, child, child_version)? + else { + return Ok(None); + }; + + Ok(Some( + match deserialize_move_object(&obj, child_layout, child_move_type)? { + ObjectResult::MismatchedType => (ObjectResult::MismatchedType, obj_meta), + ObjectResult::Loaded((_, v)) => { + // Find all UIDs inside of the value and update the object parent maps with the contained + // UIDs in the received value. They should all have an upper bound version as the receiving object. + // Only do this if we successfully load the object though. + let contained_uids = get_all_uids(child_fully_annotated_layout, obj.contents()) + .map_err(|e| { + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message(format!( + "Failed to find UIDs for receiving object. ERROR: {e}" + )) + })?; + for id in contained_uids { + self.inner.root_version.insert(id, child_version); + if id != child { + let prev = self.inner.wrapped_object_containers.insert(id, child); + debug_assert!(prev.is_none()) + } + } + (ObjectResult::Loaded((cache_info, v)), obj_meta) + } + }, + )) + } + + pub(super) fn object_exists( + &mut self, + parent: ObjectID, + child: ObjectID, + ) -> PartialVMResult> { + if let Some(child_object) = self.store.get(&child) { + return child_object + .value + .exists() + .map(|exists| (CacheInfo::CachedValue, exists)); + } + let (cache_info, obj_opt) = self.inner.get_or_fetch_object_from_store(parent, child)?; + Ok((cache_info, obj_opt.is_some())) + } + + pub(super) fn object_exists_and_has_type( + &mut self, + parent: ObjectID, + child: ObjectID, + child_move_type: &MoveObjectType, + ) -> PartialVMResult> { + if let Some(child_object) = self.store.get(&child) { + // exists and has same type + return Ok(( + CacheInfo::CachedValue, + child_object.value.exists()? && &child_object.ty == child_move_type, + )); + } + let (cache_info, obj_opt) = self.inner.get_or_fetch_object_from_store(parent, child)?; + Ok(( + cache_info, + obj_opt.is_some_and(|obj| obj.type_() == child_move_type), + )) + } + + pub(super) fn get_or_fetch_object( + &mut self, + parent: ObjectID, + child: ObjectID, + child_layout: &R::MoveTypeLayout, + child_fully_annotated_layout: &A::MoveTypeLayout, + child_move_type: MoveObjectType, + ) -> PartialVMResult>> { + let store_entries_count = self.store.len() as u64; + let (cache_info, child_object) = match self.store.entry(child) { + btree_map::Entry::Vacant(e) => { + let (cache_info, (ty, value, fingerprint)) = match self.inner.fetch_object_impl( + parent, + child, + child_layout, + child_fully_annotated_layout, + &child_move_type, + )? { + ObjectResult::MismatchedType => return Ok(ObjectResult::MismatchedType), + ObjectResult::Loaded(res) => res, + }; + + if let LimitThresholdCrossed::Hard(_, lim) = check_limit_by_meter!( + self.is_metered, + store_entries_count, + self.inner + .protocol_config + .object_runtime_max_num_store_entries(), + self.inner + .protocol_config + .object_runtime_max_num_store_entries_system_tx(), + self.inner.metrics.excessive_object_runtime_store_entries + ) { + return Err(PartialVMError::new(StatusCode::MEMORY_LIMIT_EXCEEDED) + .with_message(format!( + "Object runtime store limit ({} entries) reached", + lim + )) + .with_sub_status( + VMMemoryLimitExceededSubStatusCode::OBJECT_RUNTIME_STORE_LIMIT_EXCEEDED + as u64, + )); + }; + + debug_assert_eq!( + ty, child_move_type, + "Child object type mismatch. \ + Expected {child_move_type} but found {ty}" + ); + ( + cache_info, + e.insert(ChildObject { + owner: parent, + ty, + value, + fingerprint, + }), + ) + } + btree_map::Entry::Occupied(e) => { + let child_object = e.into_mut(); + if child_object.ty != child_move_type { + return Ok(ObjectResult::MismatchedType); + } + (CacheInfo::CachedValue, child_object) + } + }; + Ok(ObjectResult::Loaded((cache_info, child_object))) + } + + pub(super) fn add_object( + &mut self, + parent: ObjectID, + child: ObjectID, + child_move_type: MoveObjectType, + child_value: Value, + ) -> PartialVMResult<()> { + if let LimitThresholdCrossed::Hard(_, lim) = check_limit_by_meter!( + self.is_metered, + self.store.len(), + self.inner + .protocol_config + .object_runtime_max_num_store_entries(), + self.inner + .protocol_config + .object_runtime_max_num_store_entries_system_tx(), + self.inner.metrics.excessive_object_runtime_store_entries + ) { + return Err(PartialVMError::new(StatusCode::MEMORY_LIMIT_EXCEEDED) + .with_message(format!( + "Object runtime store limit ({} entries) reached", + lim + )) + .with_sub_status( + VMMemoryLimitExceededSubStatusCode::OBJECT_RUNTIME_STORE_LIMIT_EXCEEDED as u64, + )); + }; + + let (mut value, fingerprint) = if let Some(ChildObject { + value, fingerprint, .. + }) = self.store.remove(&child) + { + if value.exists()? { + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message( + "Duplicate addition of a child object. \ + The previous value cannot be dropped. Indicates possible duplication \ + of objects as an object was fetched more than once from two different \ + parents, yet was not removed from one first" + .to_string(), + ), + ); + } + (value, fingerprint) + } else { + let fingerprint = ObjectFingerprint::none(self.inner.protocol_config); + (GlobalValue::none(), fingerprint) + }; + if let Err((e, _)) = value.move_to(child_value) { + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR).with_message( + format!("Unable to set value for child {child}, with error {e}",), + ), + ); + } + let child_object = ChildObject { + owner: parent, + ty: child_move_type, + value, + fingerprint, + }; + self.store.insert(child, child_object); + Ok(()) + } + + pub(super) fn config_setting_unsequenced_read( + &mut self, + config_id: ObjectID, + name_df_id: ObjectID, + field_setting_layout: &R::MoveTypeLayout, + field_setting_object_type: &MoveObjectType, + ) -> PartialVMResult>> { + let parent = config_id; + let child = name_df_id; + + let setting = match self.config_setting_cache.entry(child) { + btree_map::Entry::Vacant(e) => { + let child_move_type = field_setting_object_type; + let inner = &self.inner; + let obj_opt = + fetch_child_object_unbounded!(inner, parent, child, SequenceNumber::MAX, true); + let Some(move_obj) = obj_opt.as_ref().map(|obj| obj.data.try_as_move().unwrap()) + else { + return Ok(ObjectResult::Loaded(None)); + }; + let Some(value) = + Value::simple_deserialize(move_obj.contents(), field_setting_layout) + else { + return Err( + PartialVMError::new(StatusCode::FAILED_TO_DESERIALIZE_RESOURCE) + .with_message(format!( + "Failed to deserialize object {child} with type {field_setting_layout}", + )), + ); + }; + e.insert(ConfigSetting { + config: parent, + ty: child_move_type.clone(), + value, + }) + } + btree_map::Entry::Occupied(e) => { + let setting = e.into_mut(); + if setting.ty != *field_setting_object_type { + return Ok(ObjectResult::MismatchedType); + } + if setting.config != parent { + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message(format!( + "Parent for config setting changed. Potential hash collision? + parent: {parent}, + child: {child}, + setting_value_object_type: {field_setting_object_type}, + setting: {setting:#?}" + )), + ); + } + setting + } + }; + let value = setting.value.copy_value().map_err(|e| { + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR).with_message( + format!("Failed to copy value for config setting {child}, with error {e}",), + ) + })?; + Ok(ObjectResult::Loaded(Some(value))) + } + + /// Used by test scenario to insert a config setting into the cache, which replicates the + /// behavior of a config already being in the object store. + pub(super) fn config_setting_cache_update( + &mut self, + config_id: ObjectID, + name_df_id: ObjectID, + setting_value_object_type: MoveObjectType, + value: Option, + ) { + let child_move_type = setting_value_object_type; + match value { + Some(value) => { + let setting = ConfigSetting { + config: config_id, + ty: child_move_type, + value, + }; + self.config_setting_cache.insert(name_df_id, setting); + } + None => { + self.config_setting_cache.remove(&name_df_id); + } + } + } + + pub(super) fn cached_objects(&self) -> &BTreeMap> { + &self.inner.cached_objects + } + + pub(super) fn wrapped_object_containers(&self) -> &BTreeMap { + &self.inner.wrapped_object_containers + } + + // retrieve the `Op` effects for the child objects + pub(super) fn take_effects(&mut self) -> PartialVMResult { + if self.inner.protocol_config.minimize_child_object_mutations() { + let v1_effects = std::mem::take(&mut self.store) + .into_iter() + .map(|(id, child_object)| { + let ChildObject { + owner, + ty, + value, + fingerprint, + } = child_object; + #[cfg(debug_assertions)] + let dirty_flag_mutated = value.is_mutated(); + let final_value = value.into_value(); + let object_changed = + fingerprint.object_has_changed(&owner, &ty, &final_value)?; + // The old dirty flag was pessimistic in its tracking of mutations, meaning + // it would mark mutations even if the value remained the same. + // This means that if the object changed, the dirty flag must have been marked. + // However, we can't guarantee the opposite. + // object changed ==> dirty flag mutated + #[cfg(debug_assertions)] + debug_assert!(!object_changed || dirty_flag_mutated); + let child_effect = ChildObjectEffectV1 { + owner, + ty, + final_value, + object_changed, + }; + Ok((id, child_effect)) + }) + .collect::>()?; + Ok(ChildObjectEffects::V1(v1_effects)) + } else { + let v0_effects = std::mem::take(&mut self.store) + .into_iter() + .filter_map(|(id, child_object)| { + let ChildObject { + owner, + ty, + value, + fingerprint: _fingerprint, + } = child_object; + let effect = value.into_effect()?; + // should be disabled if the feature is disabled + #[cfg(debug_assertions)] + debug_assert!(_fingerprint.is_disabled()); + let child_effect = ChildObjectEffectV0 { owner, ty, effect }; + Some((id, child_effect)) + }) + .collect(); + Ok(ChildObjectEffects::V0(v0_effects)) + } + } + + pub(super) fn all_active_objects(&self) -> impl Iterator> { + self.store.iter().map(|(id, child_object)| { + let copied_child_value = if child_object.value.exists().unwrap() { + Some( + child_object + .value + .borrow_global() + .unwrap() + .value_as::() + .unwrap() + .read_ref() + .unwrap(), + ) + } else { + None + }; + ActiveChildObject { + id, + owner: &child_object.owner, + ty: &child_object.ty, + copied_value: copied_child_value, + } + }) + } +} + +impl ChildObjectEffects { + pub(crate) fn empty() -> Self { + ChildObjectEffects::V0(BTreeMap::new()) + } +} diff --git a/sui-execution/replay_cut/sui-move-natives/src/random.rs b/sui-execution/replay_cut/sui-move-natives/src/random.rs new file mode 100644 index 0000000000000..3281edbd83992 --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/random.rs @@ -0,0 +1,27 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::legacy_test_cost; +use move_binary_format::errors::PartialVMResult; +use move_vm_runtime::native_functions::NativeContext; +use move_vm_types::{ + loaded_data::runtime_types::Type, natives::function::NativeResult, values::Value, +}; +use rand::Rng; +use smallvec::smallvec; +use std::collections::VecDeque; + +pub fn generate_rand_seed_for_testing( + _context: &mut NativeContext, + _ty_args: Vec, + _args: VecDeque, +) -> PartialVMResult { + let mut seed = [0u8; 32]; + rand::thread_rng() + .try_fill(&mut seed) + .expect("should never fail"); + Ok(NativeResult::ok( + legacy_test_cost(), + smallvec![Value::vector_u8(seed)], + )) +} diff --git a/sui-execution/replay_cut/sui-move-natives/src/test_scenario.rs b/sui-execution/replay_cut/sui-move-natives/src/test_scenario.rs new file mode 100644 index 0000000000000..000c845843e96 --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/test_scenario.rs @@ -0,0 +1,1031 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + get_extension, get_extension_mut, get_nth_struct_field, get_tag_and_layouts, legacy_test_cost, + object_runtime::{ObjectRuntime, RuntimeResults, object_store::ChildObjectEffects}, +}; +use better_any::{Tid, TidAble}; +use indexmap::{IndexMap, IndexSet}; +use move_binary_format::errors::{PartialVMError, PartialVMResult}; +use move_core_types::{ + account_address::AccountAddress, + annotated_value::{MoveFieldLayout, MoveStructLayout, MoveTypeLayout, MoveValue}, + annotated_visitor as AV, + language_storage::StructTag, + vm_status::StatusCode, +}; +use move_vm_runtime::{native_extensions::NativeExtensionMarker, native_functions::NativeContext}; +use move_vm_types::{ + loaded_data::runtime_types::Type, + natives::function::NativeResult, + pop_arg, + values::{self, StructRef, Value, Vector, VectorSpecialization}, +}; +use smallvec::smallvec; +use std::{ + borrow::Borrow, + cell::RefCell, + collections::{BTreeMap, BTreeSet, VecDeque}, + thread::LocalKey, +}; +use sui_types::{ + TypeTag, + base_types::{MoveObjectType, ObjectID, SequenceNumber, SuiAddress}, + config, + digests::{ObjectDigest, TransactionDigest}, + dynamic_field::DynamicFieldInfo, + execution::DynamicallyLoadedObjectMetadata, + id::UID, + in_memory_storage::InMemoryStorage, + object::{MoveObject, Object, Owner}, + storage::ChildObjectResolver, +}; + +const E_COULD_NOT_GENERATE_EFFECTS: u64 = 0; +const E_INVALID_SHARED_OR_IMMUTABLE_USAGE: u64 = 1; +const E_OBJECT_NOT_FOUND_CODE: u64 = 4; +const E_UNABLE_TO_ALLOCATE_RECEIVING_TICKET: u64 = 5; +const E_RECEIVING_TICKET_ALREADY_ALLOCATED: u64 = 6; +const E_UNABLE_TO_DEALLOCATE_RECEIVING_TICKET: u64 = 7; + +type Set = IndexSet; + +/// An in-memory test store is a thin wrapper around the in-memory storage in a mutex. The mutex +/// allows this to be used by both the object runtime (for reading) and the test scenario (for +/// writing) while hiding mutability. +#[derive(Tid)] +pub struct InMemoryTestStore(pub &'static LocalKey>); +impl<'a> NativeExtensionMarker<'a> for &'a InMemoryTestStore {} + +impl ChildObjectResolver for InMemoryTestStore { + fn read_child_object( + &self, + parent: &ObjectID, + child: &ObjectID, + child_version_upper_bound: SequenceNumber, + ) -> sui_types::error::SuiResult> { + let l: &'static LocalKey> = self.0; + l.with_borrow(|store| store.read_child_object(parent, child, child_version_upper_bound)) + } + + fn get_object_received_at_version( + &self, + owner: &ObjectID, + receiving_object_id: &ObjectID, + receive_object_at_version: SequenceNumber, + epoch_id: sui_types::committee::EpochId, + ) -> sui_types::error::SuiResult> { + self.0.with_borrow(|store| { + store.get_object_received_at_version( + owner, + receiving_object_id, + receive_object_at_version, + epoch_id, + ) + }) + } +} + +// This function updates the inventories based on the transfers and deletes that occurred in the +// transaction +// native fun end_transaction(): TransactionResult; +pub fn end_transaction( + context: &mut NativeContext, + ty_args: Vec, + args: VecDeque, +) -> PartialVMResult { + assert!(ty_args.is_empty()); + assert!(args.is_empty()); + let object_runtime_ref: &mut ObjectRuntime = get_extension_mut!(context)?; + let taken_shared_or_imm: BTreeMap<_, _> = object_runtime_ref + .test_inventories + .taken + .iter() + .filter(|(_id, owner)| matches!(owner, Owner::Shared { .. } | Owner::Immutable)) + .map(|(id, owner)| (*id, owner.clone())) + .collect(); + // set to true if a shared or imm object was: + // - transferred in a way that changes it from its original shared/imm state + // - wraps the object + // if true, we will "abort" + let mut incorrect_shared_or_imm_handling = false; + + // Handle the allocated tickets: + // * Remove all allocated_tickets in the test inventories. + // * For each allocated ticket, if the ticket's object ID is loaded, move it to `received`. + // * Otherwise re-insert the allocated ticket into the objects inventory, and mark it to be + // removed from the backing storage (deferred due to needing to have access to `context` which + // has outstanding references at this point). + let allocated_tickets = + std::mem::take(&mut object_runtime_ref.test_inventories.allocated_tickets); + let mut received = BTreeMap::new(); + let mut unreceived = BTreeSet::new(); + let loaded_runtime_objects = object_runtime_ref.loaded_runtime_objects(); + for (id, (metadata, value)) in allocated_tickets { + if loaded_runtime_objects.contains_key(&id) { + received.insert(id, metadata); + } else { + unreceived.insert(id); + // This must be untouched since the allocated ticket is still live, so ok to re-insert. + object_runtime_ref + .test_inventories + .objects + .insert(id, value); + } + } + + let object_runtime_state = object_runtime_ref.take_state(); + // Determine writes and deletes + // We pass the received objects since they should be viewed as "loaded" for the purposes of + // calculating the effects of the transaction. + let results = object_runtime_state.finish(received, ChildObjectEffects::empty()); + let RuntimeResults { + writes, + user_events, + loaded_child_objects: _, + created_object_ids, + deleted_object_ids, + accumulator_events: _, + settlement_input_sui: _, + settlement_output_sui: _, + } = match results { + Ok(res) => res, + Err(_) => { + return Ok(NativeResult::err( + legacy_test_cost(), + E_COULD_NOT_GENERATE_EFFECTS, + )); + } + }; + let object_runtime_ref: &mut ObjectRuntime = get_extension_mut!(context)?; + let all_active_child_objects_with_values = object_runtime_ref + .all_active_child_objects() + .filter(|child| child.copied_value.is_some()) + .map(|child| *child.id) + .collect::>(); + let inventories = &mut object_runtime_ref.test_inventories; + let mut new_object_values = IndexMap::new(); + let mut transferred = vec![]; + // cleanup inventories + // we will remove all changed objects + // - deleted objects need to be removed to mark deletions + // - written objects are removed and later replaced to mark new values and new owners + // - child objects will not be reflected in transfers, but need to be no longer retrievable + for id in deleted_object_ids + .iter() + .chain(writes.keys()) + .chain(&all_active_child_objects_with_values) + { + for addr_inventory in inventories.address_inventories.values_mut() { + for s in addr_inventory.values_mut() { + s.shift_remove(id); + } + } + for s in &mut inventories.shared_inventory.values_mut() { + s.shift_remove(id); + } + for s in &mut inventories.immutable_inventory.values_mut() { + s.shift_remove(id); + } + inventories.taken.remove(id); + } + + // handle transfers, inserting transferred/written objects into their respective inventory + let mut created = vec![]; + let mut written = vec![]; + for (id, (owner, ty, value)) in writes { + // write configs to cache + new_object_values.insert(id, (ty.clone(), value.copy_value().unwrap())); + transferred.push((id, owner.clone())); + incorrect_shared_or_imm_handling = incorrect_shared_or_imm_handling + || taken_shared_or_imm + .get(&id) + .map(|shared_or_imm_owner| shared_or_imm_owner != &owner) + .unwrap_or(/* not incorrect */ false); + if created_object_ids.contains(&id) { + created.push(id); + } else { + written.push(id); + } + match owner { + Owner::AddressOwner(a) => { + inventories + .address_inventories + .entry(a) + .or_default() + .entry(ty) + .or_default() + .insert(id); + } + Owner::ObjectOwner(_) => (), + Owner::Shared { .. } => { + inventories + .shared_inventory + .entry(ty) + .or_default() + .insert(id); + } + Owner::Immutable => { + inventories + .immutable_inventory + .entry(ty) + .or_default() + .insert(id); + } + Owner::ConsensusAddressOwner { owner, .. } => { + inventories + .address_inventories + .entry(owner) + .or_default() + .entry(ty) + .or_default() + .insert(id); + } + } + } + + // For any unused allocated tickets, remove them from the store. + let store: &&InMemoryTestStore = get_extension!(context)?; + for id in unreceived { + if store + .0 + .with_borrow_mut(|store| store.remove_object(id).is_none()) + { + return Ok(NativeResult::err( + context.gas_used(), + E_UNABLE_TO_DEALLOCATE_RECEIVING_TICKET, + )); + } + } + + // deletions already handled above, but we drop the delete kind for the effects + let mut deleted = vec![]; + for id in deleted_object_ids { + // Mark as "incorrect" if a imm object was deleted. Allow shared objects to be deleted though. + incorrect_shared_or_imm_handling = incorrect_shared_or_imm_handling + || taken_shared_or_imm + .get(&id) + .is_some_and(|owner| matches!(owner, Owner::Immutable)); + deleted.push(id); + } + // find all wrapped objects + let mut all_wrapped = BTreeSet::new(); + let object_runtime_ref: &ObjectRuntime = get_extension!(context)?; + find_all_wrapped_objects( + context, + &mut all_wrapped, + new_object_values + .iter() + .map(|(id, (ty, value))| (id, ty, value)), + ); + find_all_wrapped_objects( + context, + &mut all_wrapped, + object_runtime_ref + .all_active_child_objects() + .filter_map(|child| Some((child.id, child.ty, child.copied_value?))), + ); + // mark as "incorrect" if a shared/imm object was wrapped or is a child object + incorrect_shared_or_imm_handling = incorrect_shared_or_imm_handling + || taken_shared_or_imm.keys().any(|id| { + all_wrapped.contains(id) || all_active_child_objects_with_values.contains(id) + }); + // if incorrect handling, return with an 'abort' + if incorrect_shared_or_imm_handling { + return Ok(NativeResult::err( + legacy_test_cost(), + E_INVALID_SHARED_OR_IMMUTABLE_USAGE, + )); + } + + // mark all wrapped as deleted + for wrapped in all_wrapped { + deleted.push(wrapped) + } + + // new input objects are remaining taken objects not written/deleted + let object_runtime_ref: &mut ObjectRuntime = get_extension_mut!(context)?; + let mut config_settings = vec![]; + for child in object_runtime_ref.all_active_child_objects() { + let s: StructTag = child.ty.clone().into(); + let is_setting = DynamicFieldInfo::is_dynamic_field(&s) + && matches!(&s.type_params[1], TypeTag::Struct(s) if config::is_setting(s)); + if is_setting { + config_settings.push(( + *child.owner, + *child.id, + child.ty.clone(), + child.copied_value, + )); + } + } + for (config, setting, ty, value) in config_settings { + object_runtime_ref.config_setting_cache_update(config, setting, ty, value) + } + object_runtime_ref.state.input_objects = object_runtime_ref + .test_inventories + .taken + .iter() + .map(|(id, owner)| (*id, owner.clone())) + .collect::>(); + // update inventories + // check for bad updates to immutable values + for (id, (ty, value)) in new_object_values { + debug_assert!(!all_active_child_objects_with_values.contains(&id)); + if let Some(prev_value) = object_runtime_ref + .test_inventories + .taken_immutable_values + .get(&ty) + .and_then(|values| values.get(&id)) + && !value.equals(prev_value)? + { + return Ok(NativeResult::err( + legacy_test_cost(), + E_INVALID_SHARED_OR_IMMUTABLE_USAGE, + )); + } + object_runtime_ref + .test_inventories + .objects + .insert(id, value); + } + // remove deleted + for id in &deleted { + object_runtime_ref.test_inventories.objects.remove(id); + } + // remove active child objects + for id in all_active_child_objects_with_values { + object_runtime_ref.test_inventories.objects.remove(&id); + } + + let effects = transaction_effects( + created, + written, + deleted, + transferred, + user_events.len() as u64, + // TODO: do we need accumulator events here? + ); + Ok(NativeResult::ok(legacy_test_cost(), smallvec![effects])) +} + +// native fun take_from_address_by_id(account: address, id: ID): T; +pub fn take_from_address_by_id( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + let specified_ty = get_specified_ty(ty_args); + let id = pop_id(&mut args)?; + let account: SuiAddress = pop_arg!(args, AccountAddress).into(); + pop_arg!(args, StructRef); + assert!(args.is_empty()); + let specified_obj_ty = object_type_of_type(context, &specified_ty)?; + let object_runtime: &mut ObjectRuntime = get_extension_mut!(context)?; + let inventories = &mut object_runtime.test_inventories; + let res = take_from_inventory( + |x| { + inventories + .address_inventories + .get(&account) + .and_then(|inv| inv.get(&specified_obj_ty)) + .map(|s| s.contains(x)) + .unwrap_or(false) + }, + &inventories.objects, + &mut inventories.taken, + &mut object_runtime.state.input_objects, + id, + Owner::AddressOwner(account), + ); + Ok(match res { + Ok(value) => NativeResult::ok(legacy_test_cost(), smallvec![value]), + Err(native_err) => native_err, + }) +} + +// native fun ids_for_address(account: address): vector; +pub fn ids_for_address( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + let specified_ty = get_specified_ty(ty_args); + let account: SuiAddress = pop_arg!(args, AccountAddress).into(); + assert!(args.is_empty()); + let specified_obj_ty = object_type_of_type(context, &specified_ty)?; + let object_runtime: &mut ObjectRuntime = get_extension_mut!(context)?; + let inventories = &mut object_runtime.test_inventories; + let ids = inventories + .address_inventories + .get(&account) + .and_then(|inv| inv.get(&specified_obj_ty)) + .map(|s| s.iter().map(|id| pack_id(*id)).collect::>()) + .unwrap_or_default(); + let ids_vector = Vector::pack(VectorSpecialization::Container, ids).unwrap(); + Ok(NativeResult::ok(legacy_test_cost(), smallvec![ids_vector])) +} + +// native fun most_recent_id_for_address(account: address): Option; +pub fn most_recent_id_for_address( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + let specified_ty = get_specified_ty(ty_args); + let account: SuiAddress = pop_arg!(args, AccountAddress).into(); + assert!(args.is_empty()); + let specified_obj_ty = object_type_of_type(context, &specified_ty)?; + let object_runtime: &mut ObjectRuntime = get_extension_mut!(context)?; + let inventories = &mut object_runtime.test_inventories; + let most_recent_id = match inventories.address_inventories.get(&account) { + None => pack_option(vector_specialization(&specified_ty), None), + Some(inv) => most_recent_at_ty(&inventories.taken, inv, &specified_ty, specified_obj_ty), + }; + Ok(NativeResult::ok( + legacy_test_cost(), + smallvec![most_recent_id], + )) +} + +// native fun was_taken_from_address(account: address, id: ID): bool; +pub fn was_taken_from_address( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + assert!(ty_args.is_empty()); + let id = pop_id(&mut args)?; + let account: SuiAddress = pop_arg!(args, AccountAddress).into(); + assert!(args.is_empty()); + let object_runtime: &mut ObjectRuntime = get_extension_mut!(context)?; + let inventories = &mut object_runtime.test_inventories; + let was_taken = inventories + .taken + .get(&id) + .map(|owner| owner == &Owner::AddressOwner(account)) + .unwrap_or(false); + Ok(NativeResult::ok( + legacy_test_cost(), + smallvec![Value::bool(was_taken)], + )) +} + +// native fun take_immutable_by_id(id: ID): T; +pub fn take_immutable_by_id( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + let specified_ty = get_specified_ty(ty_args); + let id = pop_id(&mut args)?; + pop_arg!(args, StructRef); + assert!(args.is_empty()); + let specified_obj_ty = object_type_of_type(context, &specified_ty)?; + let object_runtime: &mut ObjectRuntime = get_extension_mut!(context)?; + let inventories = &mut object_runtime.test_inventories; + let res = take_from_inventory( + |x| { + inventories + .immutable_inventory + .get(&specified_obj_ty) + .map(|s| s.contains(x)) + .unwrap_or(false) + }, + &inventories.objects, + &mut inventories.taken, + &mut object_runtime.state.input_objects, + id, + Owner::Immutable, + ); + Ok(match res { + Ok(value) => { + inventories + .taken_immutable_values + .entry(specified_obj_ty) + .or_default() + .insert(id, value.copy_value().unwrap()); + NativeResult::ok(legacy_test_cost(), smallvec![value]) + } + Err(native_err) => native_err, + }) +} + +// native fun most_recent_immutable_id(): Option; +pub fn most_recent_immutable_id( + context: &mut NativeContext, + ty_args: Vec, + args: VecDeque, +) -> PartialVMResult { + let specified_ty = get_specified_ty(ty_args); + assert!(args.is_empty()); + let specified_obj_ty = object_type_of_type(context, &specified_ty)?; + let object_runtime: &mut ObjectRuntime = get_extension_mut!(context)?; + let inventories = &mut object_runtime.test_inventories; + let most_recent_id = most_recent_at_ty( + &inventories.taken, + &inventories.immutable_inventory, + &specified_ty, + specified_obj_ty, + ); + Ok(NativeResult::ok( + legacy_test_cost(), + smallvec![most_recent_id], + )) +} + +// native fun was_taken_immutable(id: ID): bool; +pub fn was_taken_immutable( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + assert!(ty_args.is_empty()); + let id = pop_id(&mut args)?; + assert!(args.is_empty()); + let object_runtime: &mut ObjectRuntime = get_extension_mut!(context)?; + let inventories = &mut object_runtime.test_inventories; + let was_taken = inventories + .taken + .get(&id) + .map(|owner| owner == &Owner::Immutable) + .unwrap_or(false); + Ok(NativeResult::ok( + legacy_test_cost(), + smallvec![Value::bool(was_taken)], + )) +} + +// native fun take_shared_by_id(id: ID): T; +pub fn take_shared_by_id( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + let specified_ty = get_specified_ty(ty_args); + let id = pop_id(&mut args)?; + pop_arg!(args, StructRef); + assert!(args.is_empty()); + let specified_obj_ty = object_type_of_type(context, &specified_ty)?; + let object_runtime: &mut ObjectRuntime = get_extension_mut!(context)?; + let inventories = &mut object_runtime.test_inventories; + let res = take_from_inventory( + |x| { + inventories + .shared_inventory + .get(&specified_obj_ty) + .map(|s| s.contains(x)) + .unwrap_or(false) + }, + &inventories.objects, + &mut inventories.taken, + &mut object_runtime.state.input_objects, + id, + Owner::Shared { initial_shared_version: /* dummy */ SequenceNumber::new() }, + ); + Ok(match res { + Ok(value) => NativeResult::ok(legacy_test_cost(), smallvec![value]), + Err(native_err) => native_err, + }) +} + +// native fun most_recent_id_shared(): Option; +pub fn most_recent_id_shared( + context: &mut NativeContext, + ty_args: Vec, + args: VecDeque, +) -> PartialVMResult { + let specified_ty = get_specified_ty(ty_args); + assert!(args.is_empty()); + let specified_obj_ty = object_type_of_type(context, &specified_ty)?; + let object_runtime: &mut ObjectRuntime = get_extension_mut!(context)?; + let inventories = &mut object_runtime.test_inventories; + let most_recent_id = most_recent_at_ty( + &inventories.taken, + &inventories.shared_inventory, + &specified_ty, + specified_obj_ty, + ); + Ok(NativeResult::ok( + legacy_test_cost(), + smallvec![most_recent_id], + )) +} + +// native fun was_taken_shared(id: ID): bool; +pub fn was_taken_shared( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + assert!(ty_args.is_empty()); + let id = pop_id(&mut args)?; + assert!(args.is_empty()); + let object_runtime: &mut ObjectRuntime = get_extension_mut!(context)?; + let inventories = &mut object_runtime.test_inventories; + let was_taken = inventories + .taken + .get(&id) + .map(|owner| matches!(owner, Owner::Shared { .. })) + .unwrap_or(false); + Ok(NativeResult::ok( + legacy_test_cost(), + smallvec![Value::bool(was_taken)], + )) +} + +pub fn allocate_receiving_ticket_for_object( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + let ty = get_specified_ty(ty_args); + let id = pop_id(&mut args)?; + + let abilities = context.type_to_abilities(&ty)?; + let Some((tag, layout, _)) = get_tag_and_layouts(context, &ty)? else { + return Ok(NativeResult::err( + context.gas_used(), + E_UNABLE_TO_ALLOCATE_RECEIVING_TICKET, + )); + }; + let object_runtime: &mut ObjectRuntime = get_extension_mut!(context)?; + let object_version = SequenceNumber::new(); + let inventories = &mut object_runtime.test_inventories; + if inventories.allocated_tickets.contains_key(&id) { + return Ok(NativeResult::err( + context.gas_used(), + E_RECEIVING_TICKET_ALREADY_ALLOCATED, + )); + } + + let obj_value = inventories.objects.remove(&id).unwrap(); + let Some(bytes) = obj_value.typed_serialize(&layout) else { + return Ok(NativeResult::err( + context.gas_used(), + E_UNABLE_TO_ALLOCATE_RECEIVING_TICKET, + )); + }; + let has_public_transfer = abilities.has_store(); + let move_object = unsafe { + MoveObject::new_from_execution_with_limit( + tag.into(), + has_public_transfer, + object_version, + bytes, + 250 * 1024, + ) + } + .unwrap(); + + let Some((owner, _)) = inventories + .address_inventories + .iter() + .find(|(_addr, objs)| objs.iter().any(|(_, ids)| ids.contains(&id))) + else { + return Ok(NativeResult::err( + context.gas_used(), + E_OBJECT_NOT_FOUND_CODE, + )); + }; + + inventories.allocated_tickets.insert( + id, + ( + DynamicallyLoadedObjectMetadata { + version: SequenceNumber::new(), + digest: ObjectDigest::MIN, + owner: Owner::AddressOwner(*owner), + storage_rebate: 0, + previous_transaction: TransactionDigest::default(), + }, + obj_value, + ), + ); + + let object = Object::new_move( + move_object, + Owner::AddressOwner(*owner), + TransactionDigest::default(), + ); + + // NB: Must be a `&&` reference since the extension stores a static ref to the object storage. + let store: &&InMemoryTestStore = get_extension!(context)?; + store.0.with_borrow_mut(|store| store.insert_object(object)); + + Ok(NativeResult::ok( + legacy_test_cost(), + smallvec![Value::u64(object_version.value())], + )) +} + +pub fn deallocate_receiving_ticket_for_object( + context: &mut NativeContext, + _ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + let id = pop_id(&mut args)?; + + let object_runtime: &mut ObjectRuntime = get_extension_mut!(context)?; + let inventories = &mut object_runtime.test_inventories; + // Deallocate the ticket -- we should never hit this scenario + let Some((_, value)) = inventories.allocated_tickets.remove(&id) else { + return Ok(NativeResult::err( + context.gas_used(), + E_UNABLE_TO_DEALLOCATE_RECEIVING_TICKET, + )); + }; + + // Insert the object value that we saved from earlier and put it back into the object set. + // This is fine since it can't have been touched. + inventories.objects.insert(id, value); + + // Remove the object from storage. We should never hit this scenario either. + let store: &&InMemoryTestStore = get_extension!(context)?; + if store + .0 + .with_borrow_mut(|store| store.remove_object(id).is_none()) + { + return Ok(NativeResult::err( + context.gas_used(), + E_UNABLE_TO_DEALLOCATE_RECEIVING_TICKET, + )); + }; + + Ok(NativeResult::ok(legacy_test_cost(), smallvec![])) +} + +// impls + +fn take_from_inventory( + is_in_inventory: impl FnOnce(&ObjectID) -> bool, + objects: &BTreeMap, + taken: &mut BTreeMap, + input_objects: &mut BTreeMap, + id: ObjectID, + owner: Owner, +) -> Result { + let obj_opt = objects.get(&id); + let is_taken = taken.contains_key(&id); + if is_taken || !is_in_inventory(&id) || obj_opt.is_none() { + return Err(NativeResult::err( + legacy_test_cost(), + E_OBJECT_NOT_FOUND_CODE, + )); + } + taken.insert(id, owner.clone()); + input_objects.insert(id, owner); + let obj = obj_opt.unwrap(); + Ok(obj.copy_value().unwrap()) +} + +fn vector_specialization(ty: &Type) -> VectorSpecialization { + match ty.try_into() { + Ok(s) => s, + Err(_) => { + debug_assert!(false, "Invalid vector specialization"); + VectorSpecialization::Container + } + } +} + +fn most_recent_at_ty( + taken: &BTreeMap, + inv: &BTreeMap>, + runtime_ty: &Type, + ty: MoveObjectType, +) -> Value { + pack_option( + vector_specialization(runtime_ty), + most_recent_at_ty_opt(taken, inv, ty), + ) +} + +fn most_recent_at_ty_opt( + taken: &BTreeMap, + inv: &BTreeMap>, + ty: MoveObjectType, +) -> Option { + let s = inv.get(&ty)?; + let most_recent_id = s.iter().filter(|id| !taken.contains_key(id)).next_back()?; + Some(pack_id(*most_recent_id)) +} + +fn get_specified_ty(mut ty_args: Vec) -> Type { + assert!(ty_args.len() == 1); + ty_args.pop().unwrap() +} + +// helpers +fn pop_id(args: &mut VecDeque) -> PartialVMResult { + let v = match args.pop_back() { + None => { + return Err(PartialVMError::new( + StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR, + )); + } + Some(v) => v, + }; + Ok(get_nth_struct_field(v, 0)? + .value_as::()? + .into()) +} + +fn pack_id(a: impl Into) -> Value { + Value::struct_(values::Struct::pack(vec![Value::address(a.into())])) +} + +fn pack_ids(items: impl IntoIterator>) -> Value { + Vector::pack( + VectorSpecialization::Container, + items.into_iter().map(pack_id), + ) + .unwrap() +} + +fn pack_vec_map(items: impl IntoIterator) -> Value { + Value::struct_(values::Struct::pack(vec![ + Vector::pack( + VectorSpecialization::Container, + items + .into_iter() + .map(|(k, v)| Value::struct_(values::Struct::pack(vec![k, v]))), + ) + .unwrap(), + ])) +} + +fn transaction_effects( + created: impl IntoIterator>, + written: impl IntoIterator>, + deleted: impl IntoIterator>, + transferred: impl IntoIterator, + num_events: u64, +) -> Value { + let mut transferred_to_account = vec![]; + let mut transferred_to_object = vec![]; + let mut shared = vec![]; + let mut frozen = vec![]; + for (id, owner) in transferred { + match owner { + Owner::AddressOwner(a) => { + transferred_to_account.push((pack_id(id), Value::address(a.into()))) + } + Owner::ObjectOwner(o) => transferred_to_object.push((pack_id(id), pack_id(o))), + Owner::Shared { .. } => shared.push(id), + Owner::Immutable => frozen.push(id), + Owner::ConsensusAddressOwner { owner, .. } => { + transferred_to_account.push((pack_id(id), Value::address(owner.into()))) + } + } + } + + let created_field = pack_ids(created); + let written_field = pack_ids(written); + let deleted_field = pack_ids(deleted); + let transferred_to_account_field = pack_vec_map(transferred_to_account); + let transferred_to_object_field = pack_vec_map(transferred_to_object); + let shared_field = pack_ids(shared); + let frozen_field = pack_ids(frozen); + let num_events_field = Value::u64(num_events); + Value::struct_(values::Struct::pack(vec![ + created_field, + written_field, + deleted_field, + transferred_to_account_field, + transferred_to_object_field, + shared_field, + frozen_field, + num_events_field, + ])) +} + +fn object_type_of_type(context: &NativeContext, ty: &Type) -> PartialVMResult { + let TypeTag::Struct(s_tag) = context.type_to_type_tag(ty)? else { + return Err(PartialVMError::new( + StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR, + )); + }; + Ok(MoveObjectType::from(*s_tag)) +} + +fn pack_option(specialization: VectorSpecialization, opt: Option) -> Value { + let item = match opt { + Some(v) => vec![v], + None => vec![], + }; + Value::struct_(values::Struct::pack(vec![ + Vector::pack(specialization, item).unwrap(), + ])) +} + +fn find_all_wrapped_objects<'a, 'i>( + context: &NativeContext, + ids: &'i mut BTreeSet, + new_object_values: impl IntoIterator)>, +) { + #[derive(Copy, Clone)] + enum LookingFor { + Wrapped, + Uid, + Address, + } + + struct Traversal<'i, 'u> { + state: LookingFor, + ids: &'i mut BTreeSet, + uid: &'u MoveStructLayout, + } + + impl<'b, 'l> AV::Traversal<'b, 'l> for Traversal<'_, '_> { + type Error = AV::Error; + + fn traverse_struct( + &mut self, + driver: &mut AV::StructDriver<'_, 'b, 'l>, + ) -> Result<(), Self::Error> { + match self.state { + // We're at the top-level of the traversal, looking for an object to recurse into. + // We can unconditionally switch to looking for UID fields at the level below, + // because we know that all the top-level values are objects. + LookingFor::Wrapped => { + while driver + .next_field(&mut Traversal { + state: LookingFor::Uid, + ids: self.ids, + uid: self.uid, + })? + .is_some() + {} + } + + // We are looking for UID fields. If we find one (which we confirm by checking its + // layout), switch to looking for addresses in its sub-structure. + LookingFor::Uid => { + while let Some(MoveFieldLayout { name: _, layout }) = driver.peek_field() { + if matches!(layout, MoveTypeLayout::Struct(s) if s.as_ref() == self.uid) { + driver.next_field(&mut Traversal { + state: LookingFor::Address, + ids: self.ids, + uid: self.uid, + })?; + } else { + driver.next_field(self)?; + } + } + } + + // When looking for addresses, recurse through structs, as the address is nested + // within the UID. + LookingFor::Address => while driver.next_field(self)?.is_some() {}, + } + + Ok(()) + } + + fn traverse_address( + &mut self, + _: &AV::ValueDriver<'_, 'b, 'l>, + address: AccountAddress, + ) -> Result<(), Self::Error> { + // If we're looking for addresses, and we found one, then save it. + if matches!(self.state, LookingFor::Address) { + self.ids.insert(address.into()); + } + Ok(()) + } + } + + let uid = UID::layout(); + for (_id, ty, value) in new_object_values { + let type_tag = TypeTag::from(ty.clone()); + // NB: We can get the layout from the VM's cache since the types and modules + // associated with all of these types must be in the type/module cache in the VM -- THIS IS + // BECAUSE WE ARE IN TEST SCENARIO ONLY AND THIS MAY NOT GENERALLY HOLD IN A + // MULTI-TRANSACTION SETTING. + let Ok(Some(layout)) = context.type_tag_to_layout_for_test_scenario_only(&type_tag) else { + debug_assert!(false); + continue; + }; + + let Ok(Some(annotated_layout)) = + context.type_tag_to_fully_annotated_layout_for_test_scenario_only(&type_tag) + else { + debug_assert!(false); + continue; + }; + + let blob = value.borrow().typed_serialize(&layout).unwrap(); + MoveValue::visit_deserialize( + &blob, + &annotated_layout, + &mut Traversal { + state: LookingFor::Wrapped, + ids, + uid: &uid, + }, + ) + .unwrap(); + } +} diff --git a/sui-execution/replay_cut/sui-move-natives/src/test_utils.rs b/sui-execution/replay_cut/sui-move-natives/src/test_utils.rs new file mode 100644 index 0000000000000..760a606d48485 --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/test_utils.rs @@ -0,0 +1,41 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::{legacy_test_cost, types::is_otw_struct}; +use move_binary_format::errors::PartialVMResult; +use move_core_types::{gas_algebra::InternalGas, runtime_value::MoveTypeLayout}; +use move_vm_runtime::native_functions::NativeContext; +use move_vm_types::{ + loaded_data::runtime_types::Type, natives::function::NativeResult, values::Value, +}; +use smallvec::smallvec; +use std::collections::VecDeque; + +pub fn create_one_time_witness( + context: &mut NativeContext, + mut ty_args: Vec, + args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.len() == 1); + debug_assert!(args.is_empty()); + + let ty = ty_args.pop().unwrap(); + let type_tag = context.type_to_type_tag(&ty)?; + let type_layout = context.type_to_type_layout(&ty)?; + + let Some(MoveTypeLayout::Struct(struct_layout)) = type_layout else { + return Ok(NativeResult::err(InternalGas::new(1), 0)); + }; + + let hardened_check = context.runtime_limits_config().hardened_otw_check; + if is_otw_struct(&struct_layout, &type_tag, hardened_check) { + Ok(NativeResult::ok( + legacy_test_cost(), + smallvec![Value::struct_(move_vm_types::values::Struct::pack(vec![ + Value::bool(true) + ]))], + )) + } else { + Ok(NativeResult::err(InternalGas::new(1), 1)) + } +} diff --git a/sui-execution/replay_cut/sui-move-natives/src/transaction_context.rs b/sui-execution/replay_cut/sui-move-natives/src/transaction_context.rs new file mode 100644 index 0000000000000..6cc8f21f14d36 --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/transaction_context.rs @@ -0,0 +1,115 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use better_any::{Tid, TidAble}; +use move_binary_format::errors::{PartialVMError, PartialVMResult}; +use move_core_types::{account_address::AccountAddress, vm_status::StatusCode}; +use move_vm_runtime::native_extensions::NativeExtensionMarker; +use std::{cell::RefCell, rc::Rc}; +use sui_types::{ + base_types::{ObjectID, SuiAddress, TxContext}, + committee::EpochId, + digests::TransactionDigest, +}; + +// TransactionContext is a wrapper around TxContext that is exposed to NativeContextExtensions +// in order to provide transaction context information to Move native functions. +// Holds a Rc> to allow for mutation of the TxContext. +#[derive(Tid)] +pub struct TransactionContext { + pub(crate) tx_context: Rc>, + test_only: bool, +} + +impl NativeExtensionMarker<'_> for TransactionContext {} + +impl TransactionContext { + pub fn new(tx_context: Rc>) -> Self { + Self { + tx_context, + test_only: false, + } + } + + pub fn new_for_testing(tx_context: Rc>) -> Self { + Self { + tx_context, + test_only: true, + } + } + + pub fn sender(&self) -> SuiAddress { + self.tx_context.borrow().sender() + } + + pub fn epoch(&self) -> EpochId { + self.tx_context.borrow().epoch() + } + + pub fn epoch_timestamp_ms(&self) -> u64 { + self.tx_context.borrow().epoch_timestamp_ms() + } + + pub fn digest(&self) -> TransactionDigest { + self.tx_context.borrow().digest() + } + + pub fn sponsor(&self) -> Option { + self.tx_context.borrow().sponsor() + } + + pub fn rgp(&self) -> u64 { + self.tx_context.borrow().rgp() + } + + pub fn gas_price(&self) -> u64 { + self.tx_context.borrow().gas_price() + } + + pub fn gas_budget(&self) -> u64 { + self.tx_context.borrow().gas_budget() + } + + pub fn ids_created(&self) -> u64 { + self.tx_context.borrow().ids_created() + } + + pub fn fresh_id(&self) -> ObjectID { + self.tx_context.borrow_mut().fresh_id() + } + + // + // Test only function + // + pub fn replace( + &self, + sender: AccountAddress, + tx_hash: Vec, + epoch: u64, + epoch_timestamp_ms: u64, + ids_created: u64, + rgp: u64, + gas_price: u64, + gas_budget: u64, + sponsor: Option, + ) -> PartialVMResult<()> { + if !self.test_only { + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("`replace` called on a non testing scenario".to_string()), + ); + } + self.tx_context.borrow_mut().replace( + sender, + tx_hash, + epoch, + epoch_timestamp_ms, + ids_created, + rgp, + gas_price, + gas_budget, + sponsor, + ); + Ok(()) + } +} diff --git a/sui-execution/replay_cut/sui-move-natives/src/transfer.rs b/sui-execution/replay_cut/sui-move-natives/src/transfer.rs new file mode 100644 index 0000000000000..51a3dec06a347 --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/transfer.rs @@ -0,0 +1,333 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use super::object_runtime::{ObjectRuntime, TransferResult}; +use crate::{ + NativesCostTable, get_extension, get_extension_mut, get_receiver_object_id, + get_tag_and_layouts, object_runtime::object_store::ObjectResult, +}; +use move_binary_format::errors::{PartialVMError, PartialVMResult}; +use move_core_types::{ + account_address::AccountAddress, gas_algebra::InternalGas, language_storage::TypeTag, + vm_status::StatusCode, +}; +use move_vm_runtime::{native_charge_gas_early_exit, native_functions::NativeContext}; +use move_vm_types::{ + loaded_data::runtime_types::Type, natives::function::NativeResult, pop_arg, values::Value, +}; +use smallvec::smallvec; +use std::collections::VecDeque; +use sui_types::{ + base_types::{MoveObjectType, ObjectID, SequenceNumber}, + object::Owner, +}; + +const E_SHARED_NON_NEW_OBJECT: u64 = 0; +const E_BCS_SERIALIZATION_FAILURE: u64 = 1; +const E_RECEIVING_OBJECT_TYPE_MISMATCH: u64 = 2; +// Represents both the case where the object does not exist and the case where the object is not +// able to be accessed through the parent that is passed-in. +const E_UNABLE_TO_RECEIVE_OBJECT: u64 = 3; +// Operation not yet supported +const E_NOT_SUPPORTED: u64 = 5; + +#[derive(Clone, Debug)] +pub struct TransferReceiveObjectInternalCostParams { + pub transfer_receive_object_internal_cost_base: InternalGas, +} +/*************************************************************************************************** +* native fun receive_object_internal +* Implementation of the Move native function `receive_object_internal(parent: &mut UID, rec: Receiver): T` +* gas cost: transfer_receive_object_internal_cost_base | covers various fixed costs in the oper +**************************************************************************************************/ + +pub fn receive_object_internal( + context: &mut NativeContext, + mut ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.len() == 1); + debug_assert!(args.len() == 3); + let transfer_receive_object_internal_cost_params = get_extension!(context, NativesCostTable)? + .transfer_receive_object_internal_cost_params + .clone(); + native_charge_gas_early_exit!( + context, + transfer_receive_object_internal_cost_params.transfer_receive_object_internal_cost_base + ); + let child_ty = ty_args.pop().unwrap(); + let child_receiver_sequence_number: SequenceNumber = pop_arg!(args, u64).into(); + let child_receiver_object_id = args.pop_back().unwrap(); + let parent = pop_arg!(args, AccountAddress).into(); + assert!(args.is_empty()); + let child_id: ObjectID = get_receiver_object_id(child_receiver_object_id.copy_value().unwrap()) + .unwrap() + .value_as::() + .unwrap() + .into(); + assert!(ty_args.is_empty()); + + let Some((tag, layout, annotated_layout)) = get_tag_and_layouts(context, &child_ty)? else { + return Ok(NativeResult::err( + context.gas_used(), + E_BCS_SERIALIZATION_FAILURE, + )); + }; + + let object_runtime: &mut ObjectRuntime = get_extension_mut!(context)?; + let (_cache_info, child) = match object_runtime.receive_object( + parent, + child_id, + child_receiver_sequence_number, + &layout, + &annotated_layout, + MoveObjectType::from(tag), + ) { + // NB: Loaded and doesn't exist and inauthenticated read should lead to the exact same error + Ok(None) => { + return Ok(NativeResult::err( + context.gas_used(), + E_UNABLE_TO_RECEIVE_OBJECT, + )); + } + Ok(Some(ObjectResult::Loaded(gv))) => gv, + Ok(Some(ObjectResult::MismatchedType)) => { + return Ok(NativeResult::err( + context.gas_used(), + E_RECEIVING_OBJECT_TYPE_MISMATCH, + )); + } + Err(x) => return Err(x), + }; + + Ok(NativeResult::ok(context.gas_used(), smallvec![child])) +} + +#[derive(Clone, Debug)] +pub struct TransferInternalCostParams { + pub transfer_transfer_internal_cost_base: InternalGas, +} +/*************************************************************************************************** +* native fun transfer_impl +* Implementation of the Move native function `transfer_impl(obj: T, recipient: address)` +* gas cost: transfer_transfer_internal_cost_base | covers various fixed costs in the oper +**************************************************************************************************/ +pub fn transfer_internal( + context: &mut NativeContext, + mut ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.len() == 1); + debug_assert!(args.len() == 2); + + let transfer_transfer_internal_cost_params = get_extension!(context, NativesCostTable)? + .transfer_transfer_internal_cost_params + .clone(); + + native_charge_gas_early_exit!( + context, + transfer_transfer_internal_cost_params.transfer_transfer_internal_cost_base + ); + + let ty = ty_args.pop().unwrap(); + let recipient = pop_arg!(args, AccountAddress); + let obj = args.pop_back().unwrap(); + + let owner = Owner::AddressOwner(recipient.into()); + object_runtime_transfer(context, owner, ty, obj)?; + let cost = context.gas_used(); + Ok(NativeResult::ok(cost, smallvec![])) +} + +#[derive(Clone, Debug)] +pub struct PartyTransferInternalCostParams { + pub transfer_party_transfer_internal_cost_base: Option, +} + +macro_rules! native_charge_gas_early_exit_option { + ($native_context:ident, $cost:expr) => {{ + use move_binary_format::errors::PartialVMError; + use move_core_types::vm_status::StatusCode; + native_charge_gas_early_exit!( + $native_context, + $cost.ok_or_else(|| { + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("Gas cost for party is missing".to_string()) + })? + ); + }}; +} +/*************************************************************************************************** +* native fun multi_partytransfer_impl +* Implementation of the Move native function +* `party_transfer_impl(obj: T, recipient: address)` +* gas cost: transfer_party_transfer_internal_cost_base | covers various fixed costs in the oper +**************************************************************************************************/ +pub fn party_transfer_internal( + context: &mut NativeContext, + mut ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + const NONE: u64 = 0; + const READ: u64 = 0b0001; + const WRITE: u64 = 0b0010; + const DELETE: u64 = 0b0100; + const TRANSFER: u64 = 0b1000; + const ALL: u64 = READ | WRITE | DELETE | TRANSFER; + + debug_assert!(ty_args.len() == 1); + debug_assert!(args.len() == 4); + + let is_supported = get_extension!(context, ObjectRuntime)? + .protocol_config + .enable_party_transfer(); + if !is_supported { + let cost = context.gas_used(); + return Ok(NativeResult::err(cost, E_NOT_SUPPORTED)); + } + + let transfer_party_transfer_internal_cost_params = get_extension!(context, NativesCostTable)? + .transfer_party_transfer_internal_cost_params + .clone(); + + native_charge_gas_early_exit_option!( + context, + transfer_party_transfer_internal_cost_params.transfer_party_transfer_internal_cost_base + ); + + let ty = ty_args.pop().unwrap(); + let permissions = pop_arg!(args, Vec); + let addresses = pop_arg!(args, Vec); + let default_permissions = pop_arg!(args, u64); + let obj = args.pop_back().unwrap(); + let Ok([permissions]): Result<[u64; 1], _> = permissions.try_into() else { + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("Party transfer only supports one party member".to_string()), + ); + }; + if permissions != ALL || default_permissions != NONE { + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR).with_message( + "Party transfer only supports one party member with all permissions".to_string(), + ), + ); + } + let Ok([address]): Result<[AccountAddress; 1], _> = addresses.try_into() else { + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("Party transfer only supports one party member".to_string()), + ); + }; + + // Dummy version, to be filled with the correct initial version when the effects of the + // transaction are written to storage. + let owner = Owner::ConsensusAddressOwner { + start_version: SequenceNumber::new(), + owner: address.into(), + }; + object_runtime_transfer(context, owner, ty, obj)?; + let cost = context.gas_used(); + Ok(NativeResult::ok(cost, smallvec![])) +} + +#[derive(Clone, Debug)] +pub struct TransferFreezeObjectCostParams { + pub transfer_freeze_object_cost_base: InternalGas, +} +/*************************************************************************************************** +* native fun freeze_object +* Implementation of the Move native function `freeze_object(obj: T)` +* gas cost: transfer_freeze_object_cost_base | covers various fixed costs in the oper +**************************************************************************************************/ +pub fn freeze_object( + context: &mut NativeContext, + mut ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.len() == 1); + debug_assert!(args.len() == 1); + + let transfer_freeze_object_cost_params = get_extension!(context, NativesCostTable)? + .transfer_freeze_object_cost_params + .clone(); + + native_charge_gas_early_exit!( + context, + transfer_freeze_object_cost_params.transfer_freeze_object_cost_base + ); + + let ty = ty_args.pop().unwrap(); + let obj = args.pop_back().unwrap(); + + object_runtime_transfer(context, Owner::Immutable, ty, obj)?; + + Ok(NativeResult::ok(context.gas_used(), smallvec![])) +} + +#[derive(Clone, Debug)] +pub struct TransferShareObjectCostParams { + pub transfer_share_object_cost_base: InternalGas, +} +/*************************************************************************************************** +* native fun share_object +* Implementation of the Move native function `share_object(obj: T)` +* gas cost: transfer_share_object_cost_base | covers various fixed costs in the oper +**************************************************************************************************/ +pub fn share_object( + context: &mut NativeContext, + mut ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.len() == 1); + debug_assert!(args.len() == 1); + + let transfer_share_object_cost_params = get_extension!(context, NativesCostTable)? + .transfer_share_object_cost_params + .clone(); + + native_charge_gas_early_exit!( + context, + transfer_share_object_cost_params.transfer_share_object_cost_base + ); + + let ty = ty_args.pop().unwrap(); + let obj = args.pop_back().unwrap(); + let transfer_result = object_runtime_transfer( + context, + // Dummy version, to be filled with the correct initial version when the effects of the + // transaction are written to storage. + Owner::Shared { + initial_shared_version: SequenceNumber::new(), + }, + ty, + obj, + )?; + let cost = context.gas_used(); + Ok(match transfer_result { + // New means the ID was created in this transaction + // SameOwner means the object was previously shared and was re-shared + TransferResult::New | TransferResult::SameOwner => NativeResult::ok(cost, smallvec![]), + TransferResult::OwnerChanged => NativeResult::err(cost, E_SHARED_NON_NEW_OBJECT), + }) +} + +fn object_runtime_transfer( + context: &mut NativeContext, + owner: Owner, + ty: Type, + obj: Value, +) -> PartialVMResult { + let object_type = match context.type_to_type_tag(&ty)? { + TypeTag::Struct(s) => MoveObjectType::from(*s), + _ => { + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("Sui verifier guarantees this is a struct".to_string()), + ); + } + }; + + let obj_runtime: &mut ObjectRuntime = get_extension_mut!(context)?; + obj_runtime.transfer(owner, object_type, obj, /* end of transaction */ false) +} diff --git a/sui-execution/replay_cut/sui-move-natives/src/tx_context.rs b/sui-execution/replay_cut/sui-move-natives/src/tx_context.rs new file mode 100644 index 0000000000000..ae73e12836cee --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/tx_context.rs @@ -0,0 +1,466 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use move_binary_format::errors::PartialVMResult; +use move_core_types::{account_address::AccountAddress, gas_algebra::InternalGas}; +use move_vm_runtime::{native_charge_gas_early_exit, native_functions::NativeContext}; +use move_vm_types::{ + loaded_data::runtime_types::Type, natives::function::NativeResult, pop_arg, values::Value, +}; +use smallvec::smallvec; +use std::collections::VecDeque; +use sui_types::{base_types::ObjectID, digests::TransactionDigest}; + +use crate::{ + NativesCostTable, get_extension, get_extension_mut, object_runtime::ObjectRuntime, + transaction_context::TransactionContext, +}; + +#[derive(Clone)] +pub struct TxContextDeriveIdCostParams { + pub tx_context_derive_id_cost_base: InternalGas, +} +/*************************************************************************************************** + * native fun derive_id + * Implementation of the Move native function `fun derive_id(tx_hash: vector, ids_created: u64): address` + * gas cost: tx_context_derive_id_cost_base | we operate on fixed size data structures + **************************************************************************************************/ +pub fn derive_id( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 2); + + let tx_context_derive_id_cost_params = get_extension!(context, NativesCostTable)? + .tx_context_derive_id_cost_params + .clone(); + native_charge_gas_early_exit!( + context, + tx_context_derive_id_cost_params.tx_context_derive_id_cost_base + ); + + let ids_created = pop_arg!(args, u64); + let tx_hash = pop_arg!(args, Vec); + + // unwrap safe because all digests in Move are serialized from the Rust `TransactionDigest` + let digest = TransactionDigest::try_from(tx_hash.as_slice()).unwrap(); + let address = AccountAddress::from(ObjectID::derive_id(digest, ids_created)); + let obj_runtime: &mut ObjectRuntime = get_extension_mut!(context)?; + obj_runtime.new_id(address.into())?; + + Ok(NativeResult::ok( + context.gas_used(), + smallvec![Value::address(address)], + )) +} +#[derive(Clone)] +pub struct TxContextFreshIdCostParams { + pub tx_context_fresh_id_cost_base: InternalGas, +} +/*************************************************************************************************** + * native fun fresh_id + * Implementation of the Move native function `fun fresh_id(): address` + **************************************************************************************************/ +pub fn fresh_id( + context: &mut NativeContext, + ty_args: Vec, + args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.is_empty()); + + let tx_context_fresh_id_cost_params = get_extension!(context, NativesCostTable)? + .tx_context_fresh_id_cost_params + .clone(); + native_charge_gas_early_exit!( + context, + tx_context_fresh_id_cost_params.tx_context_fresh_id_cost_base + ); + + let transaction_context: &mut TransactionContext = get_extension_mut!(context)?; + let fresh_id = transaction_context.fresh_id(); + let object_runtime: &mut ObjectRuntime = get_extension_mut!(context)?; + object_runtime.new_id(fresh_id)?; + + Ok(NativeResult::ok( + context.gas_used(), + smallvec![Value::address(fresh_id.into())], + )) +} + +#[derive(Clone)] +pub struct TxContextSenderCostParams { + pub tx_context_sender_cost_base: InternalGas, +} +/*************************************************************************************************** + * native fun native_sender + * Implementation of the Move native function `fun native_sender(): address` + **************************************************************************************************/ +pub fn sender( + context: &mut NativeContext, + ty_args: Vec, + args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.is_empty()); + + let tx_context_sender_cost_params = get_extension!(context, NativesCostTable)? + .tx_context_sender_cost_params + .clone(); + native_charge_gas_early_exit!( + context, + tx_context_sender_cost_params.tx_context_sender_cost_base + ); + + let transaction_context: &mut TransactionContext = get_extension_mut!(context)?; + let sender = transaction_context.sender(); + + Ok(NativeResult::ok( + context.gas_used(), + smallvec![Value::address(sender.into())], + )) +} + +#[derive(Clone)] +pub struct TxContextEpochCostParams { + pub tx_context_epoch_cost_base: InternalGas, +} +/*************************************************************************************************** + * native fun native_epoch + * Implementation of the Move native function `fun native_epoch(): u64` + **************************************************************************************************/ +pub fn epoch( + context: &mut NativeContext, + ty_args: Vec, + args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.is_empty()); + + let tx_context_epoch_cost_params = get_extension!(context, NativesCostTable)? + .tx_context_epoch_cost_params + .clone(); + native_charge_gas_early_exit!( + context, + tx_context_epoch_cost_params.tx_context_epoch_cost_base + ); + + let transaction_context: &mut TransactionContext = get_extension_mut!(context)?; + let epoch = transaction_context.epoch(); + + Ok(NativeResult::ok( + context.gas_used(), + smallvec![Value::u64(epoch)], + )) +} + +#[derive(Clone)] +pub struct TxContextEpochTimestampMsCostParams { + pub tx_context_epoch_timestamp_ms_cost_base: InternalGas, +} +/*************************************************************************************************** + * native fun native_epoch_timestamp_ms + * Implementation of the Move native function `fun native_epoch_timestamp_ms(): u64` + **************************************************************************************************/ +pub fn epoch_timestamp_ms( + context: &mut NativeContext, + ty_args: Vec, + args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.is_empty()); + + let tx_context_epoch_timestamp_ms_cost_params = get_extension!(context, NativesCostTable)? + .tx_context_epoch_timestamp_ms_cost_params + .clone(); + native_charge_gas_early_exit!( + context, + tx_context_epoch_timestamp_ms_cost_params.tx_context_epoch_timestamp_ms_cost_base + ); + + let transaction_context: &mut TransactionContext = get_extension_mut!(context)?; + let timestamp = transaction_context.epoch_timestamp_ms(); + + Ok(NativeResult::ok( + context.gas_used(), + smallvec![Value::u64(timestamp)], + )) +} + +#[derive(Clone)] +pub struct TxContextSponsorCostParams { + pub tx_context_sponsor_cost_base: InternalGas, +} +/*************************************************************************************************** + * native fun native_sponsor + * Implementation of the Move native function `fun native_sponsor(): Option
` + **************************************************************************************************/ +pub fn sponsor( + context: &mut NativeContext, + ty_args: Vec, + args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.is_empty()); + + let tx_context_sponsor_cost_params = get_extension!(context, NativesCostTable)? + .tx_context_sponsor_cost_params + .clone(); + native_charge_gas_early_exit!( + context, + tx_context_sponsor_cost_params.tx_context_sponsor_cost_base + ); + + let transaction_context: &mut TransactionContext = get_extension_mut!(context)?; + let sponsor = transaction_context + .sponsor() + .map(|addr| addr.into()) + .into_iter(); + let sponsor = Value::vector_address(sponsor); + Ok(NativeResult::ok(context.gas_used(), smallvec![sponsor])) +} + +#[derive(Clone)] +pub struct TxContextRGPCostParams { + pub tx_context_rgp_cost_base: InternalGas, +} +/*************************************************************************************************** + * native fun native_rgp + * Implementation of the Move native function `fun native_rgp(): u64` + **************************************************************************************************/ +pub fn rgp( + context: &mut NativeContext, + ty_args: Vec, + args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.is_empty()); + + let tx_context_rgp_cost_params = get_extension!(context, NativesCostTable)? + .tx_context_rgp_cost_params + .clone(); + native_charge_gas_early_exit!(context, tx_context_rgp_cost_params.tx_context_rgp_cost_base); + + let transaction_context: &mut TransactionContext = get_extension_mut!(context)?; + let rgp = transaction_context.rgp(); + + Ok(NativeResult::ok( + context.gas_used(), + smallvec![Value::u64(rgp)], + )) +} +#[derive(Clone)] +pub struct TxContextGasPriceCostParams { + pub tx_context_gas_price_cost_base: InternalGas, +} +/*************************************************************************************************** + * native fun native_gas_price + * Implementation of the Move native function `fun native_gas_price(): u64` + **************************************************************************************************/ +pub fn gas_price( + context: &mut NativeContext, + ty_args: Vec, + args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.is_empty()); + + let tx_context_gas_price_cost_params = get_extension!(context, NativesCostTable)? + .tx_context_gas_price_cost_params + .clone(); + native_charge_gas_early_exit!( + context, + tx_context_gas_price_cost_params.tx_context_gas_price_cost_base + ); + + let transaction_context: &mut TransactionContext = get_extension_mut!(context)?; + let gas_price = transaction_context.gas_price(); + + Ok(NativeResult::ok( + context.gas_used(), + smallvec![Value::u64(gas_price)], + )) +} + +#[derive(Clone)] +pub struct TxContextGasBudgetCostParams { + pub tx_context_gas_budget_cost_base: InternalGas, +} +/*************************************************************************************************** + * native fun native_gas_budget + * Implementation of the Move native function `fun native_gas_budget(): u64` + **************************************************************************************************/ +pub fn gas_budget( + context: &mut NativeContext, + ty_args: Vec, + args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.is_empty()); + + let tx_context_gas_budget_cost_params = get_extension!(context, NativesCostTable)? + .tx_context_gas_budget_cost_params + .clone(); + native_charge_gas_early_exit!( + context, + tx_context_gas_budget_cost_params.tx_context_gas_budget_cost_base + ); + + let transaction_context: &mut TransactionContext = get_extension_mut!(context)?; + let gas_budget = transaction_context.gas_budget(); + + Ok(NativeResult::ok( + context.gas_used(), + smallvec![Value::u64(gas_budget)], + )) +} + +#[derive(Clone)] +pub struct TxContextIdsCreatedCostParams { + pub tx_context_ids_created_cost_base: InternalGas, +} +/*************************************************************************************************** + * native fun native_ids_created + * Implementation of the Move native function `fun native_ids_created(): u64` + **************************************************************************************************/ +pub fn ids_created( + context: &mut NativeContext, + ty_args: Vec, + args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.is_empty()); + + let tx_context_ids_created_cost_params = get_extension!(context, NativesCostTable)? + .tx_context_ids_created_cost_params + .clone(); + native_charge_gas_early_exit!( + context, + tx_context_ids_created_cost_params.tx_context_ids_created_cost_base + ); + + let transaction_context: &mut TransactionContext = get_extension_mut!(context)?; + let ids_created = transaction_context.ids_created(); + + Ok(NativeResult::ok( + context.gas_used(), + smallvec![Value::u64(ids_created)], + )) +} + +// // +// // Test only function +// // +#[derive(Clone)] +pub struct TxContextReplaceCostParams { + pub tx_context_replace_cost_base: InternalGas, +} +/*************************************************************************************************** + * native fun replace + * Implementation of the Move native function + * ``` + * native fun replace( + * sender: address, + * tx_hash: vector, + * epoch: u64, + * epoch_timestamp_ms: u64, + * ids_created: u64, + * rgp: u64, + * gas_price: u64, + * gas_budget: u64, + * sponsor: vector
, + * ) + * ``` + * Used by all testing functions that have to change a value in the `TransactionContext`. + **************************************************************************************************/ +pub fn replace( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + let args_len = args.len(); + debug_assert!(args_len == 8 || args_len == 9); + + // use the `TxContextReplaceCostParams` for the cost of this function + let tx_context_replace_cost_params: TxContextReplaceCostParams = + get_extension!(context, NativesCostTable)? + .tx_context_replace_cost_params + .clone(); + native_charge_gas_early_exit!( + context, + tx_context_replace_cost_params.tx_context_replace_cost_base + ); + + let transaction_context: &mut TransactionContext = get_extension_mut!(context)?; + let mut sponsor: Vec = pop_arg!(args, Vec); + let gas_budget: u64 = pop_arg!(args, u64); + let gas_price: u64 = pop_arg!(args, u64); + let rgp: u64 = if args_len == 9 { + pop_arg!(args, u64) + } else { + transaction_context.rgp() + }; + let ids_created: u64 = pop_arg!(args, u64); + let epoch_timestamp_ms: u64 = pop_arg!(args, u64); + let epoch: u64 = pop_arg!(args, u64); + let tx_hash: Vec = pop_arg!(args, Vec); + let sender: AccountAddress = pop_arg!(args, AccountAddress); + transaction_context.replace( + sender, + tx_hash, + epoch, + epoch_timestamp_ms, + ids_created, + rgp, + gas_price, + gas_budget, + sponsor.pop(), + )?; + + Ok(NativeResult::ok(context.gas_used(), smallvec![])) +} + +// Attempt to get the most recent created object ID when none has been created. +// Lifted out of Move into this native function. +const E_NO_IDS_CREATED: u64 = 1; + +// use same protocol config and cost value as derive_id +/*************************************************************************************************** + * native fun last_created_id + * Implementation of the Move native function `fun last_created_id(): address` + **************************************************************************************************/ +pub fn last_created_id( + context: &mut NativeContext, + ty_args: Vec, + args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.is_empty()); + + let tx_context_derive_id_cost_params = get_extension!(context, NativesCostTable)? + .tx_context_derive_id_cost_params + .clone(); + native_charge_gas_early_exit!( + context, + tx_context_derive_id_cost_params.tx_context_derive_id_cost_base + ); + + let transaction_context: &mut TransactionContext = get_extension_mut!(context)?; + let mut ids_created = transaction_context.ids_created(); + if ids_created == 0 { + return Ok(NativeResult::err(context.gas_used(), E_NO_IDS_CREATED)); + } + ids_created -= 1; + let digest = transaction_context.digest(); + let address = AccountAddress::from(ObjectID::derive_id(digest, ids_created)); + let obj_runtime: &mut ObjectRuntime = get_extension_mut!(context)?; + obj_runtime.new_id(address.into())?; + + Ok(NativeResult::ok( + context.gas_used(), + smallvec![Value::address(address)], + )) +} diff --git a/sui-execution/replay_cut/sui-move-natives/src/types.rs b/sui-execution/replay_cut/sui-move-natives/src/types.rs new file mode 100644 index 0000000000000..4e5fe4dc479e3 --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/types.rs @@ -0,0 +1,98 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use move_binary_format::errors::PartialVMResult; +use move_core_types::{ + gas_algebra::InternalGas, + language_storage::TypeTag, + runtime_value::{MoveStructLayout, MoveTypeLayout}, +}; +use move_vm_runtime::{native_charge_gas_early_exit, native_functions::NativeContext}; +use move_vm_types::{ + loaded_data::runtime_types::Type, natives::function::NativeResult, values::Value, +}; +use smallvec::smallvec; +use std::collections::VecDeque; + +use crate::{NativesCostTable, get_extension}; + +pub(crate) fn is_otw_struct( + struct_layout: &MoveStructLayout, + type_tag: &TypeTag, + hardened_check: bool, +) -> bool { + let has_one_bool_field = matches!(struct_layout.0.as_slice(), [MoveTypeLayout::Bool]); + + // If a struct type has the same name as the module that defines it but capitalized, and it has + // a single field of type bool, it means that it's a one-time witness type. The remaining + // properties of a one-time witness type are checked in the one_time_witness_verifier pass in + // the Sui bytecode verifier (a type with this name and with a single bool field that does not + // have all the remaining properties of a one-time witness type will cause a verifier error). + matches!( + type_tag, + TypeTag::Struct(struct_tag) if + has_one_bool_field && + struct_tag.name.to_string() == struct_tag.module.to_string().to_ascii_uppercase() && + // hardened check ==> no generic types + (!hardened_check || struct_tag.type_params.is_empty()) + ) +} + +#[derive(Clone)] +pub struct TypesIsOneTimeWitnessCostParams { + pub types_is_one_time_witness_cost_base: InternalGas, + pub types_is_one_time_witness_type_tag_cost_per_byte: InternalGas, + pub types_is_one_time_witness_type_cost_per_byte: InternalGas, +} +/*************************************************************************************************** + * native fun is_one_time_witness + * Implementation of the Move native function `is_one_time_witness(_: &T): bool` + * gas cost: types_is_one_time_witness_cost_base | base cost as this can be expensive oper + * + types_is_one_time_witness_type_tag_cost_per_byte * type_tag.size() | cost per byte of converting type to type tag + * + types_is_one_time_witness_type_cost_per_byte * ty.size() | cost per byte of converting type to type layout + **************************************************************************************************/ +pub fn is_one_time_witness( + context: &mut NativeContext, + mut ty_args: Vec, + args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.len() == 1); + debug_assert!(args.len() == 1); + + let type_is_one_time_witness_cost_params = get_extension!(context, NativesCostTable)? + .type_is_one_time_witness_cost_params + .clone(); + + native_charge_gas_early_exit!( + context, + type_is_one_time_witness_cost_params.types_is_one_time_witness_cost_base + ); + + // unwrap safe because the interface of native function guarantees it. + let ty = ty_args.pop().unwrap(); + + native_charge_gas_early_exit!( + context, + type_is_one_time_witness_cost_params.types_is_one_time_witness_type_cost_per_byte + * u64::from(ty.size()).into() + ); + + let type_tag = context.type_to_type_tag(&ty)?; + native_charge_gas_early_exit!( + context, + type_is_one_time_witness_cost_params.types_is_one_time_witness_type_tag_cost_per_byte + * u64::from(type_tag.abstract_size_for_gas_metering()).into() + ); + + let type_layout = context.type_to_type_layout(&ty)?; + + let cost = context.gas_used(); + let Some(MoveTypeLayout::Struct(struct_layout)) = type_layout else { + return Ok(NativeResult::ok(cost, smallvec![Value::bool(false)])); + }; + + let hardened_check = context.runtime_limits_config().hardened_otw_check; + let is_otw = is_otw_struct(&struct_layout, &type_tag, hardened_check); + + Ok(NativeResult::ok(cost, smallvec![Value::bool(is_otw)])) +} diff --git a/sui-execution/replay_cut/sui-move-natives/src/validator.rs b/sui-execution/replay_cut/sui-move-natives/src/validator.rs new file mode 100644 index 0000000000000..baf039c02251b --- /dev/null +++ b/sui-execution/replay_cut/sui-move-natives/src/validator.rs @@ -0,0 +1,66 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::{NativesCostTable, get_extension}; +use move_binary_format::errors::{PartialVMError, PartialVMResult}; +use move_core_types::{gas_algebra::InternalGas, vm_status::StatusCode}; +use move_vm_runtime::{native_charge_gas_early_exit, native_functions::NativeContext}; +use move_vm_types::{ + loaded_data::runtime_types::Type, natives::function::NativeResult, pop_arg, values::Value, +}; +use smallvec::smallvec; +use std::collections::VecDeque; +use sui_types::sui_system_state::sui_system_state_inner_v1::ValidatorMetadataV1; + +#[derive(Clone, Debug)] +pub struct ValidatorValidateMetadataBcsCostParams { + pub validator_validate_metadata_cost_base: InternalGas, + pub validator_validate_metadata_data_cost_per_byte: InternalGas, +} +/*************************************************************************************************** + * native fun validate_metadata_bcs + * Implementation of the Move native function `validate_metadata_bcs(metadata: vector)` + * gas cost: validator_validate_metadata_cost_base | fixed cosrs + * + validator_validate_metadata_data_cost_per_byte * metadata_bytes.len() | assume cost is proportional to size + **************************************************************************************************/ +pub fn validate_metadata_bcs( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 1); + + let validator_validate_metadata_bcs_cost_params = get_extension!(context, NativesCostTable)? + .validator_validate_metadata_bcs_cost_params + .clone(); + + native_charge_gas_early_exit!( + context, + validator_validate_metadata_bcs_cost_params.validator_validate_metadata_cost_base + ); + + let metadata_bytes = pop_arg!(args, Vec); + + native_charge_gas_early_exit!( + context, + validator_validate_metadata_bcs_cost_params.validator_validate_metadata_data_cost_per_byte + * (metadata_bytes.len() as u64).into() + ); + + let validator_metadata = + bcs::from_bytes::(&metadata_bytes).map_err(|_| { + PartialVMError::new(StatusCode::MALFORMED).with_message( + "ValidateMetadata Move struct does not match internal ValidateMetadata struct" + .to_string(), + ) + })?; + + let cost = context.gas_used(); + + if let Result::Err(err_code) = validator_metadata.verify() { + return Ok(NativeResult::err(cost, err_code)); + } + + Ok(NativeResult::ok(cost, smallvec![])) +} diff --git a/sui-execution/replay_cut/sui-verifier/Cargo.toml b/sui-execution/replay_cut/sui-verifier/Cargo.toml new file mode 100644 index 0000000000000..94abf49dcff8b --- /dev/null +++ b/sui-execution/replay_cut/sui-verifier/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "sui-verifier-replay_cut" +version = "0.1.0" +edition = "2024" +authors = ["Mysten Labs "] +description = "Move framework for Sui platform" +license = "Apache-2.0" +publish = false + +[dependencies] +move-binary-format.workspace = true +move-bytecode-utils.workspace = true +move-bytecode-verifier-meter.workspace = true +move-core-types.workspace = true +move-vm-config.workspace = true +move-abstract-stack.workspace = true + +move-bytecode-verifier = { path = "../../../external-crates/move/move-execution/replay_cut/crates/move-bytecode-verifier", package = "move-bytecode-verifier-replay_cut" } + +sui-types.workspace = true +sui-protocol-config.workspace = true diff --git a/sui-execution/replay_cut/sui-verifier/src/entry_points_verifier.rs b/sui-execution/replay_cut/sui-verifier/src/entry_points_verifier.rs new file mode 100644 index 0000000000000..ee8981ce82916 --- /dev/null +++ b/sui-execution/replay_cut/sui-verifier/src/entry_points_verifier.rs @@ -0,0 +1,260 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use move_binary_format::{ + CompiledModule, + file_format::{AbilitySet, Bytecode, FunctionDefinition, SignatureToken, Visibility}, +}; +use move_bytecode_utils::format_signature_token; +use move_vm_config::verifier::VerifierConfig; +use sui_types::randomness_state::is_mutable_random; +use sui_types::{ + SUI_FRAMEWORK_ADDRESS, + base_types::{TX_CONTEXT_MODULE_NAME, TX_CONTEXT_STRUCT_NAME, TxContext, TxContextKind}, + clock::Clock, + error::ExecutionError, + is_object, is_object_vector, is_primitive, + move_package::{FnInfoMap, is_test_fun}, + transfer::Receiving, +}; + +use crate::{INIT_FN_NAME, verification_failure}; + +/// Checks valid rules for entry points, both for module initialization and transactions +/// +/// For module initialization +/// - The existence of the function is optional +/// - The function must have the name specified by `INIT_FN_NAME` +/// - The function must have `Visibility::Private` +/// - The function can have at most two parameters: +/// - mandatory &mut TxContext or &TxContext (see `is_tx_context`) in the last position +/// - optional one-time witness type (see one_time_witness verifier pass) passed by value in the +/// first position +/// +/// For transaction entry points +/// - The function must have `is_entry` true +/// - The function may have a &mut TxContext or &TxContext (see `is_tx_context`) parameter +/// - The transaction context parameter must be the last parameter +/// - The function cannot have any return values +pub fn verify_module( + module: &CompiledModule, + fn_info_map: &FnInfoMap, + verifier_config: &VerifierConfig, +) -> Result<(), ExecutionError> { + // When verifying test functions, a check preventing explicit calls to init functions is + // disabled. + + for func_def in &module.function_defs { + let handle = module.function_handle_at(func_def.function); + let name = module.identifier_at(handle.name); + + // allow calling init function in the test code + if !is_test_fun(name, module, fn_info_map) { + verify_init_not_called(module, func_def).map_err(verification_failure)?; + } + + if name == INIT_FN_NAME { + verify_init_function(module, func_def).map_err(verification_failure)?; + continue; + } + + // find candidate entry functions and check their parameters + // (ignore other functions) + if !func_def.is_entry { + // it's not an entry function + continue; + } + verify_entry_function_impl(module, func_def, verifier_config) + .map_err(verification_failure)?; + } + Ok(()) +} + +fn verify_init_not_called( + module: &CompiledModule, + fdef: &FunctionDefinition, +) -> Result<(), String> { + let code = match &fdef.code { + None => return Ok(()), + Some(code) => code, + }; + code.code + .iter() + .enumerate() + .filter_map(|(idx, instr)| match instr { + Bytecode::Call(fhandle_idx) => Some((idx, module.function_handle_at(*fhandle_idx))), + Bytecode::CallGeneric(finst_idx) => { + let finst = module.function_instantiation_at(*finst_idx); + Some((idx, module.function_handle_at(finst.handle))) + } + _ => None, + }) + .try_for_each(|(idx, fhandle)| { + let name = module.identifier_at(fhandle.name); + if name == INIT_FN_NAME { + Err(format!( + "{}::{} at offset {}. Cannot call a module's '{}' function from another Move function", + module.self_id(), + name, + idx, + INIT_FN_NAME + )) + } else { + Ok(()) + } + }) +} + +/// Checks if this module has a conformant `init` +fn verify_init_function(module: &CompiledModule, fdef: &FunctionDefinition) -> Result<(), String> { + if fdef.visibility != Visibility::Private { + return Err(format!( + "{}. '{}' function must be private", + module.self_id(), + INIT_FN_NAME + )); + } + + if fdef.is_entry { + return Err(format!( + "{}. '{}' cannot be 'entry'", + module.self_id(), + INIT_FN_NAME + )); + } + + let fhandle = module.function_handle_at(fdef.function); + if !fhandle.type_parameters.is_empty() { + return Err(format!( + "{}. '{}' function cannot have type parameters", + module.self_id(), + INIT_FN_NAME + )); + } + + if !module.signature_at(fhandle.return_).is_empty() { + return Err(format!( + "{}, '{}' function cannot have return values", + module.self_id(), + INIT_FN_NAME + )); + } + + let parameters = &module.signature_at(fhandle.parameters).0; + if parameters.is_empty() || parameters.len() > 2 { + return Err(format!( + "Expected at least one and at most two parameters for {}::{}", + module.self_id(), + INIT_FN_NAME, + )); + } + + // Checking only the last (and possibly the only) parameter here. If there are two parameters, + // then the first parameter must be of a one-time witness type and must be passed by value. This + // is checked by the verifier for pass one-time witness value (one_time_witness_verifier) - + // please see the description of this pass for additional details. + if TxContext::kind(module, ¶meters[parameters.len() - 1]) != TxContextKind::None { + Ok(()) + } else { + Err(format!( + "Expected last parameter for {0}::{1} to be &mut {2}::{3}::{4} or &{2}::{3}::{4}, \ + but found {5}", + module.self_id(), + INIT_FN_NAME, + SUI_FRAMEWORK_ADDRESS, + TX_CONTEXT_MODULE_NAME, + TX_CONTEXT_STRUCT_NAME, + format_signature_token(module, ¶meters[0]), + )) + } +} + +fn verify_entry_function_impl( + view: &CompiledModule, + func_def: &FunctionDefinition, + verifier_config: &VerifierConfig, +) -> Result<(), String> { + let handle = view.function_handle_at(func_def.function); + let params = view.signature_at(handle.parameters); + + let all_non_ctx_params = match params.0.last() { + Some(last_param) if TxContext::kind(view, last_param) != TxContextKind::None => { + ¶ms.0[0..params.0.len() - 1] + } + _ => ¶ms.0, + }; + for param in all_non_ctx_params { + verify_param_type(view, &handle.type_parameters, param, verifier_config)?; + } + + for return_ty in &view.signature_at(handle.return_).0 { + verify_return_type(view, &handle.type_parameters, return_ty)?; + } + + Ok(()) +} + +fn verify_return_type( + view: &CompiledModule, + type_parameters: &[AbilitySet], + return_ty: &SignatureToken, +) -> Result<(), String> { + if matches!( + return_ty, + SignatureToken::Reference(_) | SignatureToken::MutableReference(_) + ) { + return Err("Invalid entry point return type. Expected a non reference type.".to_owned()); + } + let abilities = view + .abilities(return_ty, type_parameters) + .map_err(|e| format!("Unexpected CompiledModule error: {}", e))?; + if abilities.has_drop() { + Ok(()) + } else { + Err(format!( + "Invalid entry point return type. \ + The specified return type does not have the 'drop' ability: {}", + format_signature_token(view, return_ty), + )) + } +} + +fn verify_param_type( + view: &CompiledModule, + function_type_args: &[AbilitySet], + param: &SignatureToken, + verifier_config: &VerifierConfig, +) -> Result<(), String> { + // Only `sui::sui_system` is allowed to expose entry functions that accept a mutable clock + // parameter. + if Clock::is_mutable(view, param) { + return Err(format!( + "Invalid entry point parameter type. Clock must be passed by immutable reference. got: \ + {}", + format_signature_token(view, param), + )); + } + + // Only `sui::sui_system` is allowed to expose entry functions that accept a mutable Random + // parameter. + if verifier_config.reject_mutable_random_on_entry_functions && is_mutable_random(view, param) { + return Err(format!( + "Invalid entry point parameter type. Random must be passed by immutable reference. got: \ + {}", + format_signature_token(view, param), + )); + } + + if is_primitive(view, function_type_args, param) + || is_object(view, function_type_args, param)? + || is_object_vector(view, function_type_args, param)? + || Receiving::is_receiving(view, param) + { + Ok(()) + } else { + Err(format!( + "Invalid entry point parameter type. Expected primitive or object type. Got: {}", + format_signature_token(view, param) + )) + } +} diff --git a/sui-execution/replay_cut/sui-verifier/src/global_storage_access_verifier.rs b/sui-execution/replay_cut/sui-verifier/src/global_storage_access_verifier.rs new file mode 100644 index 0000000000000..df7fb7c578e5a --- /dev/null +++ b/sui-execution/replay_cut/sui-verifier/src/global_storage_access_verifier.rs @@ -0,0 +1,122 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::verification_failure; +use move_binary_format::file_format::{Bytecode, CompiledModule}; +use sui_types::error::ExecutionError; + +pub fn verify_module(module: &CompiledModule) -> Result<(), ExecutionError> { + verify_global_storage_access(module) +} + +/// Global storage in sui is handled by sui instead of within Move. +/// Hence we want to forbid any global storage access in Move. +fn verify_global_storage_access(module: &CompiledModule) -> Result<(), ExecutionError> { + for func_def in &module.function_defs { + if func_def.code.is_none() { + continue; + } + let code = &func_def.code.as_ref().unwrap().code; + let mut invalid_bytecode = vec![]; + for bytecode in code { + match bytecode { + Bytecode::MoveFromDeprecated(_) + | Bytecode::MoveFromGenericDeprecated(_) + | Bytecode::MoveToDeprecated(_) + | Bytecode::MoveToGenericDeprecated(_) + | Bytecode::ImmBorrowGlobalDeprecated(_) + | Bytecode::MutBorrowGlobalDeprecated(_) + | Bytecode::ImmBorrowGlobalGenericDeprecated(_) + | Bytecode::MutBorrowGlobalGenericDeprecated(_) + | Bytecode::ExistsDeprecated(_) + | Bytecode::ExistsGenericDeprecated(_) => { + invalid_bytecode.push(bytecode); + } + Bytecode::Pop + | Bytecode::Ret + | Bytecode::BrTrue(_) + | Bytecode::BrFalse(_) + | Bytecode::Branch(_) + | Bytecode::LdU8(_) + | Bytecode::LdU16(_) + | Bytecode::LdU32(_) + | Bytecode::LdU64(_) + | Bytecode::LdU128(_) + | Bytecode::LdU256(_) + | Bytecode::CastU8 + | Bytecode::CastU16 + | Bytecode::CastU32 + | Bytecode::CastU64 + | Bytecode::CastU128 + | Bytecode::CastU256 + | Bytecode::LdConst(_) + | Bytecode::LdTrue + | Bytecode::LdFalse + | Bytecode::CopyLoc(_) + | Bytecode::MoveLoc(_) + | Bytecode::StLoc(_) + | Bytecode::Call(_) + | Bytecode::CallGeneric(_) + | Bytecode::Pack(_) + | Bytecode::PackGeneric(_) + | Bytecode::Unpack(_) + | Bytecode::UnpackGeneric(_) + | Bytecode::ReadRef + | Bytecode::WriteRef + | Bytecode::FreezeRef + | Bytecode::MutBorrowLoc(_) + | Bytecode::ImmBorrowLoc(_) + | Bytecode::MutBorrowField(_) + | Bytecode::MutBorrowFieldGeneric(_) + | Bytecode::ImmBorrowField(_) + | Bytecode::ImmBorrowFieldGeneric(_) + | Bytecode::Add + | Bytecode::Sub + | Bytecode::Mul + | Bytecode::Mod + | Bytecode::Div + | Bytecode::BitOr + | Bytecode::BitAnd + | Bytecode::Xor + | Bytecode::Shl + | Bytecode::Shr + | Bytecode::Or + | Bytecode::And + | Bytecode::Not + | Bytecode::Eq + | Bytecode::Neq + | Bytecode::Lt + | Bytecode::Gt + | Bytecode::Le + | Bytecode::Ge + | Bytecode::Abort + | Bytecode::Nop + | Bytecode::VecPack(_, _) + | Bytecode::VecLen(_) + | Bytecode::VecImmBorrow(_) + | Bytecode::VecMutBorrow(_) + | Bytecode::VecPushBack(_) + | Bytecode::VecPopBack(_) + | Bytecode::VecUnpack(_, _) + | Bytecode::VecSwap(_) + | Bytecode::PackVariant(_) + | Bytecode::PackVariantGeneric(_) + | Bytecode::UnpackVariant(_) + | Bytecode::UnpackVariantImmRef(_) + | Bytecode::UnpackVariantMutRef(_) + | Bytecode::UnpackVariantGeneric(_) + | Bytecode::UnpackVariantGenericImmRef(_) + | Bytecode::UnpackVariantGenericMutRef(_) + | Bytecode::VariantSwitch(_) => {} + } + } + if !invalid_bytecode.is_empty() { + return Err(verification_failure(format!( + "Access to Move global storage is not allowed. Found in function {}: {:?}", + module.identifier_at(module.function_handle_at(func_def.function).name), + invalid_bytecode, + ))); + } + } + Ok(()) +} diff --git a/sui-execution/replay_cut/sui-verifier/src/id_leak_verifier.rs b/sui-execution/replay_cut/sui-verifier/src/id_leak_verifier.rs new file mode 100644 index 0000000000000..2373ecc51f0ab --- /dev/null +++ b/sui-execution/replay_cut/sui-verifier/src/id_leak_verifier.rs @@ -0,0 +1,596 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//! Objects whose struct type has key ability represent Sui objects. +//! They have unique IDs that should never be reused. This verifier makes +//! sure that the id field of Sui objects never get leaked. +//! Unpack is the only bytecode that could extract the id field out of +//! a Sui object. From there, we track the flow of the value and make +//! sure it can never get leaked outside of the function. There are four +//! ways it can happen: +//! 1. Returned +//! 2. Written into a mutable reference +//! 3. Added to a vector +//! 4. Passed to a function cal::; +use move_abstract_stack::AbstractStack; +use move_binary_format::{ + errors::PartialVMError, + file_format::{ + Bytecode, CodeOffset, CompiledModule, FunctionDefinitionIndex, FunctionHandle, LocalIndex, + StructDefinition, StructFieldInformation, + }, +}; +use move_bytecode_verifier::absint::{ + AbstractDomain, FunctionContext, JoinResult, TransferFunctions, analyze_function, +}; +use move_bytecode_verifier_meter::{Meter, Scope}; +use move_core_types::{ident_str, vm_status::StatusCode}; +use std::{collections::BTreeMap, error::Error, num::NonZeroU64}; +use sui_types::bridge::BRIDGE_MODULE_NAME; +use sui_types::deny_list_v1::{DENY_LIST_CREATE_FUNC, DENY_LIST_MODULE}; +use sui_types::{ + BRIDGE_ADDRESS, SUI_FRAMEWORK_ADDRESS, SUI_SYSTEM_ADDRESS, + accumulator_event::ACCUMULATOR_MODULE_NAME, + authenticator_state::AUTHENTICATOR_STATE_MODULE_NAME, + clock::CLOCK_MODULE_NAME, + error::{ExecutionError, VMMVerifierErrorSubStatusCode}, + id::OBJECT_MODULE_NAME, + randomness_state::RANDOMNESS_MODULE_NAME, + sui_system_state::SUI_SYSTEM_MODULE_NAME, +}; + +use crate::{ + FunctionIdent, TEST_SCENARIO_MODULE_NAME, check_for_verifier_timeout, + to_verification_timeout_error, verification_failure, +}; +pub(crate) const JOIN_BASE_COST: u128 = 10; +pub(crate) const JOIN_PER_LOCAL_COST: u128 = 5; +pub(crate) const STEP_BASE_COST: u128 = 15; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum AbstractValue { + Fresh, + Other, +} + +const OBJECT_NEW: FunctionIdent = (SUI_FRAMEWORK_ADDRESS, OBJECT_MODULE_NAME, ident_str!("new")); +const OBJECT_NEW_UID_FROM_HASH: FunctionIdent = ( + SUI_FRAMEWORK_ADDRESS, + OBJECT_MODULE_NAME, + ident_str!("new_uid_from_hash"), +); +const OBJECT_NEW_DERIVED: FunctionIdent = ( + SUI_FRAMEWORK_ADDRESS, + ident_str!("derived_object"), + ident_str!("claim"), +); +const TS_NEW_OBJECT: FunctionIdent = ( + SUI_FRAMEWORK_ADDRESS, + ident_str!(TEST_SCENARIO_MODULE_NAME), + ident_str!("new_object"), +); +const SUI_SYSTEM_CREATE: FunctionIdent = ( + SUI_SYSTEM_ADDRESS, + SUI_SYSTEM_MODULE_NAME, + ident_str!("create"), +); +const SUI_CLOCK_CREATE: FunctionIdent = ( + SUI_FRAMEWORK_ADDRESS, + CLOCK_MODULE_NAME, + ident_str!("create"), +); +const SUI_AUTHENTICATOR_STATE_CREATE: FunctionIdent = ( + SUI_FRAMEWORK_ADDRESS, + AUTHENTICATOR_STATE_MODULE_NAME, + ident_str!("create"), +); +const SUI_RANDOMNESS_STATE_CREATE: FunctionIdent = ( + SUI_FRAMEWORK_ADDRESS, + RANDOMNESS_MODULE_NAME, + ident_str!("create"), +); +const SUI_DENY_LIST_CREATE: FunctionIdent = ( + SUI_FRAMEWORK_ADDRESS, + DENY_LIST_MODULE, + DENY_LIST_CREATE_FUNC, +); + +const SUI_BRIDGE_CREATE: FunctionIdent = (BRIDGE_ADDRESS, BRIDGE_MODULE_NAME, ident_str!("create")); +const SUI_ACCUMULATOR_CREATE: FunctionIdent = ( + SUI_FRAMEWORK_ADDRESS, + ACCUMULATOR_MODULE_NAME, + ident_str!("create"), +); +const SUI_COIN_REGISTRY_CREATE: FunctionIdent = ( + SUI_FRAMEWORK_ADDRESS, + ident_str!("coin_registry"), + ident_str!("create"), +); +const FRESH_ID_FUNCTIONS: &[FunctionIdent] = &[ + OBJECT_NEW, + OBJECT_NEW_UID_FROM_HASH, + OBJECT_NEW_DERIVED, + TS_NEW_OBJECT, +]; +const FUNCTIONS_TO_SKIP: &[FunctionIdent] = &[ + SUI_SYSTEM_CREATE, + SUI_CLOCK_CREATE, + SUI_AUTHENTICATOR_STATE_CREATE, + SUI_RANDOMNESS_STATE_CREATE, + SUI_DENY_LIST_CREATE, + SUI_BRIDGE_CREATE, + SUI_ACCUMULATOR_CREATE, + SUI_COIN_REGISTRY_CREATE, +]; + +impl AbstractValue { + pub fn join(&self, value: &AbstractValue) -> AbstractValue { + if self == value { + *value + } else { + AbstractValue::Other + } + } +} + +pub fn verify_module( + module: &CompiledModule, + meter: &mut (impl Meter + ?Sized), +) -> Result<(), ExecutionError> { + verify_id_leak(module, meter) +} + +fn verify_id_leak( + module: &CompiledModule, + meter: &mut (impl Meter + ?Sized), +) -> Result<(), ExecutionError> { + for (index, func_def) in module.function_defs.iter().enumerate() { + let code = match func_def.code.as_ref() { + Some(code) => code, + None => continue, + }; + let handle = module.function_handle_at(func_def.function); + let function_context = + FunctionContext::new(module, FunctionDefinitionIndex(index as u16), code, handle); + let initial_state = AbstractState::new(&function_context); + let mut verifier = IDLeakAnalysis::new(module, &function_context); + let function_to_verify = verifier.cur_function(); + if FUNCTIONS_TO_SKIP.contains(&function_to_verify) { + continue; + } + analyze_function(&function_context, meter, &mut verifier, initial_state).map_err( + |err| { + // Handle verification timeout specially + if check_for_verifier_timeout(&err.major_status()) { + to_verification_timeout_error(err.to_string()) + } else if let Some(message) = err.source().as_ref() { + let function_name = + module.identifier_at(module.function_handle_at(func_def.function).name); + let module_name = module.self_id(); + verification_failure(format!( + "{} Found in {module_name}::{function_name}", + message + )) + } else { + verification_failure(err.to_string()) + } + }, + )?; + } + + Ok(()) +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct AbstractState { + locals: BTreeMap, +} + +impl AbstractState { + /// create a new abstract state + pub fn new(function_context: &FunctionContext) -> Self { + let mut state = AbstractState { + locals: BTreeMap::new(), + }; + + for param_idx in 0..function_context.parameters().len() { + state + .locals + .insert(param_idx as LocalIndex, AbstractValue::Other); + } + + state + } +} + +impl AbstractDomain for AbstractState { + /// attempts to join state to self and returns the result + fn join( + &mut self, + state: &AbstractState, + meter: &mut (impl Meter + ?Sized), + ) -> Result { + meter.add(Scope::Function, JOIN_BASE_COST)?; + meter.add_items(Scope::Function, JOIN_PER_LOCAL_COST, state.locals.len())?; + let mut changed = false; + for (local, value) in &state.locals { + let old_value = *self.locals.get(local).unwrap_or(&AbstractValue::Other); + let new_value = value.join(&old_value); + changed |= new_value != old_value; + self.locals.insert(*local, new_value); + } + if changed { + Ok(JoinResult::Changed) + } else { + Ok(JoinResult::Unchanged) + } + } +} + +struct IDLeakAnalysis<'a> { + binary_view: &'a CompiledModule, + function_context: &'a FunctionContext<'a>, + stack: AbstractStack, +} + +impl<'a> IDLeakAnalysis<'a> { + fn new(binary_view: &'a CompiledModule, function_context: &'a FunctionContext<'a>) -> Self { + Self { + binary_view, + function_context, + stack: AbstractStack::new(), + } + } + + fn stack_popn(&mut self, n: u64) -> Result<(), PartialVMError> { + let Some(n) = NonZeroU64::new(n) else { + return Ok(()); + }; + self.stack.pop_any_n(n).map_err(|e| { + PartialVMError::new(StatusCode::VERIFIER_INVARIANT_VIOLATION) + .with_message(format!("Unexpected stack error on pop_n: {e}")) + }) + } + + fn stack_push(&mut self, val: AbstractValue) -> Result<(), PartialVMError> { + self.stack.push(val).map_err(|e| { + PartialVMError::new(StatusCode::VERIFIER_INVARIANT_VIOLATION) + .with_message(format!("Unexpected stack error on push: {e}")) + }) + } + + fn stack_pushn(&mut self, n: u64, val: AbstractValue) -> Result<(), PartialVMError> { + self.stack.push_n(val, n).map_err(|e| { + PartialVMError::new(StatusCode::VERIFIER_INVARIANT_VIOLATION) + .with_message(format!("Unexpected stack error on push_n: {e}")) + }) + } + + fn resolve_function(&self, function_handle: &FunctionHandle) -> FunctionIdent<'a> { + let m = self.binary_view.module_handle_at(function_handle.module); + let address = *self.binary_view.address_identifier_at(m.address); + let module = self.binary_view.identifier_at(m.name); + let function = self.binary_view.identifier_at(function_handle.name); + (address, module, function) + } + + fn cur_function(&self) -> FunctionIdent<'a> { + let fdef = self + .binary_view + .function_def_at(self.function_context.index().unwrap()); + let handle = self.binary_view.function_handle_at(fdef.function); + self.resolve_function(handle) + } +} + +impl TransferFunctions for IDLeakAnalysis<'_> { + type State = AbstractState; + + fn execute( + &mut self, + state: &mut Self::State, + bytecode: &Bytecode, + index: CodeOffset, + (_first_index, last_index): (u16, u16), + meter: &mut (impl Meter + ?Sized), + ) -> Result<(), PartialVMError> { + execute_inner(self, state, bytecode, index, meter)?; + // invariant: the stack should be empty at the end of the block + // If it is not, something is wrong with the implementation, so throw an invariant + // violation + if index == last_index && !self.stack.is_empty() { + let msg = "Invalid stack transitions. Non-zero stack size at the end of the block" + .to_string(); + debug_assert!(false, "{msg}",); + return Err( + PartialVMError::new(StatusCode::VERIFIER_INVARIANT_VIOLATION).with_message(msg), + ); + } + Ok(()) + } +} + +fn call( + verifier: &mut IDLeakAnalysis, + function_handle: &FunctionHandle, +) -> Result<(), PartialVMError> { + let parameters = verifier + .binary_view + .signature_at(function_handle.parameters); + verifier.stack_popn(parameters.len() as u64)?; + + let return_ = verifier.binary_view.signature_at(function_handle.return_); + let function = verifier.resolve_function(function_handle); + if FRESH_ID_FUNCTIONS.contains(&function) { + if return_.0.len() != 1 { + debug_assert!(false, "{:?} should have a single return value", function); + return Err(PartialVMError::new(StatusCode::UNKNOWN_VERIFICATION_ERROR) + .with_message("Should have a single return value".to_string()) + .with_sub_status( + VMMVerifierErrorSubStatusCode::MULTIPLE_RETURN_VALUES_NOT_ALLOWED as u64, + )); + } + verifier.stack_push(AbstractValue::Fresh)?; + } else { + verifier.stack_pushn(return_.0.len() as u64, AbstractValue::Other)?; + } + Ok(()) +} + +fn num_fields(struct_def: &StructDefinition) -> u64 { + match &struct_def.field_information { + StructFieldInformation::Native => 0, + StructFieldInformation::Declared(fields) => fields.len() as u64, + } +} + +fn pack( + verifier: &mut IDLeakAnalysis, + struct_def: &StructDefinition, +) -> Result<(), PartialVMError> { + // When packing, an object whose struct type has key ability must have the first field as + // "id". That fields must come from one of the functions that creates a new UID. + let handle = verifier + .binary_view + .datatype_handle_at(struct_def.struct_handle); + let num_fields = num_fields(struct_def); + verifier.stack_popn(num_fields - 1)?; + let last_value = verifier.stack.pop().unwrap(); + if handle.abilities.has_key() && last_value != AbstractValue::Fresh { + let (cur_package, cur_module, cur_function) = verifier.cur_function(); + let msg = format!( + "Invalid object creation in {cur_package}::{cur_module}::{cur_function}. \ + Object created without a newly created UID. \ + The UID must come directly from `sui::{}::{}`, or `sui::{}::{}`. \ + For tests, it can also come from `sui::{}::{}`", + OBJECT_NEW.1, + OBJECT_NEW.2, + OBJECT_NEW_DERIVED.1, + OBJECT_NEW_DERIVED.2, + TS_NEW_OBJECT.1, + TS_NEW_OBJECT.2 + ); + + return Err(PartialVMError::new(StatusCode::UNKNOWN_VERIFICATION_ERROR) + .with_message(msg) + .with_sub_status(VMMVerifierErrorSubStatusCode::INVALID_OBJECT_CREATION as u64)); + } + verifier.stack_push(AbstractValue::Other)?; + Ok(()) +} + +fn unpack( + verifier: &mut IDLeakAnalysis, + struct_def: &StructDefinition, +) -> Result<(), PartialVMError> { + verifier.stack.pop().unwrap(); + verifier.stack_pushn(num_fields(struct_def), AbstractValue::Other) +} + +fn execute_inner( + verifier: &mut IDLeakAnalysis, + state: &mut AbstractState, + bytecode: &Bytecode, + _: CodeOffset, + meter: &mut (impl Meter + ?Sized), +) -> Result<(), PartialVMError> { + meter.add(Scope::Function, STEP_BASE_COST)?; + // TODO: Better diagnostics with location + match bytecode { + Bytecode::Pop => { + verifier.stack.pop().unwrap(); + } + Bytecode::CopyLoc(_local) => { + // cannot copy a UID + verifier.stack_push(AbstractValue::Other)?; + } + Bytecode::MoveLoc(local) => { + let value = state.locals.remove(local).unwrap(); + verifier.stack_push(value)?; + } + Bytecode::StLoc(local) => { + let value = verifier.stack.pop().unwrap(); + state.locals.insert(*local, value); + } + + // Reference won't be ID. + Bytecode::FreezeRef + // ID doesn't have copy ability, hence ReadRef won't produce an ID. + | Bytecode::ReadRef + // Following are unary operators that don't apply to ID. + | Bytecode::CastU8 + | Bytecode::CastU16 + | Bytecode::CastU32 + | Bytecode::CastU64 + | Bytecode::CastU128 + | Bytecode::CastU256 + | Bytecode::Not + | Bytecode::VecLen(_) + | Bytecode::VecPopBack(_) => { + verifier.stack.pop().unwrap(); + verifier.stack_push(AbstractValue::Other)?; + } + + // These bytecodes don't operate on any value. + Bytecode::Branch(_) + | Bytecode::Nop => {} + + // These binary operators cannot produce ID as result. + Bytecode::Eq + | Bytecode::Neq + | Bytecode::Add + | Bytecode::Sub + | Bytecode::Mul + | Bytecode::Mod + | Bytecode::Div + | Bytecode::BitOr + | Bytecode::BitAnd + | Bytecode::Xor + | Bytecode::Shl + | Bytecode::Shr + | Bytecode::Or + | Bytecode::And + | Bytecode::Lt + | Bytecode::Gt + | Bytecode::Le + | Bytecode::Ge + | Bytecode::VecImmBorrow(_) + | Bytecode::VecMutBorrow(_) => { + verifier.stack.pop().unwrap(); + verifier.stack.pop().unwrap(); + verifier.stack_push(AbstractValue::Other)?; + } + Bytecode::WriteRef => { + verifier.stack.pop().unwrap(); + verifier.stack.pop().unwrap(); + } + + // These bytecodes produce references, and hence cannot be ID. + Bytecode::MutBorrowLoc(_) + | Bytecode::ImmBorrowLoc(_) => verifier.stack_push(AbstractValue::Other)?, + + | Bytecode::MutBorrowField(_) + | Bytecode::MutBorrowFieldGeneric(_) + | Bytecode::ImmBorrowField(_) + | Bytecode::ImmBorrowFieldGeneric(_) => { + verifier.stack.pop().unwrap(); + verifier.stack_push(AbstractValue::Other)?; + } + + // These bytecodes are not allowed, and will be + // flagged as error in a different verifier. + Bytecode::MoveFromDeprecated(_) + | Bytecode::MoveFromGenericDeprecated(_) + | Bytecode::MoveToDeprecated(_) + | Bytecode::MoveToGenericDeprecated(_) + | Bytecode::ImmBorrowGlobalDeprecated(_) + | Bytecode::MutBorrowGlobalDeprecated(_) + | Bytecode::ImmBorrowGlobalGenericDeprecated(_) + | Bytecode::MutBorrowGlobalGenericDeprecated(_) + | Bytecode::ExistsDeprecated(_) + | Bytecode::ExistsGenericDeprecated(_) => { + panic!("Should have been checked by global_storage_access_verifier."); + } + + Bytecode::Call(idx) => { + let function_handle = verifier.binary_view.function_handle_at(*idx); + call(verifier, function_handle)?; + } + Bytecode::CallGeneric(idx) => { + let func_inst = verifier.binary_view.function_instantiation_at(*idx); + let function_handle = verifier.binary_view.function_handle_at(func_inst.handle); + call(verifier, function_handle)?; + } + + Bytecode::Ret => { + verifier.stack_popn(verifier.function_context.return_().len() as u64)? + } + + Bytecode::BrTrue(_) | Bytecode::BrFalse(_) | Bytecode::Abort => { + verifier.stack.pop().unwrap(); + } + + // These bytecodes produce constants, and hence cannot be ID. + Bytecode::LdTrue | Bytecode::LdFalse | Bytecode::LdU8(_) | Bytecode::LdU16(_)| Bytecode::LdU32(_) | Bytecode::LdU64(_) | Bytecode::LdU128(_)| Bytecode::LdU256(_) | Bytecode::LdConst(_) => { + verifier.stack_push(AbstractValue::Other)?; + } + + Bytecode::Pack(idx) => { + let struct_def = verifier.binary_view.struct_def_at(*idx); + pack(verifier, struct_def)?; + } + Bytecode::PackGeneric(idx) => { + let struct_inst = verifier.binary_view.struct_instantiation_at(*idx); + let struct_def = verifier.binary_view.struct_def_at(struct_inst.def); + pack(verifier, struct_def)?; + } + Bytecode::Unpack(idx) => { + let struct_def = verifier.binary_view.struct_def_at(*idx); + unpack(verifier, struct_def)?; + } + Bytecode::UnpackGeneric(idx) => { + let struct_inst = verifier.binary_view.struct_instantiation_at(*idx); + let struct_def = verifier.binary_view.struct_def_at(struct_inst.def); + unpack(verifier, struct_def)?; + } + + Bytecode::VecPack(_, num) => { + verifier.stack_popn(*num )?; + verifier.stack_push(AbstractValue::Other)?; + } + + Bytecode::VecPushBack(_) => { + verifier.stack.pop().unwrap(); + verifier.stack.pop().unwrap(); + } + + Bytecode::VecUnpack(_, num) => { + verifier.stack.pop().unwrap(); + verifier.stack_pushn(*num, AbstractValue::Other)?; + } + + Bytecode::VecSwap(_) => { + verifier.stack.pop().unwrap(); + verifier.stack.pop().unwrap(); + verifier.stack.pop().unwrap(); + } + Bytecode::PackVariant(vidx) => { + let handle = verifier.binary_view.variant_handle_at(*vidx); + let variant = verifier.binary_view.variant_def_at(handle.enum_def, handle.variant); + let num_fields = variant.fields.len(); + verifier.stack_popn(num_fields as u64)?; + verifier.stack_push(AbstractValue::Other)?; + } + Bytecode::PackVariantGeneric(vidx) => { + let handle = verifier.binary_view.variant_instantiation_handle_at(*vidx); + let enum_inst = verifier.binary_view.enum_instantiation_at(handle.enum_def); + let variant = verifier.binary_view.variant_def_at(enum_inst.def, handle.variant); + let num_fields = variant.fields.len(); + verifier.stack_popn(num_fields as u64)?; + verifier.stack_push(AbstractValue::Other)?; + } + Bytecode::UnpackVariant(vidx) + | Bytecode::UnpackVariantImmRef(vidx) + | Bytecode::UnpackVariantMutRef(vidx) => { + let handle = verifier.binary_view.variant_handle_at(*vidx); + let variant = verifier.binary_view.variant_def_at(handle.enum_def, handle.variant); + let num_fields = variant.fields.len(); + verifier.stack.pop().unwrap(); + verifier.stack_pushn(num_fields as u64, AbstractValue::Other)?; + } + Bytecode::UnpackVariantGeneric(vidx) + | Bytecode::UnpackVariantGenericImmRef(vidx) + | Bytecode::UnpackVariantGenericMutRef(vidx) => { + let handle = verifier.binary_view.variant_instantiation_handle_at(*vidx); + let enum_inst = verifier.binary_view.enum_instantiation_at(handle.enum_def); + let variant = verifier.binary_view.variant_def_at(enum_inst.def, handle.variant); + let num_fields = variant.fields.len(); + verifier.stack.pop().unwrap(); + verifier.stack_pushn(num_fields as u64, AbstractValue::Other)?; + } + Bytecode::VariantSwitch(_) => { + verifier.stack.pop().unwrap(); + } + }; + Ok(()) +} diff --git a/sui-execution/replay_cut/sui-verifier/src/lib.rs b/sui-execution/replay_cut/sui-verifier/src/lib.rs new file mode 100644 index 0000000000000..b085957b5657a --- /dev/null +++ b/sui-execution/replay_cut/sui-verifier/src/lib.rs @@ -0,0 +1,49 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +pub mod verifier; + +pub mod entry_points_verifier; +pub mod global_storage_access_verifier; +pub mod id_leak_verifier; +pub mod meter; +pub mod one_time_witness_verifier; +pub mod private_generics; +pub mod private_generics_verifier_v2; +pub mod struct_with_key_verifier; + +use move_core_types::{ + account_address::AccountAddress, ident_str, identifier::IdentStr, vm_status::StatusCode, +}; +use sui_types::error::{ExecutionError, ExecutionErrorKind}; + +pub const INIT_FN_NAME: &IdentStr = ident_str!("init"); +pub const TEST_SCENARIO_MODULE_NAME: &str = "test_scenario"; + +pub type FunctionIdent<'a> = (AccountAddress, &'a IdentStr, &'a IdentStr); + +fn verification_failure(error: String) -> ExecutionError { + ExecutionError::new_with_source(ExecutionErrorKind::SuiMoveVerificationError, error) +} + +fn to_verification_timeout_error(error: String) -> ExecutionError { + ExecutionError::new_with_source(ExecutionErrorKind::SuiMoveVerificationTimedout, error) +} + +/// Runs the Move verifier and checks if the error counts as a Move verifier timeout +/// NOTE: this function only check if the verifier error is a timeout +/// All other errors are ignored +pub fn check_for_verifier_timeout(major_status_code: &StatusCode) -> bool { + [ + StatusCode::PROGRAM_TOO_COMPLEX, + // Do we want to make this a substatus of `PROGRAM_TOO_COMPLEX`? + StatusCode::TOO_MANY_BACK_EDGES, + StatusCode::BORROWLOC_EXISTS_BORROW_ERROR, + StatusCode::COPYLOC_EXISTS_BORROW_ERROR, + StatusCode::DEPRECATED_BYTECODE_FORMAT, + StatusCode::REFERENCE_SAFETY_INCONSISTENT, + StatusCode::VEC_BORROW_ELEMENT_EXISTS_MUTABLE_BORROW_ERROR, + StatusCode::CALL_BORROWED_MUTABLE_REFERENCE_ERROR, + ] + .contains(major_status_code) +} diff --git a/sui-execution/replay_cut/sui-verifier/src/meter.rs b/sui-execution/replay_cut/sui-verifier/src/meter.rs new file mode 100644 index 0000000000000..e644631bd6cac --- /dev/null +++ b/sui-execution/replay_cut/sui-verifier/src/meter.rs @@ -0,0 +1,107 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use move_binary_format::errors::{PartialVMError, PartialVMResult}; +use move_bytecode_verifier_meter::{Meter, Scope}; +use move_core_types::vm_status::StatusCode; +use move_vm_config::verifier::MeterConfig; + +struct SuiVerifierMeterBounds { + name: String, + ticks: u128, + max_ticks: Option, +} + +impl SuiVerifierMeterBounds { + fn add(&mut self, ticks: u128) -> PartialVMResult<()> { + let max_ticks = self.max_ticks.unwrap_or(u128::MAX); + + let new_ticks = self.ticks.saturating_add(ticks); + if new_ticks >= max_ticks { + return Err(PartialVMError::new(StatusCode::PROGRAM_TOO_COMPLEX) + .with_message(format!( + "program too complex. Ticks exceeded `{}` will exceed limits: `{} current + {} new > {} max`)", + self.name, self.ticks, ticks, max_ticks + ))); + } + self.ticks = new_ticks; + Ok(()) + } +} + +pub struct SuiVerifierMeter { + transaction_bounds: SuiVerifierMeterBounds, + package_bounds: SuiVerifierMeterBounds, + module_bounds: SuiVerifierMeterBounds, + function_bounds: SuiVerifierMeterBounds, +} + +impl SuiVerifierMeter { + pub fn new(config: MeterConfig) -> Self { + Self { + transaction_bounds: SuiVerifierMeterBounds { + name: "".to_string(), + ticks: 0, + max_ticks: None, + }, + package_bounds: SuiVerifierMeterBounds { + name: "".to_string(), + ticks: 0, + max_ticks: config.max_per_pkg_meter_units, + }, + module_bounds: SuiVerifierMeterBounds { + name: "".to_string(), + ticks: 0, + max_ticks: config.max_per_mod_meter_units, + }, + function_bounds: SuiVerifierMeterBounds { + name: "".to_string(), + ticks: 0, + max_ticks: config.max_per_fun_meter_units, + }, + } + } + + fn get_bounds_mut(&mut self, scope: Scope) -> &mut SuiVerifierMeterBounds { + match scope { + Scope::Transaction => &mut self.transaction_bounds, + Scope::Package => &mut self.package_bounds, + Scope::Module => &mut self.module_bounds, + Scope::Function => &mut self.function_bounds, + } + } + + fn get_bounds(&self, scope: Scope) -> &SuiVerifierMeterBounds { + match scope { + Scope::Transaction => &self.transaction_bounds, + Scope::Package => &self.package_bounds, + Scope::Module => &self.module_bounds, + Scope::Function => &self.function_bounds, + } + } + + pub fn get_usage(&self, scope: Scope) -> u128 { + self.get_bounds(scope).ticks + } + + pub fn get_limit(&self, scope: Scope) -> Option { + self.get_bounds(scope).max_ticks + } +} + +impl Meter for SuiVerifierMeter { + fn enter_scope(&mut self, name: &str, scope: Scope) { + let bounds = self.get_bounds_mut(scope); + bounds.name = name.into(); + bounds.ticks = 0; + } + + fn transfer(&mut self, from: Scope, to: Scope, factor: f32) -> PartialVMResult<()> { + let ticks = (self.get_bounds_mut(from).ticks as f32 * factor) as u128; + self.add(to, ticks) + } + + fn add(&mut self, scope: Scope, ticks: u128) -> PartialVMResult<()> { + self.get_bounds_mut(scope).add(ticks) + } +} diff --git a/sui-execution/replay_cut/sui-verifier/src/one_time_witness_verifier.rs b/sui-execution/replay_cut/sui-verifier/src/one_time_witness_verifier.rs new file mode 100644 index 0000000000000..b443813d87aa2 --- /dev/null +++ b/sui-execution/replay_cut/sui-verifier/src/one_time_witness_verifier.rs @@ -0,0 +1,232 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//! A module can define a one-time witness type, that is a type that is instantiated only once, and +//! this property is enforced by the system. We define a one-time witness type as a struct type that +//! has the same name as the module that defines it but with all the letters capitalized, and +//! possessing certain special properties specified below (please note that by convention, "regular" +//! struct type names are expressed in camel case). In other words, if a module defines a struct +//! type whose name is the same as the module name, this type MUST possess these special properties, +//! otherwise the module definition will be considered invalid and will be rejected by the +//! validator: +//! +//! - it has only one ability: drop +//! - it has only one arbitrarily named field of type boolean (since Move structs cannot be empty) +//! - its definition does not involve type parameters +//! - its only instance in existence is passed as an argument to the module initializer +//! - it is never instantiated anywhere in its defining module +use move_binary_format::file_format::{ + Ability, AbilitySet, Bytecode, CompiledModule, DatatypeHandle, FunctionDefinition, + FunctionHandle, SignatureToken, StructDefinition, +}; +use move_core_types::{ident_str, language_storage::ModuleId}; +use sui_types::bridge::BRIDGE_SUPPORTED_ASSET; +use sui_types::{ + BRIDGE_ADDRESS, SUI_FRAMEWORK_ADDRESS, + base_types::{TX_CONTEXT_MODULE_NAME, TX_CONTEXT_STRUCT_NAME}, + error::ExecutionError, + move_package::{FnInfoMap, is_test_fun}, +}; + +use crate::{INIT_FN_NAME, verification_failure}; + +pub fn verify_module( + module: &CompiledModule, + fn_info_map: &FnInfoMap, +) -> Result<(), ExecutionError> { + // When verifying test functions, a check preventing by-hand instantiation of one-time withess + // is disabled + + // In Sui's framework code there is an exception to the one-time witness type rule - we have a + // SUI type in the sui module but it is instantiated outside of the module initializer (in fact, + // the module has no initializer). The reason for it is that the SUI coin is only instantiated + // during genesis. It is easiest to simply special-case this module particularly that this is + // framework code and thus deemed correct. + let self_id = module.self_id(); + + if ModuleId::new(SUI_FRAMEWORK_ADDRESS, ident_str!("sui").to_owned()) == self_id { + return Ok(()); + } + + if BRIDGE_SUPPORTED_ASSET + .iter() + .any(|token| ModuleId::new(BRIDGE_ADDRESS, ident_str!(token).to_owned()) == self_id) + { + return Ok(()); + } + + let mod_handle = module.module_handle_at(module.self_module_handle_idx); + let mod_name = module.identifier_at(mod_handle.name).as_str(); + let struct_defs = &module.struct_defs; + let mut one_time_witness_candidate = None; + // find structs that can potentially represent a one-time witness type + for def in struct_defs { + let struct_handle = module.datatype_handle_at(def.struct_handle); + let struct_name = module.identifier_at(struct_handle.name).as_str(); + if mod_name.to_ascii_uppercase() == struct_name { + // one-time witness candidate's type name must be the same as capitalized module name + if let Ok(field_count) = def.declared_field_count() { + // checks if the struct is non-native (and if it isn't then that's why unwrap below + // is safe) + if field_count == 1 && def.field(0).unwrap().signature.0 == SignatureToken::Bool { + // a single boolean field means that we found a one-time witness candidate - + // make sure that the remaining properties hold + verify_one_time_witness(module, struct_name, struct_handle) + .map_err(verification_failure)?; + // if we reached this point, it means we have a legitimate one-time witness type + // candidate and we have to make sure that both the init function's signature + // reflects this and that this type is not instantiated in any function of the + // module + one_time_witness_candidate = Some((struct_name, struct_handle, def)); + break; // no reason to look any further + } + } + } + } + for fn_def in &module.function_defs { + let fn_handle = module.function_handle_at(fn_def.function); + let fn_name = module.identifier_at(fn_handle.name); + if fn_name == INIT_FN_NAME { + if let Some((candidate_name, candidate_handle, _)) = one_time_witness_candidate { + // only verify if init function conforms to one-time witness type requirements if we + // have a one-time witness type candidate + verify_init_one_time_witness(module, fn_handle, candidate_name, candidate_handle) + .map_err(verification_failure)?; + } else { + // if there is no one-time witness type candidate than the init function should have + // only one parameter of TxContext type + verify_init_single_param(module, fn_handle).map_err(verification_failure)?; + } + } + if let Some((candidate_name, _, def)) = one_time_witness_candidate { + // only verify lack of one-time witness type instantiations if we have a one-time + // witness type candidate and if instantiation does not happen in test code + + if !is_test_fun(fn_name, module, fn_info_map) { + verify_no_instantiations(module, fn_def, candidate_name, def) + .map_err(verification_failure)?; + } + } + } + + Ok(()) +} + +// Verifies all required properties of a one-time witness type candidate (that is a type whose name +// is the same as the name of a module but capitalized) +fn verify_one_time_witness( + module: &CompiledModule, + candidate_name: &str, + candidate_handle: &DatatypeHandle, +) -> Result<(), String> { + // must have only one ability: drop + let drop_set = AbilitySet::EMPTY | Ability::Drop; + let abilities = candidate_handle.abilities; + if abilities != drop_set { + return Err(format!( + "one-time witness type candidate {}::{} must have a single ability: drop", + module.self_id(), + candidate_name, + )); + } + + if !candidate_handle.type_parameters.is_empty() { + return Err(format!( + "one-time witness type candidate {}::{} cannot have type parameters", + module.self_id(), + candidate_name, + )); + } + Ok(()) +} + +/// Checks if this module's `init` function conformant with the one-time witness type +fn verify_init_one_time_witness( + module: &CompiledModule, + fn_handle: &FunctionHandle, + candidate_name: &str, + candidate_handle: &DatatypeHandle, +) -> Result<(), String> { + let fn_sig = module.signature_at(fn_handle.parameters); + if fn_sig.len() != 2 || !is_one_time_witness(module, &fn_sig.0[0], candidate_handle) { + // check only the first parameter - the other one is checked in entry_points verification + // pass + return Err(format!( + "init function of a module containing one-time witness type candidate must have \ + {}::{} as the first parameter (a struct which has no fields or a single field of type \ + bool)", + module.self_id(), + candidate_name, + )); + } + + Ok(()) +} + +// Checks if a given SignatureToken represents a one-time witness type struct +fn is_one_time_witness( + view: &CompiledModule, + tok: &SignatureToken, + candidate_handle: &DatatypeHandle, +) -> bool { + matches!(tok, SignatureToken::Datatype(idx) if view.datatype_handle_at(*idx) == candidate_handle) +} + +/// Checks if this module's `init` function has a single parameter of TxContext type only +fn verify_init_single_param( + module: &CompiledModule, + fn_handle: &FunctionHandle, +) -> Result<(), String> { + let fn_sig = module.signature_at(fn_handle.parameters); + if fn_sig.len() != 1 { + return Err(format!( + "Expected last (and at most second) parameter for {0}::{1} to be &mut {2}::{3}::{4} or \ + &{2}::{3}::{4}; optional first parameter must be of one-time witness type whose name \ + is the same as the capitalized module name ({5}::{6}) and which has no fields or a \ + single field of type bool", + module.self_id(), + INIT_FN_NAME, + SUI_FRAMEWORK_ADDRESS, + TX_CONTEXT_MODULE_NAME, + TX_CONTEXT_STRUCT_NAME, + module.self_id(), + module.self_id().name().as_str().to_uppercase(), + )); + } + + Ok(()) +} + +/// Checks if this module function does not contain instantiation of the one-time witness type +fn verify_no_instantiations( + module: &CompiledModule, + fn_def: &FunctionDefinition, + struct_name: &str, + struct_def: &StructDefinition, +) -> Result<(), String> { + if fn_def.code.is_none() { + return Ok(()); + } + for bcode in &fn_def.code.as_ref().unwrap().code { + let struct_def_idx = match bcode { + Bytecode::Pack(idx) => idx, + _ => continue, + }; + // unwrap is safe below since we know we are getting a struct out of a module (see + // definition of struct_def_at) + if module.struct_def_at(*struct_def_idx) == struct_def { + let fn_handle = module.function_handle_at(fn_def.function); + let fn_name = module.identifier_at(fn_handle.name); + return Err(format!( + "one-time witness type {}::{} is instantiated \ + in the {}::{} function and must never be", + module.self_id(), + struct_name, + module.self_id(), + fn_name, + )); + } + } + + Ok(()) +} diff --git a/sui-execution/replay_cut/sui-verifier/src/private_generics.rs b/sui-execution/replay_cut/sui-verifier/src/private_generics.rs new file mode 100644 index 0000000000000..4ed3995a04129 --- /dev/null +++ b/sui-execution/replay_cut/sui-verifier/src/private_generics.rs @@ -0,0 +1,283 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use move_binary_format::{ + CompiledModule, + file_format::{ + Bytecode, FunctionDefinition, FunctionHandle, FunctionInstantiation, ModuleHandle, + SignatureToken, + }, +}; +use move_bytecode_utils::format_signature_token; +use move_core_types::{account_address::AccountAddress, ident_str, identifier::IdentStr}; +use move_vm_config::verifier::VerifierConfig; +use sui_types::{SUI_FRAMEWORK_ADDRESS, error::ExecutionError}; + +use crate::{TEST_SCENARIO_MODULE_NAME, verification_failure}; + +pub const TRANSFER_MODULE: &IdentStr = ident_str!("transfer"); +pub const EVENT_MODULE: &IdentStr = ident_str!("event"); +pub const EVENT_FUNCTION: &IdentStr = ident_str!("emit"); +pub const EMIT_AUTHENTICATED_FUNCTION: &IdentStr = ident_str!("emit_authenticated"); +pub const EMIT_AUTHENTICATED_IMPL_FUNCTION: &IdentStr = ident_str!("emit_authenticated_impl"); + +pub const PRIVATE_EVENT_FUNCTIONS: &[&IdentStr] = &[EVENT_FUNCTION, EMIT_AUTHENTICATED_FUNCTION]; +pub const GET_EVENTS_TEST_FUNCTION: &IdentStr = ident_str!("events_by_type"); +pub const COIN_REGISTRY_MODULE: &IdentStr = ident_str!("coin_registry"); +pub const DYNAMIC_COIN_CREATION_FUNCTION: &IdentStr = ident_str!("new_currency"); +pub const PUBLIC_TRANSFER_FUNCTIONS: &[&IdentStr] = &[ + ident_str!("public_transfer"), + ident_str!("public_freeze_object"), + ident_str!("public_share_object"), + ident_str!("public_receive"), + ident_str!("receiving_object_id"), + ident_str!("public_party_transfer"), +]; +pub const PRIVATE_TRANSFER_FUNCTIONS: &[&IdentStr] = &[ + ident_str!("transfer"), + ident_str!("freeze_object"), + ident_str!("share_object"), + ident_str!("receive"), + ident_str!("party_transfer"), +]; + +/// All transfer functions (the functions in `sui::transfer`) are "private" in that they are +/// restricted to the module. +/// For example, with `transfer::transfer(...)`, either: +/// - `T` must be a type declared in the current module or +/// - `T` must have `store` +/// +/// Similarly, `event::emit` is also "private" to the module. Unlike the `transfer` functions, there +/// is no relaxation for `store` +/// Concretely, with `event::emit(...)`: +/// - `T` must be a type declared in the current module +pub fn verify_module( + module: &CompiledModule, + verifier_config: &VerifierConfig, +) -> Result<(), ExecutionError> { + if *module.address() == SUI_FRAMEWORK_ADDRESS + && module.name() == IdentStr::new(TEST_SCENARIO_MODULE_NAME).unwrap() + { + // exclude test_module which is a test-only module in the Sui framework which "emulates" + // transactional execution and needs to allow test code to bypass private generics + return Ok(()); + } + // do not need to check the sui::transfer module itself + for func_def in &module.function_defs { + verify_function(module, func_def, verifier_config.allow_receiving_object_id).map_err( + |error| { + verification_failure(format!( + "{}::{}. {}", + module.self_id(), + module.identifier_at(module.function_handle_at(func_def.function).name), + error + )) + }, + )?; + } + Ok(()) +} + +fn verify_function( + view: &CompiledModule, + fdef: &FunctionDefinition, + allow_receiving_object_id: bool, +) -> Result<(), String> { + let code = match &fdef.code { + None => return Ok(()), + Some(code) => code, + }; + for instr in &code.code { + if let Bytecode::CallGeneric(finst_idx) = instr { + let FunctionInstantiation { + handle, + type_parameters, + } = view.function_instantiation_at(*finst_idx); + + let fhandle = view.function_handle_at(*handle); + let mhandle = view.module_handle_at(fhandle.module); + + let type_arguments = &view.signature_at(*type_parameters).0; + let ident = addr_module(view, mhandle); + if ident == (SUI_FRAMEWORK_ADDRESS, TRANSFER_MODULE) { + verify_private_transfer(view, fhandle, type_arguments, allow_receiving_object_id)? + } else if ident == (SUI_FRAMEWORK_ADDRESS, EVENT_MODULE) { + verify_private_event_emit(view, fhandle, type_arguments)? + } else if ident == (SUI_FRAMEWORK_ADDRESS, COIN_REGISTRY_MODULE) { + verify_dynamic_coin_creation(view, fhandle, type_arguments)? + } + } + } + Ok(()) +} + +fn verify_private_transfer( + view: &CompiledModule, + fhandle: &FunctionHandle, + type_arguments: &[SignatureToken], + allow_receiving_object_id: bool, +) -> Result<(), String> { + let public_transfer_functions = if allow_receiving_object_id { + PUBLIC_TRANSFER_FUNCTIONS + } else { + // Before protocol version 33, the `receiving_object_id` function was not public + &PUBLIC_TRANSFER_FUNCTIONS[..4] + }; + let self_handle = view.module_handle_at(view.self_handle_idx()); + if addr_module(view, self_handle) == (SUI_FRAMEWORK_ADDRESS, TRANSFER_MODULE) { + return Ok(()); + } + let fident = view.identifier_at(fhandle.name); + // public transfer functions require `store` and have no additional rules + if public_transfer_functions.contains(&fident) { + return Ok(()); + } + if !PRIVATE_TRANSFER_FUNCTIONS.contains(&fident) { + // unknown function, so a bug in the implementation here + debug_assert!(false, "unknown transfer function {}", fident); + return Err(format!("Calling unknown transfer function, {}", fident)); + }; + + if type_arguments.len() != 1 { + debug_assert!(false, "Expected 1 type argument for {}", fident); + return Err(format!("Expected 1 type argument for {}", fident)); + } + + let type_arg = &type_arguments[0]; + if !is_defined_in_current_module(view, type_arg) { + return Err(format!( + "Invalid call to '{sui}::transfer::{f}' on an object of type '{t}'. \ + The transferred object's type must be defined in the current module. \ + If the object has the 'store' type ability, you can use the non-internal variant \ + instead, i.e. '{sui}::transfer::public_{f}'", + sui = SUI_FRAMEWORK_ADDRESS, + f = fident, + t = format_signature_token(view, type_arg), + )); + } + + Ok(()) +} + +fn verify_private_event_emit( + view: &CompiledModule, + fhandle: &FunctionHandle, + type_arguments: &[SignatureToken], +) -> Result<(), String> { + let fident = view.identifier_at(fhandle.name); + if fident == GET_EVENTS_TEST_FUNCTION { + // test-only function with no params--no need to verify + return Ok(()); + } + + if fident == EMIT_AUTHENTICATED_IMPL_FUNCTION { + let module_id = view.self_id(); + if (module_id.address(), module_id.name()) != (&SUI_FRAMEWORK_ADDRESS, EVENT_MODULE) { + debug_assert!( + false, + "Calling {} outside of {} module this shouldn't happen", + EMIT_AUTHENTICATED_IMPL_FUNCTION, EVENT_MODULE + ); + return Err(format!( + "Calling {} outside of {} which is impossible", + EMIT_AUTHENTICATED_IMPL_FUNCTION, EVENT_MODULE + )); + } else { + return Ok(()); + } + } + + if !PRIVATE_EVENT_FUNCTIONS.contains(&fident) { + debug_assert!(false, "unknown event function {}", fident); + return Err(format!("Calling unknown event function, {}", fident)); + }; + + if type_arguments.len() != 1 { + debug_assert!(false, "Expected 1 type argument for {}", fident); + return Err(format!("Expected 1 type argument for {}", fident)); + } + + let type_arg = &type_arguments[0]; + if !is_defined_in_current_module(view, type_arg) { + return Err(format!( + "Invalid call to '{}::event::{}' with an event type '{}'. \ + The event's type must be defined in the current module", + SUI_FRAMEWORK_ADDRESS, + fident, + format_signature_token(view, type_arg), + )); + } + + Ok(()) +} + +/// Coin creation using `key` (non OTW coin creation) is only possible with +/// internal types. +fn verify_dynamic_coin_creation( + view: &CompiledModule, + fhandle: &FunctionHandle, + type_arguments: &[SignatureToken], +) -> Result<(), String> { + let fident = view.identifier_at(fhandle.name); + + // If we are calling anything besides `coin::new_currency`, + // we don't need this check. + if fident != DYNAMIC_COIN_CREATION_FUNCTION { + return Ok(()); + } + + // We expect a single type argument (`T: key`) + if type_arguments.len() != 1 { + debug_assert!(false, "Expected 1 type argument for {}", fident); + return Err(format!("Expected 1 type argument for {}", fident)); + } + + let type_arg = &type_arguments[0]; + if !is_defined_in_current_module(view, type_arg) { + return Err(format!( + "Invalid call to '{}::coin_registry::{}' with type '{}'. \ + The coin's type must be defined in the current module", + SUI_FRAMEWORK_ADDRESS, + fident, + format_signature_token(view, type_arg), + )); + } + + Ok(()) +} + +fn is_defined_in_current_module(view: &CompiledModule, type_arg: &SignatureToken) -> bool { + match type_arg { + SignatureToken::Datatype(_) | SignatureToken::DatatypeInstantiation(_) => { + let idx = match type_arg { + SignatureToken::Datatype(idx) => *idx, + SignatureToken::DatatypeInstantiation(s) => s.0, + _ => unreachable!(), + }; + let shandle = view.datatype_handle_at(idx); + view.self_handle_idx() == shandle.module + } + SignatureToken::TypeParameter(_) + | SignatureToken::Bool + | SignatureToken::U8 + | SignatureToken::U16 + | SignatureToken::U32 + | SignatureToken::U64 + | SignatureToken::U128 + | SignatureToken::U256 + | SignatureToken::Address + | SignatureToken::Vector(_) + | SignatureToken::Signer + | SignatureToken::Reference(_) + | SignatureToken::MutableReference(_) => false, + } +} + +fn addr_module<'a>( + view: &'a CompiledModule, + mhandle: &ModuleHandle, +) -> (AccountAddress, &'a IdentStr) { + let maddr = view.address_identifier_at(mhandle.address); + let mident = view.identifier_at(mhandle.name); + (*maddr, mident) +} diff --git a/sui-execution/replay_cut/sui-verifier/src/private_generics_verifier_v2.rs b/sui-execution/replay_cut/sui-verifier/src/private_generics_verifier_v2.rs new file mode 100644 index 0000000000000..d24d3737df62a --- /dev/null +++ b/sui-execution/replay_cut/sui-verifier/src/private_generics_verifier_v2.rs @@ -0,0 +1,336 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use std::borrow::Cow; + +use move_binary_format::{ + CompiledModule, + file_format::{Bytecode, FunctionDefinition, FunctionHandle, SignatureToken, Visibility}, +}; +use move_bytecode_utils::format_signature_token; +use move_core_types::{account_address::AccountAddress, ident_str, identifier::IdentStr}; +use move_vm_config::verifier::VerifierConfig; +use sui_types::{ + MOVE_STDLIB_ADDRESS, SUI_FRAMEWORK_ADDRESS, error::ExecutionError, make_invariant_violation, +}; + +use crate::{FunctionIdent, TEST_SCENARIO_MODULE_NAME, verification_failure}; + +pub const TRANSFER_MODULE: &IdentStr = ident_str!("transfer"); +pub const EVENT_MODULE: &IdentStr = ident_str!("event"); +pub const COIN_REGISTRY_MODULE: &IdentStr = ident_str!("coin_registry"); + +// Event function +pub const SUI_EVENT_EMIT_EVENT: FunctionIdent = + (SUI_FRAMEWORK_ADDRESS, EVENT_MODULE, ident_str!("emit")); +pub const SUI_EVENT_EMIT_AUTHENTICATED: FunctionIdent = ( + SUI_FRAMEWORK_ADDRESS, + EVENT_MODULE, + ident_str!("emit_authenticated"), +); +pub const SUI_EVENT_NUM_EVENTS: FunctionIdent = ( + SUI_FRAMEWORK_ADDRESS, + EVENT_MODULE, + ident_str!("num_events"), +); +pub const SUI_EVENT_EVENTS_BY_TYPE: FunctionIdent = ( + SUI_FRAMEWORK_ADDRESS, + EVENT_MODULE, + ident_str!("events_by_type"), +); + +// Public transfer functions +pub const SUI_TRANSFER_PUBLIC_TRANSFER: FunctionIdent = ( + SUI_FRAMEWORK_ADDRESS, + TRANSFER_MODULE, + ident_str!("public_transfer"), +); +pub const SUI_TRANSFER_PUBLIC_FREEZE_OBJECT: FunctionIdent = ( + SUI_FRAMEWORK_ADDRESS, + TRANSFER_MODULE, + ident_str!("public_freeze_object"), +); +pub const SUI_TRANSFER_PUBLIC_SHARE_OBJECT: FunctionIdent = ( + SUI_FRAMEWORK_ADDRESS, + TRANSFER_MODULE, + ident_str!("public_share_object"), +); +pub const SUI_TRANSFER_PUBLIC_RECEIVE: FunctionIdent = ( + SUI_FRAMEWORK_ADDRESS, + TRANSFER_MODULE, + ident_str!("public_receive"), +); +pub const SUI_TRANSFER_RECEIVING_OBJECT_ID: FunctionIdent = ( + SUI_FRAMEWORK_ADDRESS, + TRANSFER_MODULE, + ident_str!("receiving_object_id"), +); +pub const SUI_TRANSFER_PUBLIC_PARTY_TRANSFER: FunctionIdent = ( + SUI_FRAMEWORK_ADDRESS, + TRANSFER_MODULE, + ident_str!("public_party_transfer"), +); + +// Private transfer functions +pub const SUI_TRANSFER_TRANSFER: FunctionIdent = ( + SUI_FRAMEWORK_ADDRESS, + TRANSFER_MODULE, + ident_str!("transfer"), +); +pub const SUI_TRANSFER_FREEZE_OBJECT: FunctionIdent = ( + SUI_FRAMEWORK_ADDRESS, + TRANSFER_MODULE, + ident_str!("freeze_object"), +); +pub const SUI_TRANSFER_SHARE_OBJECT: FunctionIdent = ( + SUI_FRAMEWORK_ADDRESS, + TRANSFER_MODULE, + ident_str!("share_object"), +); +pub const SUI_TRANSFER_RECEIVE: FunctionIdent = ( + SUI_FRAMEWORK_ADDRESS, + TRANSFER_MODULE, + ident_str!("receive"), +); +pub const SUI_TRANSFER_PARTY_TRANSFER: FunctionIdent = ( + SUI_FRAMEWORK_ADDRESS, + TRANSFER_MODULE, + ident_str!("party_transfer"), +); + +// Coin registry functions +pub const SUI_COIN_REGISTRY_NEW_CURRENCY: FunctionIdent = ( + SUI_FRAMEWORK_ADDRESS, + COIN_REGISTRY_MODULE, + ident_str!("new_currency"), +); + +// Modules that must have all public functions listed in `FUNCTIONS_TO_CHECK` +pub const EXHAUSTIVE_MODULES: &[(AccountAddress, &IdentStr)] = &[ + (SUI_FRAMEWORK_ADDRESS, EVENT_MODULE), + (SUI_FRAMEWORK_ADDRESS, TRANSFER_MODULE), +]; + +// A list of all functions to check for internal rules. A boolean for each type parameter indicates +// if the type parameter is `internal` +pub const FUNCTIONS_TO_CHECK: &[(FunctionIdent, &[/* is internal */ bool])] = &[ + // event functions + (SUI_EVENT_EMIT_EVENT, &[true]), + (SUI_EVENT_EMIT_AUTHENTICATED, &[true]), + (SUI_EVENT_NUM_EVENTS, &[]), + (SUI_EVENT_EVENTS_BY_TYPE, &[false]), + // public transfer functions + (SUI_TRANSFER_PUBLIC_TRANSFER, &[false]), + (SUI_TRANSFER_PUBLIC_FREEZE_OBJECT, &[false]), + (SUI_TRANSFER_PUBLIC_SHARE_OBJECT, &[false]), + (SUI_TRANSFER_PUBLIC_RECEIVE, &[false]), + (SUI_TRANSFER_RECEIVING_OBJECT_ID, &[false]), + (SUI_TRANSFER_PUBLIC_PARTY_TRANSFER, &[false]), + // private transfer functions + (SUI_TRANSFER_TRANSFER, &[true]), + (SUI_TRANSFER_FREEZE_OBJECT, &[true]), + (SUI_TRANSFER_SHARE_OBJECT, &[true]), + (SUI_TRANSFER_RECEIVE, &[true]), + (SUI_TRANSFER_PARTY_TRANSFER, &[true]), + // coin registry functions + (SUI_COIN_REGISTRY_NEW_CURRENCY, &[true]), +]; + +enum Error { + User(String), + InvariantViolation(String), +} + +/// Several functions in the Sui Framework have `internal` type parameters, whose arguments must be +/// instantiated with types defined in the caller's module. +/// For example, with `transfer::transfer(...)` `T` must be a type declared in the current +/// module. Otherwise, `transfer::public_transfer(...)` can be used without restriction, as long +/// as `T` has `store`. Note thought that the ability constraint is not checked in this verifier, +/// but rather in the normal bytecode verifier type checking. +/// To avoid, issues, all `su::transfer` and `sui::event` functions must be configured in `INTERNAL_FUNCTIONS`. +pub fn verify_module( + module: &CompiledModule, + _verifier_config: &VerifierConfig, +) -> Result<(), ExecutionError> { + let module_id = module.self_id(); + let module_address = *module_id.address(); + let module_name = module_id.name(); + + // Skip sui::test_scenario + if module_address == SUI_FRAMEWORK_ADDRESS && module_name.as_str() == TEST_SCENARIO_MODULE_NAME + { + // exclude test_module which is a test-only module in the Sui framework which "emulates" + // transactional execution and needs to allow test code to bypass private generics + return Ok(()); + }; + + // Check exhaustiveness for sensitive modules + if EXHAUSTIVE_MODULES.contains(&(module_address, module_name)) { + for fdef in module + .function_defs + .iter() + .filter(|fdef| fdef.visibility == Visibility::Public) + { + let function_name = module.identifier_at(module.function_handle_at(fdef.function).name); + let resolved = &(module_address, module_name, function_name); + let rules_opt = FUNCTIONS_TO_CHECK.iter().find(|(f, _)| f == resolved); + if rules_opt.is_none() { + // The function needs to be added to the FUNCTIONS_TO_CHECK list + return Err(make_invariant_violation!( + "Unknown function '{module_id}::{function_name}'. \ + All functions in '{module_id}' must be listed in FUNCTIONS_TO_CHECK", + )); + } + } + } + + // Check calls + for func_def in &module.function_defs { + verify_function(module, func_def).map_err(|error| match error { + Error::User(error) => verification_failure(format!( + "{}::{}. {}", + module.self_id(), + module.identifier_at(module.function_handle_at(func_def.function).name), + error + )), + Error::InvariantViolation(error) => { + make_invariant_violation!( + "{}::{}. {}", + module.self_id(), + module.identifier_at(module.function_handle_at(func_def.function).name), + error + ) + } + })?; + } + Ok(()) +} + +fn verify_function(module: &CompiledModule, fdef: &FunctionDefinition) -> Result<(), Error> { + let code = match &fdef.code { + None => return Ok(()), + Some(code) => code, + }; + for instr in &code.code { + let (callee, ty_args): (FunctionIdent<'_>, &[SignatureToken]) = match instr { + Bytecode::Call(fhandle_idx) => { + let fhandle = module.function_handle_at(*fhandle_idx); + (resolve_function(module, fhandle), &[]) + } + Bytecode::CallGeneric(finst_idx) => { + let finst = module.function_instantiation_at(*finst_idx); + let fhandle = module.function_handle_at(finst.handle); + let type_arguments = &module.signature_at(finst.type_parameters).0; + (resolve_function(module, fhandle), type_arguments) + } + _ => continue, + }; + verify_call(module, callee, ty_args)?; + } + Ok(()) +} + +fn verify_call( + module: &CompiledModule, + callee @ (callee_addr, callee_module, callee_function): FunctionIdent<'_>, + ty_args: &[SignatureToken], +) -> Result<(), Error> { + let Some((_, internal_flags)) = FUNCTIONS_TO_CHECK.iter().find(|(f, _)| &callee == f) else { + return Ok(()); + }; + let internal_flags = *internal_flags; + if ty_args.len() != internal_flags.len() { + // This should have been caught by the bytecode verifier + return Err(Error::InvariantViolation(format!( + "'{callee_addr}::{callee_module}::{callee_function}' \ + expects {} type arguments found {}", + internal_flags.len(), + ty_args.len() + ))); + } + for (idx, (ty_arg, &is_internal)) in ty_args.iter().zip(internal_flags).enumerate() { + if !is_internal { + continue; + } + if !is_defined_in_current_module(module, ty_arg) { + let callee_package_name = callee_package_name(&callee_addr); + let help = help_message(&callee_addr, callee_module, callee_function); + return Err(Error::User(format!( + "Invalid call to '{callee_package_name}::{callee_module}::{callee_function}'. \ + Type argument #{idx} must be a type defined in the current module, found '{}'.\ + {help}", + format_signature_token(module, ty_arg), + ))); + } + } + + Ok(()) +} + +fn resolve_function<'a>( + module: &'a CompiledModule, + callee_handle: &FunctionHandle, +) -> FunctionIdent<'a> { + let mh = module.module_handle_at(callee_handle.module); + let a = *module.address_identifier_at(mh.address); + let m = module.identifier_at(mh.name); + let f = module.identifier_at(callee_handle.name); + (a, m, f) +} + +fn is_defined_in_current_module(module: &CompiledModule, type_arg: &SignatureToken) -> bool { + match type_arg { + SignatureToken::Datatype(_) | SignatureToken::DatatypeInstantiation(_) => { + let idx = match type_arg { + SignatureToken::Datatype(idx) => *idx, + SignatureToken::DatatypeInstantiation(s) => s.0, + _ => unreachable!(), + }; + let shandle = module.datatype_handle_at(idx); + module.self_handle_idx() == shandle.module + } + SignatureToken::TypeParameter(_) + | SignatureToken::Bool + | SignatureToken::U8 + | SignatureToken::U16 + | SignatureToken::U32 + | SignatureToken::U64 + | SignatureToken::U128 + | SignatureToken::U256 + | SignatureToken::Address + | SignatureToken::Vector(_) + | SignatureToken::Signer + | SignatureToken::Reference(_) + | SignatureToken::MutableReference(_) => false, + } +} + +pub fn callee_package_name(callee_addr: &AccountAddress) -> Cow<'static, str> { + match *callee_addr { + SUI_FRAMEWORK_ADDRESS => Cow::Borrowed("sui"), + MOVE_STDLIB_ADDRESS => Cow::Borrowed("std"), + a => { + debug_assert!( + false, + "unknown package in private generics verifier. \ + Please improve this error message" + ); + Cow::Owned(format!("{a}")) + } + } +} + +pub fn help_message( + callee_addr: &AccountAddress, + callee_module: &IdentStr, + callee_function: &IdentStr, +) -> String { + if *callee_addr == SUI_FRAMEWORK_ADDRESS && callee_module == TRANSFER_MODULE { + format!( + " If the type has the 'store' ability, use the public variant instead: 'sui::transfer::public_{}'.", + callee_function + ) + } else { + String::new() + } +} diff --git a/sui-execution/replay_cut/sui-verifier/src/struct_with_key_verifier.rs b/sui-execution/replay_cut/sui-verifier/src/struct_with_key_verifier.rs new file mode 100644 index 0000000000000..23eee83f816ca --- /dev/null +++ b/sui-execution/replay_cut/sui-verifier/src/struct_with_key_verifier.rs @@ -0,0 +1,96 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//! This pass verifies necessary properties for Move Objects, i.e. structs with the `key` ability. +//! The properties checked are +//! - The first field is named "id" +//! - The first field has type `sui::object::UID` + +use crate::verification_failure; +use move_binary_format::file_format::{CompiledModule, SignatureToken}; +use sui_types::{ + SUI_FRAMEWORK_ADDRESS, + error::ExecutionError, + fp_ensure, + id::{OBJECT_MODULE_NAME, UID_STRUCT_NAME}, +}; + +pub fn verify_module(module: &CompiledModule) -> Result<(), ExecutionError> { + verify_key_structs(module)?; + verify_no_key_enums(module) +} + +fn verify_key_structs(module: &CompiledModule) -> Result<(), ExecutionError> { + let struct_defs = &module.struct_defs; + for def in struct_defs { + let handle = module.datatype_handle_at(def.struct_handle); + if !handle.abilities.has_key() { + continue; + } + let name = module.identifier_at(handle.name); + + // Check that the first field of the struct must be named "id". + let first_field = match def.field(0) { + Some(field) => field, + None => { + return Err(verification_failure(format!( + "First field of struct {} must be 'id', no field found", + name + ))); + } + }; + let first_field_name = module.identifier_at(first_field.name).as_str(); + if first_field_name != "id" { + return Err(verification_failure(format!( + "First field of struct {} must be 'id', {} found", + name, first_field_name + ))); + } + // Check that the "id" field must have a struct type. + let uid_field_type = &first_field.signature.0; + let uid_field_type = match uid_field_type { + SignatureToken::Datatype(struct_type) => struct_type, + _ => { + return Err(verification_failure(format!( + "First field of struct {} must be of type {}::object::UID, \ + {:?} type found", + name, SUI_FRAMEWORK_ADDRESS, uid_field_type + ))); + } + }; + // check that the struct type for "id" field must be SUI_FRAMEWORK_ADDRESS::object::UID. + let uid_type_struct = module.datatype_handle_at(*uid_field_type); + let uid_type_struct_name = module.identifier_at(uid_type_struct.name); + let uid_type_module = module.module_handle_at(uid_type_struct.module); + let uid_type_module_address = module.address_identifier_at(uid_type_module.address); + let uid_type_module_name = module.identifier_at(uid_type_module.name); + fp_ensure!( + uid_type_struct_name == UID_STRUCT_NAME + && uid_type_module_address == &SUI_FRAMEWORK_ADDRESS + && uid_type_module_name == OBJECT_MODULE_NAME, + verification_failure(format!( + "First field of struct {} must be of type {}::object::UID, \ + {}::{}::{} type found", + name, + SUI_FRAMEWORK_ADDRESS, + uid_type_module_address, + uid_type_module_name, + uid_type_struct_name + )) + ); + } + Ok(()) +} + +fn verify_no_key_enums(module: &CompiledModule) -> Result<(), ExecutionError> { + for def in &module.enum_defs { + let handle = module.datatype_handle_at(def.enum_handle); + if handle.abilities.has_key() { + return Err(verification_failure(format!( + "Enum {} cannot have the 'key' ability. Enums cannot have the 'key' ability.", + module.identifier_at(handle.name) + ))); + } + } + Ok(()) +} diff --git a/sui-execution/replay_cut/sui-verifier/src/verifier.rs b/sui-execution/replay_cut/sui-verifier/src/verifier.rs new file mode 100644 index 0000000000000..f957a018afe24 --- /dev/null +++ b/sui-execution/replay_cut/sui-verifier/src/verifier.rs @@ -0,0 +1,76 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//! This module contains the public APIs supported by the bytecode verifier. + +use move_binary_format::file_format::CompiledModule; +use move_vm_config::verifier::VerifierConfig; +use sui_types::{error::ExecutionError, move_package::FnInfoMap}; + +use crate::{ + entry_points_verifier, global_storage_access_verifier, id_leak_verifier, + one_time_witness_verifier, private_generics, private_generics_verifier_v2, + struct_with_key_verifier, +}; +use move_bytecode_verifier_meter::Meter; +use move_bytecode_verifier_meter::dummy::DummyMeter; + +/// Helper for a "canonical" verification of a module. +pub fn sui_verify_module_metered( + module: &CompiledModule, + fn_info_map: &FnInfoMap, + meter: &mut (impl Meter + ?Sized), + verifier_config: &VerifierConfig, +) -> Result<(), ExecutionError> { + struct_with_key_verifier::verify_module(module)?; + global_storage_access_verifier::verify_module(module)?; + id_leak_verifier::verify_module(module, meter)?; + if verifier_config.private_generics_verifier_v2 { + private_generics_verifier_v2::verify_module(module, verifier_config)?; + } else { + private_generics::verify_module(module, verifier_config)?; + } + entry_points_verifier::verify_module(module, fn_info_map, verifier_config)?; + one_time_witness_verifier::verify_module(module, fn_info_map) +} + +/// Runs the Sui verifier and checks if the error counts as a Sui verifier timeout +/// NOTE: this function only check if the verifier error is a timeout +/// All other errors are ignored +pub fn sui_verify_module_metered_check_timeout_only( + module: &CompiledModule, + fn_info_map: &FnInfoMap, + meter: &mut (impl Meter + ?Sized), + verifier_config: &VerifierConfig, +) -> Result<(), ExecutionError> { + // Checks if the error counts as a Sui verifier timeout + if let Err(error) = sui_verify_module_metered(module, fn_info_map, meter, verifier_config) + && matches!( + error.kind(), + sui_types::execution_status::ExecutionFailureStatus::SuiMoveVerificationTimedout + ) + { + return Err(error); + } + // Any other scenario, including a non-timeout error counts as Ok + Ok(()) +} + +pub fn sui_verify_module_unmetered( + module: &CompiledModule, + fn_info_map: &FnInfoMap, + verifier_config: &VerifierConfig, +) -> Result<(), ExecutionError> { + sui_verify_module_metered(module, fn_info_map, &mut DummyMeter, verifier_config).inspect_err( + |err| { + // We must never see timeout error in execution + debug_assert!( + !matches!( + err.kind(), + sui_types::execution_status::ExecutionFailureStatus::SuiMoveVerificationTimedout + ), + "Unexpected timeout error in execution" + ); + }, + ) +} diff --git a/sui-execution/replay_cut/sui-verifier/tests/common/mod.rs b/sui-execution/replay_cut/sui-verifier/tests/common/mod.rs new file mode 100644 index 0000000000000..bff241775f2d5 --- /dev/null +++ b/sui-execution/replay_cut/sui-verifier/tests/common/mod.rs @@ -0,0 +1,5 @@ +// Copyright (c) 2021, Facebook, Inc. and its affiliates +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +pub mod module_builder; diff --git a/sui-execution/replay_cut/sui-verifier/tests/common/module_builder.rs b/sui-execution/replay_cut/sui-verifier/tests/common/module_builder.rs new file mode 100644 index 0000000000000..a8d2cde27d961 --- /dev/null +++ b/sui-execution/replay_cut/sui-verifier/tests/common/module_builder.rs @@ -0,0 +1,271 @@ +// Copyright (c) 2021, Facebook, Inc. and its affiliates +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use move_binary_format::file_format::*; +use move_core_types::{account_address::AccountAddress, identifier::Identifier}; +use sui_types::SUI_FRAMEWORK_ADDRESS; + +pub struct ModuleBuilder { + module: CompiledModule, +} + +pub struct StructInfo { + pub handle: DatatypeHandleIndex, + pub def: StructDefinitionIndex, + pub fields: Vec, +} + +pub struct FuncInfo { + pub handle: FunctionHandleIndex, + pub def: FunctionDefinitionIndex, +} + +pub struct GenericFuncInfo { + pub handle: FunctionInstantiationIndex, + pub def: FunctionDefinitionIndex, +} + +impl ModuleBuilder { + pub fn new(address: AccountAddress, name: &str) -> Self { + Self { + module: CompiledModule { + version: move_binary_format::file_format_common::VERSION_MAX, + module_handles: vec![ModuleHandle { + address: AddressIdentifierIndex(0), + name: IdentifierIndex(0), + }], + self_module_handle_idx: ModuleHandleIndex(0), + identifiers: vec![Identifier::new(name).unwrap()], + address_identifiers: vec![address], + struct_handles: vec![], + struct_defs: vec![], + function_handles: vec![], + function_defs: vec![], + signatures: vec![ + Signature(vec![]), // void + ], + constant_pool: vec![], + field_handles: vec![], + friend_decls: vec![], + struct_def_instantiations: vec![], + function_instantiations: vec![], + field_instantiations: vec![], + }, + } + } + + /// Creates the "object" module in framework address, along with the "Info" struct. + /// Both the module and the Info struct information are returned. + pub fn default() -> (Self, StructInfo) { + let mut module = Self::new(SUI_FRAMEWORK_ADDRESS, OBJECT_MODULE_NAME); + let id = module.add_struct( + module.get_self_index(), + INFO_STRUCT_NAME, + AbilitySet::EMPTY | Ability::Store | Ability::Drop, + vec![], + ); + (module, id) + } + + pub fn get_module(&self) -> &CompiledModule { + &self.module + } + + pub fn get_self_index(&self) -> ModuleHandleIndex { + self.module.self_module_handle_idx + } + + pub fn add_function_verbose( + &mut self, + module_idx: ModuleHandleIndex, + name: &str, + parameters: Vec, + ret: Vec, + type_parameters: Vec, + visibility: Visibility, + code_unit: CodeUnit, + ) -> FuncInfo { + let new_handle = FunctionHandle { + module: module_idx, + name: self.add_identifier(name), + parameters: self.add_signature(parameters), + return_: self.add_signature(ret), + type_parameters, + }; + let handle_idx = FunctionHandleIndex(self.module.function_handles.len() as u16); + self.module.function_handles.push(new_handle); + let new_def = FunctionDefinition { + function: handle_idx, + visibility, + acquires_global_resources: vec![], + code: Some(code_unit), + }; + self.module.function_defs.push(new_def); + FuncInfo { + handle: handle_idx, + def: FunctionDefinitionIndex((self.module.function_defs.len() - 1) as u16), + } + } + + pub fn add_function( + &mut self, + module_idx: ModuleHandleIndex, + name: &str, + parameters: Vec, + ret: Vec, + ) -> FuncInfo { + self.add_function_verbose( + module_idx, + name, + parameters, + ret, + vec![], + Visibility::Public, + CodeUnit { + locals: SignatureIndex(0), + code: vec![Bytecode::Ret], + }, + ) + } + + pub fn add_generic_function( + &mut self, + module_idx: ModuleHandleIndex, + name: &str, + type_parameters: Vec, + parameters: Vec, + ret: Vec, + ) -> GenericFuncInfo { + let func_info = self.add_function(module_idx, name, parameters, ret); + let sig = self.add_signature(type_parameters); + let handle_idx = + FunctionInstantiationIndex(self.module.function_instantiations.len() as u16); + self.module + .function_instantiations + .push(FunctionInstantiation { + handle: func_info.handle, + type_parameters: sig, + }); + GenericFuncInfo { + handle: handle_idx, + def: func_info.def, + } + } + + pub fn add_struct_verbose( + &mut self, + module_index: ModuleHandleIndex, + name: &str, + abilities: AbilitySet, + fields: Vec<(&str, SignatureToken)>, + type_parameters: Vec, + ) -> StructInfo { + let new_handle = DatatypeHandle { + module: module_index, + name: self.add_identifier(name), + abilities, + type_parameters, + }; + let handle_idx = DatatypeHandleIndex(self.module.struct_handles.len() as u16); + self.module.struct_handles.push(new_handle); + + let field_len = fields.len(); + let field_defs = fields + .into_iter() + .map(|(name, ty)| self.create_field(name, ty)) + .collect(); + let new_def = StructDefinition { + struct_handle: handle_idx, + field_information: StructFieldInformation::Declared(field_defs), + }; + let def_idx = StructDefinitionIndex(self.module.struct_defs.len() as u16); + self.module.struct_defs.push(new_def); + + let field_handles = (0..field_len) + .map(|idx| self.add_field_handle(def_idx, idx as u16)) + .collect(); + + StructInfo { + handle: handle_idx, + def: def_idx, + fields: field_handles, + } + } + + pub fn add_struct( + &mut self, + module_index: ModuleHandleIndex, + name: &str, + abilities: AbilitySet, + fields: Vec<(&str, SignatureToken)>, + ) -> StructInfo { + self.add_struct_verbose(module_index, name, abilities, fields, vec![]) + } + + pub fn add_module(&mut self, address: AccountAddress, name: &str) -> ModuleHandleIndex { + let handle = ModuleHandle { + address: self.add_address(address), + name: self.add_identifier(name), + }; + self.module.module_handles.push(handle); + ModuleHandleIndex((self.module.module_handles.len() - 1) as u16) + } + + fn create_field(&mut self, name: &str, ty: SignatureToken) -> FieldDefinition { + let id = self.add_identifier(name); + FieldDefinition { + name: id, + signature: TypeSignature(ty), + } + } + + pub fn set_bytecode(&mut self, func_def: FunctionDefinitionIndex, bytecode: Vec) { + let code = &mut self.module.function_defs[func_def.0 as usize] + .code + .as_mut() + .unwrap() + .code; + *code = bytecode; + } + + pub fn add_field_instantiation( + &mut self, + handle: FieldHandleIndex, + type_params: Vec, + ) -> FieldInstantiationIndex { + let type_parameters = self.add_signature(type_params); + self.module.field_instantiations.push(FieldInstantiation { + handle, + type_parameters, + }); + FieldInstantiationIndex((self.module.field_instantiations.len() - 1) as u16) + } + + fn add_field_handle( + &mut self, + struct_def: StructDefinitionIndex, + field: u16, + ) -> FieldHandleIndex { + self.module.field_handles.push(FieldHandle { + owner: struct_def, + field, + }); + FieldHandleIndex((self.module.field_handles.len() - 1) as u16) + } + + fn add_identifier(&mut self, id: &str) -> IdentifierIndex { + self.module.identifiers.push(Identifier::new(id).unwrap()); + IdentifierIndex((self.module.identifiers.len() - 1) as u16) + } + + fn add_signature(&mut self, sig: Vec) -> SignatureIndex { + self.module.signatures.push(Signature(sig)); + SignatureIndex((self.module.signatures.len() - 1) as u16) + } + + fn add_address(&mut self, address: AccountAddress) -> AddressIdentifierIndex { + self.module.address_identifiers.push(address); + AddressIdentifierIndex((self.module.address_identifiers.len() - 1) as u16) + } +} diff --git a/sui-execution/src/latest.rs b/sui-execution/src/latest.rs index 62f76248762fb..c62c6aa98bd1c 100644 --- a/sui-execution/src/latest.rs +++ b/sui-execution/src/latest.rs @@ -4,10 +4,14 @@ use move_binary_format::CompiledModule; use move_trace_format::format::MoveTraceBuilder; use move_vm_config::verifier::{MeterConfig, VerifierConfig}; +use similar::TextDiff; use std::{cell::RefCell, rc::Rc, sync::Arc}; use sui_protocol_config::ProtocolConfig; +use sui_types::effects::TransactionEffectsAPI; use sui_types::execution::ExecutionTiming; use sui_types::execution_params::ExecutionOrEarlyError; +use sui_types::execution_status::{ExecutionFailureStatus, ExecutionStatus}; +use sui_types::gas::SuiGasStatusAPI; use sui_types::transaction::GasData; use sui_types::{ base_types::{SuiAddress, TxContext}, @@ -26,9 +30,7 @@ use sui_types::{ use move_bytecode_verifier_meter::Meter; use move_vm_runtime_latest::runtime::MoveRuntime; use sui_adapter_latest::adapter::{new_move_runtime, run_metered_move_bytecode_verifier}; -use sui_adapter_latest::execution_engine::{ - execute_genesis_state_update, execute_transaction_to_effects, -}; +use sui_adapter_latest::execution_engine::execute_transaction_to_effects; use sui_adapter_latest::type_layout_resolver::TypeLayoutResolver; use sui_move_natives_latest::all_natives; use sui_types::storage::BackingStore; @@ -38,7 +40,10 @@ use crate::executor; use crate::verifier; use sui_adapter_latest::execution_mode; -pub(crate) struct Executor(Arc); +pub(crate) struct Executor { + bella_ciao_vm: Arc, + current_main_runtime: Arc, +} pub(crate) struct Verifier<'m> { config: VerifierConfig, @@ -47,10 +52,18 @@ pub(crate) struct Verifier<'m> { impl Executor { pub(crate) fn new(protocol_config: &ProtocolConfig, silent: bool) -> Result { - Ok(Executor(Arc::new(new_move_runtime( + let bella_ciao_vm = Arc::new(new_move_runtime( all_natives(silent, protocol_config), protocol_config, - )?))) + )?); + let current_main_runtime = Arc::new(sui_adapter_replay_cut::adapter::new_move_vm( + sui_move_natives_replay_cut::all_natives(silent, protocol_config), + protocol_config, + )?); + Ok(Executor { + bella_ciao_vm, + current_main_runtime, + }) } } @@ -84,22 +97,93 @@ impl executor::Executor for Executor { Vec, Result<(), ExecutionError>, ) { - execute_transaction_to_effects::( - store, - input_objects, - gas, - gas_status, - transaction_kind, - transaction_signer, - transaction_digest, - &self.0, - epoch_id, - epoch_timestamp_ms, - protocol_config, - metrics, - enable_expensive_checks, - execution_params, - trace_builder_opt, + use sui_adapter_replay_cut as replay_cut; + let new_vm = protocol_config.get_use_vm_v2(); + let new_adapter = protocol_config.get_use_adapter_v2(); + + if !new_vm && !new_adapter { + // old vm + old adapter + let current_main = replay_cut::execution_engine::execute_transaction_to_effects::< + replay_cut::execution_mode::Normal, + >( + store, + input_objects.clone(), + gas.clone(), + gas_status.clone(), + transaction_kind.clone(), + transaction_signer, + transaction_digest, + &self.current_main_runtime, + epoch_id, + epoch_timestamp_ms, + protocol_config, + metrics.clone(), + enable_expensive_checks, + execution_params.clone(), + trace_builder_opt, + ); + return current_main; + } + + let mut ptb_v2_protocol_config = protocol_config.clone(); + ptb_v2_protocol_config.set_enable_ptb_execution_v2_for_testing(true); + + if !new_vm && new_adapter { + // old vm + new adapter + let (m_inner_temporary_store, m_sui_gas_status, m_transaction_effects, _, _) = + replay_cut::execution_engine::execute_transaction_to_effects::< + replay_cut::execution_mode::Normal, + >( + store, + input_objects.clone(), + gas.clone(), + gas_status.clone(), + transaction_kind.clone(), + transaction_signer, + transaction_digest, + &self.current_main_runtime, + epoch_id, + epoch_timestamp_ms, + &ptb_v2_protocol_config, + metrics.clone(), + enable_expensive_checks, + execution_params.clone(), + trace_builder_opt, + ); + return ( + m_inner_temporary_store, + m_sui_gas_status, + m_transaction_effects, + vec![], + Ok(()), + ); + } + + // New VM + New Adapter + let (b_inner_temporary_store, b_sui_gas_status, b_transaction_effects, _, _) = + execute_transaction_to_effects::( + store, + input_objects, + gas, + gas_status, + transaction_kind, + transaction_signer, + transaction_digest, + &self.bella_ciao_vm, + epoch_id, + epoch_timestamp_ms, + &ptb_v2_protocol_config, + metrics, + enable_expensive_checks, + execution_params, + trace_builder_opt, + ); + ( + b_inner_temporary_store, + b_sui_gas_status, + b_transaction_effects, + vec![], + Ok(()), ) } @@ -125,8 +209,11 @@ impl executor::Executor for Executor { TransactionEffects, Result, ExecutionError>, ) { + use sui_adapter_replay_cut as replay_cut; let (inner_temp_store, gas_status, effects, _timings, result) = if skip_all_checks { - execute_transaction_to_effects::>( + replay_cut::execution_engine::execute_transaction_to_effects::< + replay_cut::execution_mode::DevInspect, + >( store, input_objects, gas, @@ -134,7 +221,7 @@ impl executor::Executor for Executor { transaction_kind, transaction_signer, transaction_digest, - &self.0, + &self.current_main_runtime, epoch_id, epoch_timestamp_ms, protocol_config, @@ -144,7 +231,9 @@ impl executor::Executor for Executor { &mut None, ) } else { - execute_transaction_to_effects::>( + replay_cut::execution_engine::execute_transaction_to_effects::< + replay_cut::execution_mode::DevInspect, + >( store, input_objects, gas, @@ -152,7 +241,7 @@ impl executor::Executor for Executor { transaction_kind, transaction_signer, transaction_digest, - &self.0, + &self.current_main_runtime, epoch_id, epoch_timestamp_ms, protocol_config, @@ -176,6 +265,7 @@ impl executor::Executor for Executor { input_objects: CheckedInputObjects, pt: ProgrammableTransaction, ) -> Result { + use sui_adapter_replay_cut as replay_cut; let tx_context = TxContext::new_from_components( &SuiAddress::default(), transaction_digest, @@ -189,11 +279,11 @@ impl executor::Executor for Executor { protocol_config, ); let tx_context = Rc::new(RefCell::new(tx_context)); - execute_genesis_state_update( + replay_cut::execution_engine::execute_genesis_state_update( store, protocol_config, metrics, - &self.0, + &self.current_main_runtime, tx_context, input_objects, pt, @@ -204,7 +294,7 @@ impl executor::Executor for Executor { &'vm self, store: Box, ) -> Box { - Box::new(TypeLayoutResolver::new(&self.0, store)) + Box::new(TypeLayoutResolver::new(&self.bella_ciao_vm, store)) } } @@ -232,3 +322,75 @@ pub fn init_vm_for_msim() { use move_vm_runtime_latest::cache::identifier_interner; identifier_interner::init_interner(); } + +#[allow(unused)] +fn compare_effects( + normal_effects: &(InnerTemporaryStore, SuiGasStatus, TransactionEffects), + new_effects: &(InnerTemporaryStore, SuiGasStatus, TransactionEffects), +) { + 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, + }; + + // If you want to log gas usage differences, uncomment this line + // and add the gas row writing function from the other replay branch here: https://github.com/MystenLabs/sui/pull/24042/files#diff-2e9d962a08321605940b5a657135052fbcef87b5e360662bb527c96d9a615542 + // write_gas_row + // normal_effects.2.transaction_digest().to_string(), + // &new_effects.1.gas_usage_report(), + // &normal_effects.1.gas_usage_report(), + // ); + + // Probably want to only log this when they differ, but set to always log for now just for you + // to play with. + // if !ok { + if true { + 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<<<\n{:#?}\n{:#?}\n", + normal_effects.2.transaction_digest(), + s, + normal_effects.1.gas_usage_report(), + new_effects.1.gas_usage_report(), + ); + let output_file = format!("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() + ); + } +} diff --git a/sui-execution/src/lib.rs b/sui-execution/src/lib.rs index 4073217dd6f0c..db5b789505f8f 100644 --- a/sui-execution/src/lib.rs +++ b/sui-execution/src/lib.rs @@ -15,6 +15,7 @@ pub mod executor; pub mod verifier; mod latest; +mod replay_cut; mod v0; mod v1; mod v2; @@ -22,6 +23,7 @@ mod v2; #[cfg(test)] mod tests; +pub const REPLAY_CUT: u64 = u64::MAX; pub fn executor( protocol_config: &ProtocolConfig, silent: bool, @@ -36,6 +38,8 @@ pub fn executor( 3 => Arc::new(latest::Executor::new(protocol_config, silent)?), + REPLAY_CUT => Arc::new(replay_cut::Executor::new(protocol_config, silent)?), + v => panic!("Unsupported execution version {v}"), }) } @@ -52,6 +56,7 @@ pub fn verifier<'m>( 1 => Box::new(v1::Verifier::new(config, metrics)), 2 => Box::new(v2::Verifier::new(config, metrics)), 3 => Box::new(latest::Verifier::new(config, metrics)), + REPLAY_CUT => Box::new(replay_cut::Verifier::new(config, metrics)), v => panic!("Unsupported execution version {v}"), } } diff --git a/sui-execution/src/replay_cut.rs b/sui-execution/src/replay_cut.rs new file mode 100644 index 0000000000000..fb9e58f11deba --- /dev/null +++ b/sui-execution/src/replay_cut.rs @@ -0,0 +1,228 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use move_binary_format::CompiledModule; +use move_trace_format::format::MoveTraceBuilder; +use move_vm_config::verifier::{MeterConfig, VerifierConfig}; +use std::{cell::RefCell, rc::Rc, sync::Arc}; +use sui_protocol_config::ProtocolConfig; +use sui_types::execution::ExecutionTiming; +use sui_types::execution_params::ExecutionOrEarlyError; +use sui_types::transaction::GasData; +use sui_types::{ + base_types::{SuiAddress, TxContext}, + committee::EpochId, + digests::TransactionDigest, + effects::TransactionEffects, + error::{ExecutionError, SuiError, SuiResult}, + execution::{ExecutionResult, TypeLayoutStore}, + gas::SuiGasStatus, + inner_temporary_store::InnerTemporaryStore, + layout_resolver::LayoutResolver, + metrics::{BytecodeVerifierMetrics, LimitsMetrics}, + transaction::{CheckedInputObjects, ProgrammableTransaction, TransactionKind}, +}; + +use move_bytecode_verifier_meter::Meter; +use move_vm_runtime_replay_cut::move_vm::MoveVM; +use sui_adapter_replay_cut::adapter::{new_move_vm, run_metered_move_bytecode_verifier}; +use sui_adapter_replay_cut::execution_engine::{ + execute_genesis_state_update, execute_transaction_to_effects, +}; +use sui_adapter_replay_cut::type_layout_resolver::TypeLayoutResolver; +use sui_move_natives_replay_cut::all_natives; +use sui_types::storage::BackingStore; +use sui_verifier_replay_cut::meter::SuiVerifierMeter; + +use crate::executor; +use crate::verifier; +use sui_adapter_replay_cut::execution_mode; + +pub(crate) struct Executor(Arc); + +pub(crate) struct Verifier<'m> { + config: VerifierConfig, + metrics: &'m Arc, +} + +impl Executor { + pub(crate) fn new(protocol_config: &ProtocolConfig, silent: bool) -> Result { + Ok(Executor(Arc::new(new_move_vm( + all_natives(silent, protocol_config), + protocol_config, + )?))) + } +} + +impl<'m> Verifier<'m> { + pub(crate) fn new(config: VerifierConfig, metrics: &'m Arc) -> Self { + Verifier { config, metrics } + } +} + +impl executor::Executor for Executor { + fn execute_transaction_to_effects( + &self, + store: &dyn BackingStore, + protocol_config: &ProtocolConfig, + metrics: Arc, + enable_expensive_checks: bool, + execution_params: ExecutionOrEarlyError, + epoch_id: &EpochId, + epoch_timestamp_ms: u64, + input_objects: CheckedInputObjects, + gas: GasData, + gas_status: SuiGasStatus, + transaction_kind: TransactionKind, + transaction_signer: SuiAddress, + transaction_digest: TransactionDigest, + trace_builder_opt: &mut Option, + ) -> ( + InnerTemporaryStore, + SuiGasStatus, + TransactionEffects, + Vec, + Result<(), ExecutionError>, + ) { + execute_transaction_to_effects::( + store, + input_objects, + gas, + gas_status, + transaction_kind, + transaction_signer, + transaction_digest, + &self.0, + epoch_id, + epoch_timestamp_ms, + protocol_config, + metrics, + enable_expensive_checks, + execution_params, + trace_builder_opt, + ) + } + + fn dev_inspect_transaction( + &self, + store: &dyn BackingStore, + protocol_config: &ProtocolConfig, + metrics: Arc, + enable_expensive_checks: bool, + execution_params: ExecutionOrEarlyError, + epoch_id: &EpochId, + epoch_timestamp_ms: u64, + input_objects: CheckedInputObjects, + gas: GasData, + gas_status: SuiGasStatus, + transaction_kind: TransactionKind, + transaction_signer: SuiAddress, + transaction_digest: TransactionDigest, + skip_all_checks: bool, + ) -> ( + InnerTemporaryStore, + SuiGasStatus, + TransactionEffects, + Result, ExecutionError>, + ) { + let (inner_temp_store, gas_status, effects, _timings, result) = if skip_all_checks { + execute_transaction_to_effects::>( + store, + input_objects, + gas, + gas_status, + transaction_kind, + transaction_signer, + transaction_digest, + &self.0, + epoch_id, + epoch_timestamp_ms, + protocol_config, + metrics, + enable_expensive_checks, + execution_params, + &mut None, + ) + } else { + execute_transaction_to_effects::>( + store, + input_objects, + gas, + gas_status, + transaction_kind, + transaction_signer, + transaction_digest, + &self.0, + epoch_id, + epoch_timestamp_ms, + protocol_config, + metrics, + enable_expensive_checks, + execution_params, + &mut None, + ) + }; + (inner_temp_store, gas_status, effects, result) + } + + fn update_genesis_state( + &self, + store: &dyn BackingStore, + protocol_config: &ProtocolConfig, + metrics: Arc, + epoch_id: EpochId, + epoch_timestamp_ms: u64, + transaction_digest: &TransactionDigest, + input_objects: CheckedInputObjects, + pt: ProgrammableTransaction, + ) -> Result { + let tx_context = TxContext::new_from_components( + &SuiAddress::default(), + transaction_digest, + &epoch_id, + epoch_timestamp_ms, + // genesis transaction: RGP: 1, budget: 1M, sponsor: None + 1, + 1, + 1_000_000, + None, + protocol_config, + ); + let tx_context = Rc::new(RefCell::new(tx_context)); + execute_genesis_state_update( + store, + protocol_config, + metrics, + &self.0, + tx_context, + input_objects, + pt, + ) + } + + fn type_layout_resolver<'r, 'vm: 'r, 'store: 'r>( + &'vm self, + store: Box, + ) -> Box { + Box::new(TypeLayoutResolver::new(&self.0, store)) + } +} + +impl verifier::Verifier for Verifier<'_> { + fn meter(&self, config: MeterConfig) -> Box { + Box::new(SuiVerifierMeter::new(config)) + } + + fn override_deprecate_global_storage_ops_during_deserialization(&self) -> Option { + Some(true) + } + + fn meter_compiled_modules( + &mut self, + _protocol_config: &ProtocolConfig, + modules: &[CompiledModule], + meter: &mut dyn Meter, + ) -> SuiResult<()> { + run_metered_move_bytecode_verifier(modules, &self.config, meter, self.metrics) + } +} diff --git a/sui-execution/src/tests.rs b/sui-execution/src/tests.rs index fa28c77cbb967..39598b3cbbb0e 100644 --- a/sui-execution/src/tests.rs +++ b/sui-execution/src/tests.rs @@ -141,3 +141,61 @@ impl Packages { self.normal_edges(pkg).map(move |(_, to)| to) } } + +#[cfg(test)] +mod adapter_vm_v2_tests { + use sui_protocol_config::ProtocolConfig; + + #[test] + fn test_executor_with_old_adapter_old_vm() { + let protocol_config = ProtocolConfig::get_for_max_version_UNSAFE(); + + // By default both adapter v2 and vm v2 are disabled. + + let executor_result = crate::executor(&protocol_config, true); + + assert!( + executor_result.is_ok(), + "Executor should be created successfully with VM v2 enabled" + ); + + assert!(!protocol_config.get_use_adapter_v2()); + assert!(!protocol_config.get_use_vm_v2()); + } + + #[test] + fn test_executor_with_new_adapter_old_vm() { + let mut protocol_config = ProtocolConfig::get_for_max_version_UNSAFE(); + + // Enable Adapter v2 (old vm + new adapter) + protocol_config.set_enable_adapter_v2_for_testing(true); + + let executor_result = crate::executor(&protocol_config, true); + + assert!( + executor_result.is_ok(), + "Executor should be created successfully with VM v2 enabled" + ); + + assert!(protocol_config.get_use_adapter_v2()); + assert!(!protocol_config.get_use_vm_v2()); + } + #[test] + fn test_executor_with_new_adapter_new_vm() { + let mut protocol_config = ProtocolConfig::get_for_max_version_UNSAFE(); + + // Enable Adapter v2 (new vm + new adapter) + protocol_config.set_enable_adapter_v2_for_testing(true); + protocol_config.set_enable_vm_v2_for_testing(true); + + let executor_result = crate::executor(&protocol_config, true); + + assert!( + executor_result.is_ok(), + "Executor should be created successfully with VM v2 enabled" + ); + + assert!(protocol_config.get_use_adapter_v2()); + assert!(protocol_config.get_use_vm_v2()); + } +}