diff --git a/Cargo.lock b/Cargo.lock index fce60843f7..64e905e9f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -723,19 +723,19 @@ dependencies = [ "rustversion", "serde 1.0.210", "sync_wrapper 0.1.2", - "tower", + "tower 0.4.13", "tower-layer", "tower-service", ] [[package]] name = "axum" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +checksum = "8f43644eed690f5374f1af436ecd6aea01cd201f6fbdf0178adaf6907afb2cec" dependencies = [ "async-trait", - "axum-core 0.4.3", + "axum-core 0.4.4", "base64 0.21.7", "bytes", "futures-util", @@ -758,8 +758,8 @@ dependencies = [ "sha1", "sync_wrapper 1.0.1", "tokio", - "tokio-tungstenite 0.21.0", - "tower", + "tokio-tungstenite 0.23.1", + "tower 0.5.1", "tower-layer", "tower-service", "tracing", @@ -784,9 +784,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +checksum = "5e6b8ba012a258d63c9adfa28b9ddcf66149da6f986c5b5452e629d5ee64bf00" dependencies = [ "async-trait", "bytes", @@ -797,7 +797,7 @@ dependencies = [ "mime", "pin-project-lite", "rustversion", - "sync_wrapper 0.1.2", + "sync_wrapper 1.0.1", "tower-layer", "tower-service", "tracing", @@ -822,7 +822,7 @@ dependencies = [ "rustls-pemfile 2.1.2", "tokio", "tokio-rustls 0.24.1", - "tower", + "tower 0.4.13", "tower-service", ] @@ -1103,7 +1103,7 @@ name = "bitcoin-move" version = "0.7.2" dependencies = [ "anyhow", - "axum 0.7.5", + "axum 0.7.6", "bitcoin 0.32.2", "brotli 3.5.0", "hex", @@ -3425,6 +3425,15 @@ dependencies = [ "regex", ] +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "log", +] + [[package]] name = "env_logger" version = "0.11.5" @@ -4640,6 +4649,20 @@ dependencies = [ "crunchy", ] +[[package]] +name = "handlebars" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faa67bab9ff362228eb3d00bd024a4965d8231bbb7921167f0cfa66c6626b225" +dependencies = [ + "log", + "pest", + "pest_derive", + "serde 1.0.210", + "serde_json", + "thiserror", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -5005,7 +5028,7 @@ dependencies = [ "pin-project-lite", "socket2", "tokio", - "tower", + "tower 0.4.13", "tower-service", "tracing", ] @@ -5194,6 +5217,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "321f0f839cd44a4686e9504b0a62b4d69a50b62072144c71c68f5873c167b8d9" dependencies = [ "ahash 0.8.11", + "clap 4.5.17", + "crossbeam-channel", + "crossbeam-utils", + "dashmap 5.5.3", + "env_logger 0.10.2", "indexmap 2.2.6", "is-terminal", "itoa", @@ -5562,7 +5590,7 @@ dependencies = [ "serde_json", "thiserror", "tokio", - "tower", + "tower 0.4.13", "tracing", "url", ] @@ -5587,7 +5615,7 @@ dependencies = [ "serde_json", "thiserror", "tokio", - "tower", + "tower 0.4.13", "tracing", "url", ] @@ -5642,7 +5670,7 @@ dependencies = [ "tokio", "tokio-stream", "tokio-util", - "tower", + "tower 0.4.13", "tracing", ] @@ -6123,7 +6151,7 @@ version = "0.7.2" dependencies = [ "anyhow", "async-trait", - "axum 0.7.5", + "axum 0.7.6", "axum-server", "dashmap 6.0.1", "futures", @@ -6934,7 +6962,9 @@ dependencies = [ "move-transactional-test-runner", "move-vm-runtime", "move-vm-types", + "moveos-common", "moveos-eventbus", + "moveos-gas-profiling", "moveos-object-runtime", "moveos-stdlib", "moveos-store", @@ -6959,6 +6989,9 @@ dependencies = [ "itertools 0.13.0", "libc", "log", + "move-binary-format", + "move-core-types", + "move-vm-types", "serde 1.0.210", ] @@ -6994,6 +7027,23 @@ dependencies = [ "tokio", ] +[[package]] +name = "moveos-gas-profiling" +version = "0.7.2" +dependencies = [ + "anyhow", + "handlebars", + "inferno", + "move-binary-format", + "move-core-types", + "move-vm-types", + "moveos-common", + "moveos-types", + "regex", + "serde_json", + "smallvec", +] + [[package]] name = "moveos-object-runtime" version = "0.7.2" @@ -9377,6 +9427,7 @@ dependencies = [ "moveos-common", "moveos-compiler", "moveos-config", + "moveos-gas-profiling", "moveos-object-runtime", "moveos-stdlib", "moveos-store", @@ -9558,6 +9609,7 @@ dependencies = [ "async-trait", "coerce", "function_name", + "hex", "log", "metrics", "move-core-types", @@ -9581,7 +9633,7 @@ name = "rooch-faucet" version = "0.7.2" dependencies = [ "anyhow", - "axum 0.7.5", + "axum 0.7.6", "axum-server", "clap 4.5.17", "futures", @@ -9596,7 +9648,7 @@ dependencies = [ "serenity", "thiserror", "tokio", - "tower", + "tower 0.4.13", "tower-http", "tracing", "tracing-subscriber", @@ -10030,7 +10082,7 @@ name = "rooch-rpc-server" version = "0.7.2" dependencies = [ "anyhow", - "axum 0.7.5", + "axum 0.7.6", "bcs", "bitcoincore-rpc", "coerce", @@ -10068,7 +10120,7 @@ dependencies = [ "serde_json", "tokio", "tokio-util", - "tower", + "tower 0.4.13", "tower-http", "tower_governor", "tracing", @@ -11897,7 +11949,7 @@ dependencies = [ "backtrace", "clap 4.5.17", "cucumber", - "env_logger", + "env_logger 0.11.5", "futures", "hex", "hmac", @@ -11929,18 +11981,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", @@ -12341,7 +12393,7 @@ dependencies = [ "tokio", "tokio-stream", "tokio-util", - "tower", + "tower 0.4.13", "tower-layer", "tower-service", "tracing", @@ -12369,6 +12421,22 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 0.1.2", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower-http" version = "0.5.2" @@ -12393,7 +12461,7 @@ dependencies = [ "pin-project-lite", "tokio", "tokio-util", - "tower", + "tower 0.4.13", "tower-layer", "tower-service", "tracing", @@ -12402,15 +12470,15 @@ dependencies = [ [[package]] name = "tower-layer" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tower_governor" @@ -12418,13 +12486,13 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "313fa625fea5790ed56360a30ea980e41229cf482b4835801a67ef1922bf63b9" dependencies = [ - "axum 0.7.5", + "axum 0.7.6", "forwarded-header-value", "governor", "http 1.1.0", "pin-project", "thiserror", - "tower", + "tower 0.4.13", "tracing", ] diff --git a/Cargo.toml b/Cargo.toml index c192d7a19f..7da3650a0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "moveos/moveos-commons/bcs_ext", "moveos/moveos-commons/moveos-common", "moveos/moveos-commons/timeout-join-handler", + "moveos/moveos-commons/moveos-common", "moveos/moveos-compiler", "moveos/moveos-config", "moveos/moveos-object-runtime", @@ -18,6 +19,7 @@ members = [ "moveos/raw-store", "moveos/smt", "moveos/moveos-eventbus", + "moveos/moveos-gas-profiling", "crates/data_verify", "crates/rooch", "crates/rooch-benchmarks", @@ -103,6 +105,7 @@ moveos-object-runtime = { path = "moveos/moveos-object-runtime" } moveos-compiler = { path = "moveos/moveos-compiler" } moveos-eventbus = { path = "moveos/moveos-eventbus" } accumulator = { path = "moveos/moveos-commons/accumulator" } +moveos-gas-profiling = { path = "moveos/moveos-gas-profiling" } # crates for Rooch rooch = { path = "crates/rooch" } @@ -217,7 +220,7 @@ strum_macros = "^0.26" sha2 = "0.10.2" sha3 = "0.10.8" smallvec = "1.6.1" -thiserror = "1.0.63" +thiserror = "1.0.64" tiny-keccak = { version = "2", features = ["keccak", "sha3"] } tiny-bip39 = "1.0.0" tokio = { version = "1.40.0", features = ["full"] } @@ -283,7 +286,7 @@ diesel = { version = "2.2.4", features = [ ] } diesel-derive-enum = { version = "2.1.0", features = ["sqlite"] } diesel_migrations = { version = "2.2.0" } -axum = { version = "0.7.5", default-features = false, features = [ +axum = { version = "0.7.6", default-features = false, features = [ "tokio", "http1", "http2", @@ -347,6 +350,9 @@ vergen-git2 = { version = "1.0.0", features = ["build", "cargo", "rustc"] } vergen-pretty = "0.3.5" crossbeam-channel = "0.5.13" +inferno = "0.11.14" +handlebars = "4.2.2" + # Note: the BEGIN and END comments below are required for external tooling. Do not remove. # BEGIN MOVE DEPENDENCIES move-abigen = { git = "https://github.com/rooch-network/move", rev = "c05b5f71a24beb498eb743d73be4f69d8d325e62" } diff --git a/crates/rooch-executor/Cargo.toml b/crates/rooch-executor/Cargo.toml index 33d1a84186..e41dfdd3be 100644 --- a/crates/rooch-executor/Cargo.toml +++ b/crates/rooch-executor/Cargo.toml @@ -35,3 +35,4 @@ rooch-types = { workspace = true } rooch-genesis = { workspace = true } rooch-event = { workspace = true } rooch-store = { workspace = true } +hex = "0.4.3" diff --git a/crates/rooch-executor/src/actor/messages.rs b/crates/rooch-executor/src/actor/messages.rs index 2e85ac7ec9..f0e6f3311e 100644 --- a/crates/rooch-executor/src/actor/messages.rs +++ b/crates/rooch-executor/src/actor/messages.rs @@ -99,6 +99,7 @@ impl Message for ExecuteViewFunctionMessage { #[derive(Debug, Serialize, Deserialize)] pub struct StatesMessage { + pub state_root: Option, pub access_path: AccessPath, } @@ -126,6 +127,7 @@ impl Message for AnnotatedStatesMessage { #[derive(Debug, Serialize, Deserialize)] pub struct ListStatesMessage { + pub state_root: Option, pub access_path: AccessPath, pub cursor: Option, pub limit: usize, diff --git a/crates/rooch-executor/src/actor/reader_executor.rs b/crates/rooch-executor/src/actor/reader_executor.rs index fc67b2bc5c..06d27dd7e5 100644 --- a/crates/rooch-executor/src/actor/reader_executor.rs +++ b/crates/rooch-executor/src/actor/reader_executor.rs @@ -146,7 +146,12 @@ impl Handler for ReaderExecutorActor { msg: StatesMessage, _ctx: &mut ActorContext, ) -> Result>, anyhow::Error> { - let resolver = RootObjectResolver::new(self.root.clone(), &self.moveos_store); + let resolver = if let Some(state_root) = msg.state_root { + let root_object_meta = ObjectMeta::root_metadata(state_root, 0); + RootObjectResolver::new(root_object_meta, &self.moveos_store) + } else { + RootObjectResolver::new(self.root.clone(), &self.moveos_store) + }; resolver.get_states(msg.access_path) } } @@ -170,7 +175,12 @@ impl Handler for ReaderExecutorActor { msg: ListStatesMessage, _ctx: &mut ActorContext, ) -> Result, anyhow::Error> { - let resolver = RootObjectResolver::new(self.root.clone(), &self.moveos_store); + let resolver = if let Some(state_root) = msg.state_root { + let root_object_meta = ObjectMeta::root_metadata(state_root, 0); + RootObjectResolver::new(root_object_meta, &self.moveos_store) + } else { + RootObjectResolver::new(self.root.clone(), &self.moveos_store) + }; resolver.list_states(msg.access_path, msg.cursor, msg.limit) } } diff --git a/crates/rooch-executor/src/proxy/mod.rs b/crates/rooch-executor/src/proxy/mod.rs index e88cd073b4..7703a256f8 100644 --- a/crates/rooch-executor/src/proxy/mod.rs +++ b/crates/rooch-executor/src/proxy/mod.rs @@ -116,9 +116,16 @@ impl ExecutorProxy { .await? } - pub async fn get_states(&self, access_path: AccessPath) -> Result>> { + pub async fn get_states( + &self, + access_path: AccessPath, + state_root: Option, + ) -> Result>> { self.reader_actor - .send(StatesMessage { access_path }) + .send(StatesMessage { + state_root, + access_path, + }) .await? } @@ -133,12 +140,14 @@ impl ExecutorProxy { pub async fn list_states( &self, + state_root: Option, access_path: AccessPath, cursor: Option, limit: usize, ) -> Result> { self.reader_actor .send(ListStatesMessage { + state_root, access_path, cursor, limit, @@ -261,7 +270,7 @@ impl ExecutorProxy { } pub async fn chain_id(&self) -> Result { - self.get_states(AccessPath::object(ChainID::chain_id_object_id())) + self.get_states(AccessPath::object(ChainID::chain_id_object_id()), None) .await? .into_iter() .next() @@ -271,7 +280,7 @@ impl ExecutorProxy { } pub async fn bitcoin_network(&self) -> Result { - self.get_states(AccessPath::object(BitcoinNetwork::object_id())) + self.get_states(AccessPath::object(BitcoinNetwork::object_id()), None) .await? .into_iter() .next() @@ -283,7 +292,10 @@ impl ExecutorProxy { //TODO provide a trait to abstract the async state reader, elemiate the duplicated code bwteen RpcService and Client pub async fn get_sequence_number(&self, address: AccountAddress) -> Result { Ok(self - .get_states(AccessPath::object(Account::account_object_id(address))) + .get_states( + AccessPath::object(Account::account_object_id(address)), + None, + ) .await? .pop() .flatten() diff --git a/crates/rooch-open-rpc-spec/schemas/openrpc.json b/crates/rooch-open-rpc-spec/schemas/openrpc.json index 79ce1698b3..61d2240a11 100644 --- a/crates/rooch-open-rpc-spec/schemas/openrpc.json +++ b/crates/rooch-open-rpc-spec/schemas/openrpc.json @@ -2827,6 +2827,18 @@ "description": "If true, result with display rendered is returned", "default": false, "type": "boolean" + }, + "stateRoot": { + "description": "The state root of remote stateDB", + "default": null, + "anyOf": [ + { + "$ref": "#/components/schemas/primitive_types::H256" + }, + { + "type": "null" + } + ] } } }, diff --git a/crates/rooch-rpc-api/src/jsonrpc_types/rpc_options.rs b/crates/rooch-rpc-api/src/jsonrpc_types/rpc_options.rs index 04f239b762..618025c65f 100644 --- a/crates/rooch-rpc-api/src/jsonrpc_types/rpc_options.rs +++ b/crates/rooch-rpc-api/src/jsonrpc_types/rpc_options.rs @@ -1,6 +1,8 @@ // Copyright (c) RoochNetwork // SPDX-License-Identifier: Apache-2.0 +use crate::jsonrpc_types::H256View; +use moveos_types::h256::H256; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -11,6 +13,8 @@ pub struct StateOptions { pub decode: bool, /// If true, result with display rendered is returned pub show_display: bool, + /// The state root of remote stateDB + pub state_root: Option, } impl StateOptions { @@ -27,6 +31,16 @@ impl StateOptions { self.show_display = show_display; self } + + pub fn state_root(mut self, state_root: Option) -> Self { + match state_root { + None => {} + Some(h256) => { + self.state_root = Some(H256View::from(h256)); + } + } + self + } } #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Eq, PartialEq, Default)] diff --git a/crates/rooch-rpc-client/src/lib.rs b/crates/rooch-rpc-client/src/lib.rs index 1771a16d78..95b9ebfde7 100644 --- a/crates/rooch-rpc-client/src/lib.rs +++ b/crates/rooch-rpc-client/src/lib.rs @@ -1,16 +1,21 @@ // Copyright (c) RoochNetwork // SPDX-License-Identifier: Apache-2.0 -use anyhow::Result; +use anyhow::{ensure, Error, Result}; use jsonrpsee::core::client::ClientT; use jsonrpsee::http_client::{HttpClient, HttpClientBuilder}; -use move_core_types::language_storage::ModuleId; +use move_core_types::account_address::AccountAddress; +use move_core_types::language_storage::{ModuleId, StructTag}; use move_core_types::metadata::Metadata; -use move_core_types::resolver::ModuleResolver; +use move_core_types::resolver::{ModuleResolver, ResourceResolver}; use moveos_types::access_path::AccessPath; +use moveos_types::h256::H256; use moveos_types::move_std::string::MoveString; +use moveos_types::moveos_std::account::Account; use moveos_types::moveos_std::move_module::MoveModule; -use moveos_types::state::ObjectState; +use moveos_types::moveos_std::object::{ObjectID, ObjectMeta, RawField}; +use moveos_types::state::{FieldKey, MoveType, ObjectState}; +use moveos_types::state_resolver::{StateKV, StateResolver, StatelessResolver}; use moveos_types::{ function_return_value::FunctionResult, module_binding::MoveFunctionCaller, moveos_std::tx_context::TxContext, transaction::FunctionCall, @@ -109,7 +114,7 @@ impl ModuleResolver for &Client { fn get_module(&self, id: &ModuleId) -> Result>> { tokio::task::block_in_place(|| { Handle::current().block_on(async { - let mut states = self.rooch.get_states(AccessPath::module(id)).await?; + let mut states = self.rooch.get_states(AccessPath::module(id), None).await?; states .pop() .flatten() @@ -123,3 +128,111 @@ impl ModuleResolver for &Client { }) } } + +#[derive(Clone)] +pub struct ClientResolver { + root: ObjectMeta, + client: Client, +} + +impl ClientResolver { + pub fn new(client: Client, root: ObjectMeta) -> Self { + Self { root, client } + } +} + +impl ResourceResolver for ClientResolver { + fn get_resource_with_metadata( + &self, + address: &AccountAddress, + resource_tag: &StructTag, + _metadata: &[Metadata], + ) -> std::result::Result<(Option>, usize), Error> { + let account_object_id = Account::account_object_id(*address); + + let key = FieldKey::derive_resource_key(resource_tag); + let result = self + .get_field(&account_object_id, &key)? + .map(|s| { + ensure!( + s.match_dynamic_field_type(MoveString::type_tag(), resource_tag.clone().into()), + "Resource type mismatch, expected field value type: {:?}, actual: {:?}", + resource_tag, + s.object_type() + ); + let field = RawField::parse_resource_field(&s.value, resource_tag.clone().into())?; + Ok(field.value) + }) + .transpose(); + + match result { + Ok(opt) => { + if let Some(data) = opt { + Ok((Some(data), 0)) + } else { + Ok((None, 0)) + } + } + Err(err) => Err(err), + } + } +} + +impl ModuleResolver for ClientResolver { + fn get_module_metadata(&self, _module_id: &ModuleId) -> Vec { + vec![] + } + + fn get_module(&self, id: &ModuleId) -> std::result::Result>, Error> { + (&self.client).get_module(id) + } +} + +impl StatelessResolver for ClientResolver { + fn get_field_at(&self, state_root: H256, key: &FieldKey) -> Result, Error> { + tokio::task::block_in_place(|| { + Handle::current().block_on(async { + let access_path = AccessPath::object(ObjectID::new(key.0)); + let mut object_state_view_list = self + .client + .rooch + .get_states(access_path, Some(state_root)) + .await?; + Ok(object_state_view_list.pop().flatten().map(|state_view| { + let v: ObjectState = state_view.into(); + v + })) + }) + }) + } + + fn list_fields_at( + &self, + state_root: H256, + cursor: Option, + limit: usize, + ) -> Result> { + tokio::task::block_in_place(|| { + Handle::current().block_on(async { + let object_id = ObjectID::new(state_root.0); + let field_cursor = cursor.map(|field_key| field_key.to_hex_literal()); + let fields_states = self + .client + .rooch + .list_field_states(object_id.into(), field_cursor, Some(limit as u64), None) + .await?; + Ok(fields_states + .data + .iter() + .map(|item| StateKV::from((item.field_key.into(), item.state.clone().into()))) + .collect()) + }) + }) + } +} + +impl StateResolver for ClientResolver { + fn root(&self) -> &ObjectMeta { + &self.root + } +} diff --git a/crates/rooch-rpc-client/src/rooch_client.rs b/crates/rooch-rpc-client/src/rooch_client.rs index cdd57c3d46..7ff545e7cf 100644 --- a/crates/rooch-rpc-client/src/rooch_client.rs +++ b/crates/rooch-rpc-client/src/rooch_client.rs @@ -92,19 +92,27 @@ impl RoochRpcClient { pub async fn get_states( &self, access_path: AccessPath, + state_root: Option, ) -> Result>> { - Ok(self.http.get_states(access_path.into(), None).await?) + Ok(self + .http + .get_states( + access_path.into(), + Some(StateOptions::new().state_root(state_root)), + ) + .await?) } pub async fn get_decoded_states( &self, access_path: AccessPath, + state_root: Option, ) -> Result>> { Ok(self .http .get_states( access_path.into(), - Some(StateOptions::default().decode(true)), + Some(StateOptions::default().decode(true).state_root(state_root)), ) .await?) } @@ -168,9 +176,10 @@ impl RoochRpcClient { pub async fn get_sequence_number(&self, sender: RoochAddress) -> Result { Ok(self - .get_states(AccessPath::object(Account::account_object_id( - sender.into(), - ))) + .get_states( + AccessPath::object(Account::account_object_id(sender.into())), + None, + ) .await? .pop() .flatten() @@ -375,7 +384,7 @@ impl RoochRpcClient { account: RoochAddress, ) -> Result> { let access_path = AccessPath::resource(account.into(), T::struct_tag()); - let mut states = self.get_states(access_path).await?; + let mut states = self.get_states(access_path, None).await?; let state = states.pop().flatten(); if let Some(state) = state { let state = ObjectState::from(state); diff --git a/crates/rooch-rpc-server/src/server/rooch_server.rs b/crates/rooch-rpc-server/src/server/rooch_server.rs index db24f57402..6e31ea1100 100644 --- a/crates/rooch-rpc-server/src/server/rooch_server.rs +++ b/crates/rooch-rpc-server/src/server/rooch_server.rs @@ -202,6 +202,8 @@ impl RoochAPIServer for RoochServer { let show_display = state_option.show_display && (access_path.0.is_object() || access_path.0.is_resource()); + let state_root = state_option.state_root.map(|h256_view| h256_view.0); + let state_views = if state_option.decode || show_display { let states = self .rpc_service @@ -212,7 +214,7 @@ impl RoochAPIServer for RoochServer { let valid_states = states.iter().filter_map(|s| s.as_ref()).collect::>(); let mut valid_display_field_views = self .rpc_service - .get_display_fields_and_render(valid_states.as_slice()) + .get_display_fields_and_render(valid_states.as_slice(), state_root) .await?; valid_display_field_views.reverse(); states @@ -236,7 +238,7 @@ impl RoochAPIServer for RoochServer { } } else { self.rpc_service - .get_states(access_path.into()) + .get_states(access_path.into(), state_root) .await? .into_iter() .map(|s| s.map(ObjectStateView::from)) @@ -256,6 +258,8 @@ impl RoochAPIServer for RoochServer { let show_display = state_option.show_display && (access_path.0.is_object() || access_path.0.is_resource()); + let state_root = state_option.state_root.map(|h256_view| h256_view.0); + let limit_of = min( limit.map(Into::into).unwrap_or(DEFAULT_RESULT_LIMIT_USIZE), MAX_RESULT_LIMIT_USIZE, @@ -275,7 +279,7 @@ impl RoochAPIServer for RoochServer { if show_display { let display_field_views = self .rpc_service - .get_display_fields_and_render(state_refs.as_slice()) + .get_display_fields_and_render(state_refs.as_slice(), state_root) .await?; key_states .into_iter() @@ -297,7 +301,7 @@ impl RoochAPIServer for RoochServer { } } else { self.rpc_service - .list_states(access_path.into(), cursor_of, limit_of + 1) + .list_states(state_root, access_path.into(), cursor_of, limit_of + 1) .await? .into_iter() .map(|(key, state)| StateKVView::new(key.into(), state.into())) @@ -335,7 +339,7 @@ impl RoochAPIServer for RoochServer { let mut valid_display_field_views = if show_display { let valid_states = states.iter().filter_map(|s| s.as_ref()).collect::>(); self.rpc_service - .get_display_fields_and_render(valid_states.as_slice()) + .get_display_fields_and_render(valid_states.as_slice(), None) .await? } else { vec![] @@ -368,7 +372,7 @@ impl RoochAPIServer for RoochServer { } } else { self.rpc_service - .get_states(access_path) + .get_states(access_path, None) .await? .into_iter() .map(|s| s.map(Into::into)) @@ -621,7 +625,7 @@ impl RoochAPIServer for RoochServer { let access_path = AccessPath::module(&module_id); let module = self .rpc_service - .get_states(access_path) + .get_states(access_path, None) .await? .pop() .flatten(); diff --git a/crates/rooch-rpc-server/src/service/aggregate_service.rs b/crates/rooch-rpc-server/src/service/aggregate_service.rs index 7315bfcd5a..b9fb6f5367 100644 --- a/crates/rooch-rpc-server/src/service/aggregate_service.rs +++ b/crates/rooch-rpc-server/src/service/aggregate_service.rs @@ -45,7 +45,7 @@ impl AggregateService { .collect(), ); self.rpc_service - .get_states(access_path) + .get_states(access_path, None) .await? .into_iter() .zip(coin_types) @@ -72,7 +72,7 @@ impl AggregateService { ) -> Result>> { let access_path = AccessPath::objects(coin_store_ids); self.rpc_service - .get_states(access_path) + .get_states(access_path, None) .await? .into_iter() .map(|state_opt| state_opt.map(CoinStoreInfo::try_from).transpose()) diff --git a/crates/rooch-rpc-server/src/service/rpc_service.rs b/crates/rooch-rpc-server/src/service/rpc_service.rs index 57f3f9276e..5448eeecec 100644 --- a/crates/rooch-rpc-server/src/service/rpc_service.rs +++ b/crates/rooch-rpc-server/src/service/rpc_service.rs @@ -112,12 +112,18 @@ impl RpcService { Ok(resp) } - pub async fn get_states(&self, access_path: AccessPath) -> Result>> { - self.executor.get_states(access_path).await + pub async fn get_states( + &self, + access_path: AccessPath, + state_root: Option, + ) -> Result>> { + self.executor.get_states(access_path, state_root).await } pub async fn exists_module(&self, module_id: ModuleId) -> Result { - let mut resp = self.get_states(AccessPath::module(&module_id)).await?; + let mut resp = self + .get_states(AccessPath::module(&module_id), None) + .await?; Ok(resp.pop().flatten().is_some()) } @@ -130,11 +136,14 @@ impl RpcService { pub async fn list_states( &self, + state_root: Option, access_path: AccessPath, cursor: Option, limit: usize, ) -> Result> { - self.executor.list_states(access_path, cursor, limit).await + self.executor + .list_states(state_root, access_path, cursor, limit) + .await } pub async fn list_annotated_states( @@ -367,8 +376,9 @@ impl RpcService { .iter() .filter_map(|s| s.as_ref()) .collect::>(); - let valid_display_field_views = - self.get_display_fields_and_render(&valid_states).await?; + let valid_display_field_views = self + .get_display_fields_and_render(&valid_states, None) + .await?; valid_states .iter() .zip(valid_display_field_views) @@ -405,7 +415,7 @@ impl RpcService { } object_states } else { - let states = self.get_states(access_path).await?; + let states = self.get_states(access_path, None).await?; states .into_iter() .zip(indexer_ids) @@ -466,7 +476,7 @@ impl RpcService { let access_path = AccessPath::fields(mapping_object_id, owner_keys); let address_mapping = self - .get_states(access_path) + .get_states(access_path, None) .await? .into_iter() .zip(user_addresses) @@ -489,6 +499,7 @@ impl RpcService { pub async fn get_display_fields_and_render( &self, states: &[&AnnotatedState], + state_root: Option, ) -> Result>> { let mut display_ids = vec![]; let mut displayable_states = vec![]; @@ -504,7 +515,7 @@ impl RpcService { // get display fields let path = AccessPath::objects(display_ids); let mut display_fields = self - .get_states(path) + .get_states(path, state_root) .await? .into_iter() .map(|option_s| { @@ -596,7 +607,7 @@ impl RpcService { ) -> Result<()> { { let states = self - .get_states(AccessPath::objects(object_ids.clone())) + .get_states(AccessPath::objects(object_ids.clone()), None) .await?; let mut remove_object_ids = vec![]; diff --git a/crates/rooch/Cargo.toml b/crates/rooch/Cargo.toml index 7e26fa3170..452a9061eb 100644 --- a/crates/rooch/Cargo.toml +++ b/crates/rooch/Cargo.toml @@ -86,6 +86,7 @@ moveos-compiler = { workspace = true } moveos-config = { workspace = true } accumulator = { workspace = true } metrics = { workspace = true } +moveos-gas-profiling = { workspace = true } framework-builder = { workspace = true } framework-types = { workspace = true } diff --git a/crates/rooch/src/commands/move_cli/commands/publish.rs b/crates/rooch/src/commands/move_cli/commands/publish.rs index 4d0933a56d..c6dfbca712 100644 --- a/crates/rooch/src/commands/move_cli/commands/publish.rs +++ b/crates/rooch/src/commands/move_cli/commands/publish.rs @@ -5,9 +5,12 @@ use crate::cli_types::{CommandAction, TransactionOptions, WalletContextOptions}; use async_trait::async_trait; use clap::Parser; use move_cli::Move; +use move_core_types::account_address::AccountAddress; use move_core_types::effects::Op; +use move_core_types::resolver::ModuleResolver; use move_core_types::{identifier::Identifier, language_storage::ModuleId}; use moveos_compiler::dependency_order::sort_by_dependency_order; +use moveos_types::access_path::AccessPath; use moveos_types::move_std::string::MoveString; use moveos_types::moveos_std::module_store::{ModuleStore, PackageData}; use moveos_types::moveos_std::move_module::MoveModule; @@ -22,11 +25,93 @@ use rooch_key::keystore::account_keystore::AccountKeystore; use rooch_rpc_api::jsonrpc_types::{ ExecuteTransactionResponseView, HumanReadableDisplay, KeptVMStatusView, }; +use rooch_rpc_client::Client; use rooch_types::error::{RoochError, RoochResult}; use rooch_types::transaction::rooch::RoochTransaction; use rpassword::prompt_password; use std::collections::BTreeMap; use std::io::stderr; +use tokio::runtime::Handle; + +struct MemoryModuleResolver { + packages: BTreeMap>>, + client: Client, +} + +impl MemoryModuleResolver { + fn new(client: Client) -> Self { + Self { + packages: BTreeMap::new(), + client, + } + } + + fn download(&mut self, module_ids: Vec) -> Result<(), anyhow::Error> { + // group module_ids by ModuleId.address + let mut package_group = BTreeMap::new(); + module_ids.into_iter().for_each(|mid| { + package_group + .entry(*mid.address()) + .or_insert_with(Vec::new) + .push(Identifier::from(mid.name())) + }); + + // download each package + for (package_address, module_names) in package_group { + let access_path = AccessPath::modules(package_address, module_names); + let mut modules = BTreeMap::new(); + tokio::task::block_in_place(|| { + Handle::current().block_on(async { + let states = self.client.rooch.get_states(access_path, None).await?; + + states.into_iter().try_for_each(|state_view| { + if let Some(sv) = state_view { + let state = ObjectState::from(sv); + let module = match state.value_as_df::() { + Ok(module) => module, + Err(e) => return Err(e), + }; + modules.insert( + module.name.clone().as_str().to_owned(), + module.value.byte_codes, + ); + }; + Ok(()) + }) + }) + })?; + if !modules.is_empty() { + self.packages.insert(package_address, modules); + }; + } + + Ok(()) + } +} + +impl ModuleResolver for MemoryModuleResolver { + fn get_module(&self, module_id: &ModuleId) -> Result>, anyhow::Error> { + let pkg_addr = module_id.address(); + let module_name = module_id.name(); + match self.packages.get(pkg_addr) { + Some(modules) => { + let module = modules.get(module_name.as_str()); + match module { + Some(module) => Ok(Some(module.clone())), + None => Ok(None), + } + } + None => Ok(None), + } + } + + fn get_module_metadata( + &self, + _module_id: &ModuleId, + ) -> Vec { + unimplemented!("get_module_metadata not implemented") + } +} #[derive(Parser)] pub struct Publish { @@ -100,11 +185,20 @@ impl CommandAction for Publish { // Initialize bundles vector and sort modules by dependency order let mut bundles: Vec> = vec![]; let sorted_modules = sort_by_dependency_order(modules.iter_modules())?; + + // Download all modules from remote + let all_module_ids = package + .all_modules_map() + .get_map() + .iter() + .map(|(mid, _)| mid.clone()) + .collect::>(); + //Because the verify modules function will load many modules from the rpc server, - //We need to find a more efficient way to verify the modules. - //let resolver = context.get_client().await?; - // Serialize and collect module binaries into bundles - //verifier::verify_modules(&sorted_modules, &resolver)?; + //We need to download all modules in one rpc request and then verify the modules. + let mut resolver = MemoryModuleResolver::new(context.get_client().await?); + resolver.download(all_module_ids)?; + moveos_verifier::verifier::verify_modules(&sorted_modules, &resolver)?; for module in sorted_modules { let module_address = module.self_id().address().to_owned(); if module_address != pkg_address { diff --git a/crates/rooch/src/commands/move_cli/commands/run_function.rs b/crates/rooch/src/commands/move_cli/commands/run_function.rs index b677680f00..ac1b2bd69e 100644 --- a/crates/rooch/src/commands/move_cli/commands/run_function.rs +++ b/crates/rooch/src/commands/move_cli/commands/run_function.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::cli_types::{CommandAction, FunctionArg, TransactionOptions, WalletContextOptions}; +use crate::tx_runner::execute_tx_locally_with_gas_profile; use anyhow::Result; use async_trait::async_trait; use clap::Parser; @@ -58,6 +59,10 @@ pub struct RunFunction { /// Return command outputs in json format #[clap(long, default_value = "false")] json: bool, + + /// Run the gas profiler and output html report + #[clap(long, default_value = "false")] + gas_profile: bool, } #[async_trait] @@ -160,9 +165,33 @@ impl CommandAction for RunFunction { )); } - context - .sign_and_execute(sender, action, Some(password), max_gas_amount) - .await? + let tx_execution_result = context + .sign_and_execute( + sender, + action.clone(), + Some(password.clone()), + max_gas_amount, + ) + .await?; + + if self.gas_profile { + let state_root = tx_execution_result + .execution_info + .state_root + .0 + .as_bytes() + .to_vec(); + let tx = context + .sign(sender, action, Some(password), max_gas_amount) + .await?; + execute_tx_locally_with_gas_profile( + state_root, + context.get_client().await?, + tx.data, + ); + } + + tx_execution_result } } }; diff --git a/crates/rooch/src/commands/resource.rs b/crates/rooch/src/commands/resource.rs index 8d913b9c5a..cc4f08d5db 100644 --- a/crates/rooch/src/commands/resource.rs +++ b/crates/rooch/src/commands/resource.rs @@ -49,7 +49,7 @@ impl CommandAction> for ResourceCommand { } else { client .rooch - .get_decoded_states(AccessPath::resource(address, resource)) + .get_decoded_states(AccessPath::resource(address, resource), None) .await? .pop() .flatten() diff --git a/crates/rooch/src/commands/state.rs b/crates/rooch/src/commands/state.rs index 74e04a0b98..81ca5b4c6c 100644 --- a/crates/rooch/src/commands/state.rs +++ b/crates/rooch/src/commands/state.rs @@ -42,7 +42,7 @@ impl CommandAction>> for StateCommand { } else { client .rooch - .get_decoded_states(self.access_path) + .get_decoded_states(self.access_path, None) .await .map_err(RoochError::from)? }; diff --git a/crates/rooch/src/lib.rs b/crates/rooch/src/lib.rs index 5f8694c287..bbeaa0f7b2 100644 --- a/crates/rooch/src/lib.rs +++ b/crates/rooch/src/lib.rs @@ -23,6 +23,8 @@ pub mod cli_types; pub mod commands; pub mod utils; +pub mod tx_runner; + #[derive(clap::Parser)] #[clap(author, long_version = LONG_VERSION.as_str(), about, long_about = None, styles = Styles::styled() diff --git a/crates/rooch/src/tx_runner.rs b/crates/rooch/src/tx_runner.rs new file mode 100644 index 0000000000..d50b91ce4c --- /dev/null +++ b/crates/rooch/src/tx_runner.rs @@ -0,0 +1,201 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +use move_core_types::vm_status::KeptVMStatus::Executed; +use moveos::gas::table::{ + get_gas_schedule_entries, initial_cost_schedule, CostTable, MoveOSGasMeter, +}; +use moveos::moveos::MoveOSConfig; +use moveos::vm::moveos_vm::{MoveOSSession, MoveOSVM}; +use moveos_common::types::ClassifiedGasMeter; +use moveos_gas_profiling::profiler::{new_gas_profiler, ProfileGasMeter}; +use moveos_object_runtime::runtime::ObjectRuntime; +use moveos_types::h256::H256; +use moveos_types::move_std::option::MoveOption; +use moveos_types::moveos_std::gas_schedule::GasScheduleConfig; +use moveos_types::moveos_std::object::ObjectMeta; +use moveos_types::moveos_std::tx_context::TxContext; +use moveos_types::transaction::{MoveAction, VerifiedMoveAction, VerifiedMoveOSTransaction}; +use parking_lot::RwLock; +use rooch_genesis::FrameworksGasParameters; +use rooch_rpc_client::{Client, ClientResolver}; +use rooch_types::address::{BitcoinAddress, MultiChainAddress}; +use rooch_types::framework::auth_validator::{BuiltinAuthValidator, TxValidateResult}; +use rooch_types::framework::system_pre_execute_functions; +use rooch_types::transaction::RoochTransactionData; +use std::rc::Rc; +use std::str::FromStr; + +pub fn execute_tx_locally(state_root_bytes: Vec, client: Client, tx: RoochTransactionData) { + let state_root = H256::from_slice(state_root_bytes.as_slice()); + let root_object_meta = ObjectMeta::root_metadata(state_root, 0); + let client_resolver = ClientResolver::new(client, root_object_meta.clone()); + + let (move_mv, object_runtime, client_resolver, action, cost_table) = + prepare_execute_env(root_object_meta, &client_resolver, tx.clone()); + + let mut gas_meter = + MoveOSGasMeter::new(cost_table, GasScheduleConfig::CLI_DEFAULT_MAX_GAS_AMOUNT); + gas_meter.charge_io_write(tx.tx_size()).unwrap(); + + let mut moveos_session = MoveOSSession::new( + move_mv.inner(), + client_resolver, + object_runtime, + gas_meter, + false, + ); + + let system_pre_execute_functions = system_pre_execute_functions(); + + moveos_session + .execute_function_call(system_pre_execute_functions, false) + .expect("system_pre_execute_functions execution failed"); + + moveos_session + .execute_move_action(action) + .expect("execute_move_action failed"); + + let (_tx_context, _raw_output) = moveos_session + .finish_with_extensions(Executed) + .expect("finish_with_extensions failed"); +} + +pub fn execute_tx_locally_with_gas_profile( + state_root_bytes: Vec, + client: Client, + tx: RoochTransactionData, +) { + let state_root = H256::from_slice(state_root_bytes.as_slice()); + let root_object_meta = ObjectMeta::root_metadata(state_root, 0); + let client_resolver = ClientResolver::new(client, root_object_meta.clone()); + + let (move_mv, object_runtime, client_resolver, action, cost_table) = + prepare_execute_env(root_object_meta, &client_resolver, tx.clone()); + + let mut gas_meter = + MoveOSGasMeter::new(cost_table, GasScheduleConfig::CLI_DEFAULT_MAX_GAS_AMOUNT); + gas_meter.charge_io_write(tx.tx_size()).unwrap(); + + let mut gas_profiler = new_gas_profiler(tx.clone().action, gas_meter); + + let mut moveos_session = MoveOSSession::new( + move_mv.inner(), + client_resolver, + object_runtime, + gas_profiler.clone(), + false, + ); + + let system_pre_execute_functions = system_pre_execute_functions(); + + moveos_session + .execute_function_call(system_pre_execute_functions, false) + .expect("system_pre_execute_functions execution failed"); + + moveos_session + .execute_move_action(action) + .expect("execute_move_action failed"); + + let (_tx_context, _raw_output) = moveos_session + .finish_with_extensions(Executed) + .expect("finish_with_extensions failed"); + + let gas_profiling_info = gas_profiler.finish(); + + gas_profiling_info + .generate_html_report( + format!("./gas_profiling_{:?}", tx.tx_hash()), + "Rooch Gas Profiling".to_string(), + ) + .unwrap(); +} + +pub fn prepare_execute_env( + state_root: ObjectMeta, + client_resolver: &ClientResolver, + tx: RoochTransactionData, +) -> ( + MoveOSVM, + Rc>, + &ClientResolver, + VerifiedMoveAction, + CostTable, +) { + let gas_entries = + get_gas_schedule_entries(client_resolver).expect("get_gas_schedule_entries failed"); + let cost_table = initial_cost_schedule(gas_entries); + + let verified_tx = + convert_to_verified_tx(state_root.clone(), tx).expect("convert_to_verified_tx failed"); + + let VerifiedMoveOSTransaction { + root: _, + ctx, + action, + } = verified_tx; + + let gas_parameters = + FrameworksGasParameters::load_from_chain(client_resolver).expect("load_from_chain failed"); + + let object_runtime = Rc::new(RwLock::new(ObjectRuntime::new( + ctx, + state_root, + client_resolver, + ))); + + let vm = MoveOSVM::new( + gas_parameters.all_natives(), + MoveOSConfig::default().vm_config, + ) + .expect("create MoveVM failed"); + + (vm, object_runtime, client_resolver, action, cost_table) +} + +fn convert_to_verified_tx( + root: ObjectMeta, + tx_data: RoochTransactionData, +) -> anyhow::Result { + let mut tx_ctx = TxContext::new( + tx_data.sender.into(), + tx_data.sequence_number, + tx_data.max_gas_amount, + tx_data.tx_hash(), + tx_data.tx_size(), + ); + + let mut bitcoin_address = BitcoinAddress::from_str("18cBEMRxXHqzWWCxZNtU91F5sbUNKhL5PX")?; + + let user_multi_chain_address: MultiChainAddress = tx_data.sender.into(); + if user_multi_chain_address.is_bitcoin_address() { + bitcoin_address = user_multi_chain_address.try_into()?; + } + + let dummy_result = TxValidateResult { + auth_validator_id: BuiltinAuthValidator::Bitcoin.flag().into(), + auth_validator: MoveOption::none(), + session_key: MoveOption::none(), + bitcoin_address, + }; + + tx_ctx.add(dummy_result)?; + + let verified_action = match tx_data.action { + MoveAction::Script(script_call) => VerifiedMoveAction::Script { call: script_call }, + MoveAction::Function(function_call) => VerifiedMoveAction::Function { + call: function_call, + bypass_visibility: false, + }, + MoveAction::ModuleBundle(module_bundle) => VerifiedMoveAction::ModuleBundle { + module_bundle, + init_function_modules: vec![], + }, + }; + + Ok(VerifiedMoveOSTransaction::new( + root, + tx_ctx, + verified_action, + )) +} diff --git a/crates/testsuite/features/cmd.feature b/crates/testsuite/features/cmd.feature index dd0fd3002b..882c55ef22 100644 --- a/crates/testsuite/features/cmd.feature +++ b/crates/testsuite/features/cmd.feature @@ -3,15 +3,15 @@ Feature: Rooch CLI integration tests @serial Scenario: rooch rpc test Given a server for rooch_rpc_test - Then cmd: "rpc request --method rooch_getStates --params '["/resource/0x3/0x3::account_coin_store::AutoAcceptCoins",{"decode":true}]' --json" + Then cmd: "rpc request --method rooch_getStates --params '["/resource/0x3/0x3::account_coin_store::AutoAcceptCoins", {"decode":true}]' --json" #The object_type contians blank space, so, we should quote it Then assert: "'{{$.rpc[-1][0].object_type}}' == '0x2::object::DynamicField<0x1::string::String, 0x3::account_coin_store::AutoAcceptCoins>'" - Then cmd: "rpc request --method rooch_getStates --params '["/object/0x3",{"decode":true}]' --json" + Then cmd: "rpc request --method rooch_getStates --params '["/object/0x3", {"decode":true}]' --json" Then assert: "{{$.rpc[-1][0].object_type}} == '0x2::account::Account'" Then cmd: "rpc request --method rooch_listStates --params '["/resource/0x3", null, null, {"decode":true}]' --json" Then assert: "'{{$.rpc[-1]}}' contains '0x3::account_coin_store::AutoAcceptCoins'" # named_object_id(0x2::timestamp::Timestamp) == 0x3a7dfe7a9a5cd608810b5ebd60c7adf7316667b17ad5ae703af301b74310bcca - Then cmd: "rpc request --method rooch_getStates --params '["/object/0x3a7dfe7a9a5cd608810b5ebd60c7adf7316667b17ad5ae703af301b74310bcca",{"decode":true}]' --json" + Then cmd: "rpc request --method rooch_getStates --params '["/object/0x3a7dfe7a9a5cd608810b5ebd60c7adf7316667b17ad5ae703af301b74310bcca", {"decode":true}]' --json" Then assert: "{{$.rpc[-1][0].object_type}} == '0x2::timestamp::Timestamp'" Then assert: "{{$.rpc[-1][0].decoded_value.value.milliseconds}} == 0" Then cmd: "rpc request --method rooch_getStates --params '["/object/0x2::timestamp::Timestamp",{"decode":true}]' --json" @@ -58,7 +58,7 @@ Feature: Rooch CLI integration tests Then cmd: "move run --function rooch_framework::gas_coin::faucet_entry --args u256:10000000000 --json" Then assert: "{{$.move[-1].execution_info.status.type}} == executed" - Then cmd: "rpc request --method rooch_getStates --params '["/object/0x3a7dfe7a9a5cd608810b5ebd60c7adf7316667b17ad5ae703af301b74310bcca",{"decode":true}]' --json" + Then cmd: "rpc request --method rooch_getStates --params '["/object/0x3a7dfe7a9a5cd608810b5ebd60c7adf7316667b17ad5ae703af301b74310bcca", {"decode":true}]' --json" Then assert: "{{$.rpc[-1][0].object_type}} == '0x2::timestamp::Timestamp'" # ensure the tx_timestamp update the global timestamp Then assert: "{{$.rpc[-1][0].decoded_value.value.milliseconds}} != 0" diff --git a/frameworks/bitcoin-move/doc/bitcoin.md b/frameworks/bitcoin-move/doc/bitcoin.md index e5f8e01ae3..55a39443c1 100644 --- a/frameworks/bitcoin-move/doc/bitcoin.md +++ b/frameworks/bitcoin-move/doc/bitcoin.md @@ -34,6 +34,7 @@ use 0x2::timestamp; use 0x3::address_mapping; use 0x3::bitcoin_address; +use 0x3::chain_id; use 0x4::inscription_updater; use 0x4::network; use 0x4::pending_block; @@ -139,6 +140,15 @@ The reorg is too deep, we need to stop the system and fix the issue + + + + +
const ORDINALS_PAUSE_HEIGHT: u64 = 861150;
+
+ + + ## Function `genesis_init` diff --git a/frameworks/bitcoin-move/sources/bitcoin.move b/frameworks/bitcoin-move/sources/bitcoin.move index b01812c6bf..563f58b91b 100644 --- a/frameworks/bitcoin-move/sources/bitcoin.move +++ b/frameworks/bitcoin-move/sources/bitcoin.move @@ -37,6 +37,8 @@ module bitcoin_move::bitcoin{ /// https://github.com/bitcoin/bips/blob/master/bip-0034.mediawiki const BIP_34_HEIGHT:u64 = 227835; + const ORDINALS_PAUSE_HEIGHT:u64 = 861150; + struct UTXONotExistsEvent has copy, drop{ outpoint: OutPoint, } @@ -162,16 +164,19 @@ module bitcoin_move::bitcoin{ idx = idx + 1; }; - - let seal_outs = bitcoin_move::inscription_updater::process_tx(pending_block, tx, &mut input_utxos); - let seal_outs_len = vector::length(&seal_outs); - if (seal_outs_len > 0) { - let seal_out_idx = 0; - while (seal_out_idx < seal_outs_len) { - let seal_out = vector::pop_back(&mut seal_outs); - let (output_index, utxo_seal) = utxo::unpack_seal_out(seal_out); - simple_multimap::add(&mut output_seals, output_index, utxo_seal); - seal_out_idx = seal_out_idx + 1; + //temporary pause the ordinals process for the performance reason + let skip_ordinals = block_height >= ORDINALS_PAUSE_HEIGHT && network::is_mainnet() && rooch_framework::chain_id::is_main(); + if(!skip_ordinals){ + let seal_outs = bitcoin_move::inscription_updater::process_tx(pending_block, tx, &mut input_utxos); + let seal_outs_len = vector::length(&seal_outs); + if (seal_outs_len > 0) { + let seal_out_idx = 0; + while (seal_out_idx < seal_outs_len) { + let seal_out = vector::pop_back(&mut seal_outs); + let (output_index, utxo_seal) = utxo::unpack_seal_out(seal_out); + simple_multimap::add(&mut output_seals, output_index, utxo_seal); + seal_out_idx = seal_out_idx + 1; + }; }; }; diff --git a/frameworks/framework-release/released/10/README.md b/frameworks/framework-release/released/10/README.md new file mode 100644 index 0000000000..cf57422b5b --- /dev/null +++ b/frameworks/framework-release/released/10/README.md @@ -0,0 +1,6 @@ +# Rooch Move Framework v10 + +* [bitcoin_move] Add `update_reorg_block_count` function to `pending_block`. +* [moveos_std] Add `has_upgrade_permission` and `ensure_upgrade_permission` function to `module_store`. +* [rooch_framework] Binding bitcoin address when transfer. +* [bitcoin_move] Add assert to ensure threshold for multisign_account. \ No newline at end of file diff --git a/frameworks/framework-release/released/11/0x1/package.rpd b/frameworks/framework-release/released/11/0x1/package.rpd new file mode 100644 index 0000000000..88adae99ef Binary files /dev/null and b/frameworks/framework-release/released/11/0x1/package.rpd differ diff --git a/frameworks/framework-release/released/11/0x2/package.rpd b/frameworks/framework-release/released/11/0x2/package.rpd new file mode 100644 index 0000000000..173755c58a Binary files /dev/null and b/frameworks/framework-release/released/11/0x2/package.rpd differ diff --git a/frameworks/framework-release/released/11/0x3/package.rpd b/frameworks/framework-release/released/11/0x3/package.rpd new file mode 100644 index 0000000000..6af8365320 Binary files /dev/null and b/frameworks/framework-release/released/11/0x3/package.rpd differ diff --git a/frameworks/framework-release/released/11/0x4/package.rpd b/frameworks/framework-release/released/11/0x4/package.rpd new file mode 100644 index 0000000000..df2ab35496 Binary files /dev/null and b/frameworks/framework-release/released/11/0x4/package.rpd differ diff --git a/frameworks/framework-release/released/11/README.md b/frameworks/framework-release/released/11/README.md new file mode 100644 index 0000000000..9debbde1a1 --- /dev/null +++ b/frameworks/framework-release/released/11/README.md @@ -0,0 +1,8 @@ +# Rooch Move Framework v11 + +* [bitcoin_move] Do not frozen the Inscription when burn. +* [bitcoin_move] Add `remove_metaprotocol_attachment` and `exists_metaprotocol_attachment` function to `ord`. +* [moveos_std] Implement `to_json` in `json` module. +* [moveos_std] Add `exists_new_events` and `subscriber_info` function to `event_queue`. +* [bitcoin_move] Temporary pause the ordinals protocol. +* [rooch_nursery] Implement bitseed merge and runner. \ No newline at end of file diff --git a/frameworks/framework-release/released/11/stdlib b/frameworks/framework-release/released/11/stdlib new file mode 100644 index 0000000000..3ca529fdef Binary files /dev/null and b/frameworks/framework-release/released/11/stdlib differ diff --git a/frameworks/moveos-stdlib/doc/event_queue.md b/frameworks/moveos-stdlib/doc/event_queue.md index 67514cb480..999fb1095f 100644 --- a/frameworks/moveos-stdlib/doc/event_queue.md +++ b/frameworks/moveos-stdlib/doc/event_queue.md @@ -15,6 +15,8 @@ - [Function `subscribe`](#0x2_event_queue_subscribe) - [Function `unsubscribe`](#0x2_event_queue_unsubscribe) - [Function `remove_expired_events`](#0x2_event_queue_remove_expired_events) +- [Function `subscriber_info`](#0x2_event_queue_subscriber_info) +- [Function `exists_new_events`](#0x2_event_queue_exists_new_events)
use 0x1::option;
@@ -201,3 +203,25 @@ Anyone can call this function to remove the expired events
 
 
public fun remove_expired_events<E: copy, drop, store>(queue_name: string::String)
 
+ + + + + +## Function `subscriber_info` + + + +
public fun subscriber_info<E: copy, drop, store>(subscriber_obj: &object::Object<event_queue::Subscriber<E>>): (u64, u64, u64)
+
+ + + + + +## Function `exists_new_events` + + + +
public fun exists_new_events<E: copy, drop, store>(subscriber_obj: &object::Object<event_queue::Subscriber<E>>): bool
+
diff --git a/frameworks/moveos-stdlib/sources/event_queue.move b/frameworks/moveos-stdlib/sources/event_queue.move index 4cbe63ea04..fd60f1608f 100644 --- a/frameworks/moveos-stdlib/sources/event_queue.move +++ b/frameworks/moveos-stdlib/sources/event_queue.move @@ -7,7 +7,7 @@ module moveos_std::event_queue { use std::string::String; use moveos_std::timestamp; use moveos_std::event; - use moveos_std::object::{Self, Object, ObjectID}; + use moveos_std::object::{Self, Object, ObjectID, borrow_object}; const EVENT_EXPIRE_TIME: u64 = 1000 * 60 * 60 * 24 * 31; // 31 days const REMOVE_EXPIRED_EVENT_BATCH_SIZE: u64 = 10; @@ -175,6 +175,39 @@ module moveos_std::event_queue { let event_queue_obj = borrow_mut_queue(queue_id); internal_remove_expired_events(event_queue_obj); } + + + public fun subscriber_info(subscriber_obj: &Object>):(u64, u64, u64) { + let subscriber = object::borrow(subscriber_obj); + let subscriber_sequence_number = subscriber.sequence_number; + // If the subscriber_obj is exsited, the queue must be existed + let event_queue_obj = borrow_object>(subscriber.queue_id); + let event_queue = object::borrow(event_queue_obj); + let head_sequence_number = event_queue.head_sequence_number; + let tail_sequence_number = event_queue.tail_sequence_number; + (subscriber_sequence_number, head_sequence_number, tail_sequence_number) + } + + public fun exists_new_events(subscriber_obj: &Object>): bool { + let subscriber = object::borrow(subscriber_obj); + let subscriber_sequence_number = subscriber.sequence_number; + // If the subscriber_obj is exsited, the queue must be existed + let event_queue_obj = borrow_object>(subscriber.queue_id); + let head_sequence_number = object::borrow(event_queue_obj).head_sequence_number; + let tail_sequence_number = object::borrow(event_queue_obj).tail_sequence_number; + if(head_sequence_number == tail_sequence_number){ + return false + }; + if (subscriber_sequence_number >= head_sequence_number) { + return false + }; + subscriber_sequence_number = if (tail_sequence_number > subscriber_sequence_number) { + tail_sequence_number + } else { + subscriber_sequence_number + }; + return object::contains_field(event_queue_obj, subscriber_sequence_number) + } fun internal_remove_expired_events(event_queue_obj: &mut Object>){ let head_sequence_number = object::borrow(event_queue_obj).head_sequence_number; diff --git a/moveos/moveos-commons/moveos-common/Cargo.toml b/moveos/moveos-commons/moveos-common/Cargo.toml index acabec2767..3f5cdccdc2 100644 --- a/moveos/moveos-commons/moveos-common/Cargo.toml +++ b/moveos/moveos-commons/moveos-common/Cargo.toml @@ -20,3 +20,7 @@ bcs = { workspace = true } log = { workspace = true } itertools = { workspace = true } libc = { workspace = true } + +move-core-types = { workspace = true } +move-binary-format = { workspace = true } +move-vm-types = { workspace = true } \ No newline at end of file diff --git a/moveos/moveos-commons/moveos-common/src/lib.rs b/moveos/moveos-commons/moveos-common/src/lib.rs index ae386d55b4..c51b47321a 100644 --- a/moveos/moveos-commons/moveos-common/src/lib.rs +++ b/moveos/moveos-commons/moveos-common/src/lib.rs @@ -1,4 +1,5 @@ // Copyright (c) RoochNetwork // SPDX-License-Identifier: Apache-2.0 +pub mod types; pub mod utils; diff --git a/moveos/moveos-commons/moveos-common/src/types.rs b/moveos/moveos-commons/moveos-common/src/types.rs new file mode 100644 index 0000000000..4aa23c94ac --- /dev/null +++ b/moveos/moveos-commons/moveos-common/src/types.rs @@ -0,0 +1,59 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +use move_binary_format::errors::PartialVMResult; +use move_core_types::gas_algebra::InternalGas; +use move_vm_types::gas::{GasMeter, UnmeteredGasMeter}; + +#[derive(Debug, Clone)] +pub struct GasStatement { + pub execution_gas_used: InternalGas, + pub storage_gas_used: InternalGas, +} + +pub trait ClassifiedGasMeter { + fn charge_execution(&mut self, gas_cost: u64) -> PartialVMResult<()>; + // fn charge_io_read(&mut self); + fn charge_io_write(&mut self, data_size: u64) -> PartialVMResult<()>; + //fn charge_event(&mut self, events: &[TransactionEvent]) -> PartialVMResult<()>; + //fn charge_change_set(&mut self, change_set: &StateChangeSet) -> PartialVMResult<()>; + fn check_constrains(&self, max_gas_amount: u64) -> PartialVMResult<()>; + fn gas_statement(&self) -> GasStatement; +} + +impl ClassifiedGasMeter for UnmeteredGasMeter { + fn charge_execution(&mut self, _gas_cost: u64) -> PartialVMResult<()> { + Ok(()) + } + + fn charge_io_write(&mut self, _data_size: u64) -> PartialVMResult<()> { + Ok(()) + } + + fn check_constrains(&self, _max_gas_amount: u64) -> PartialVMResult<()> { + Ok(()) + } + + fn gas_statement(&self) -> GasStatement { + GasStatement { + execution_gas_used: InternalGas::from(0), + storage_gas_used: InternalGas::from(0), + } + } +} + +pub trait SwitchableGasMeter: GasMeter { + fn stop_metering(&mut self); + fn start_metering(&mut self); + fn is_metering(&self) -> bool; +} + +impl SwitchableGasMeter for UnmeteredGasMeter { + fn stop_metering(&mut self) {} + + fn start_metering(&mut self) {} + + fn is_metering(&self) -> bool { + false + } +} diff --git a/moveos/moveos-eventbus/Cargo.toml b/moveos/moveos-eventbus/Cargo.toml index dd829a8749..5a36481c71 100644 --- a/moveos/moveos-eventbus/Cargo.toml +++ b/moveos/moveos-eventbus/Cargo.toml @@ -17,4 +17,4 @@ once_cell = { workspace = true } crossbeam-channel = { workspace = true } coerce = { workspace = true } tokio = "1.40.0" -thiserror = "1.0.63" \ No newline at end of file +thiserror = "1.0.64" \ No newline at end of file diff --git a/moveos/moveos-gas-profiling/Cargo.toml b/moveos/moveos-gas-profiling/Cargo.toml new file mode 100644 index 0000000000..3b8b137e49 --- /dev/null +++ b/moveos/moveos-gas-profiling/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "moveos-gas-profiling" + +# Workspace inherited keys +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +publish = { workspace = true } +repository = { workspace = true } +rust-version = { workspace = true } + +[dependencies] +inferno = { workspace = true } +move-core-types = { workspace = true } +move-vm-types = { workspace = true } +move-binary-format = { workspace = true } +anyhow = { workspace = true } +regex = { workspace = true } +serde_json = { workspace = true } +smallvec = { workspace = true } +handlebars = { workspace = true } + +moveos-types = { workspace = true } +moveos-common = { workspace = true } \ No newline at end of file diff --git a/moveos/moveos-gas-profiling/src/aggregate.rs b/moveos/moveos-gas-profiling/src/aggregate.rs new file mode 100644 index 0000000000..79ad7a2e50 --- /dev/null +++ b/moveos/moveos-gas-profiling/src/aggregate.rs @@ -0,0 +1,96 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +use crate::log::ExecutionAndIOCosts; +use crate::log::ExecutionGasEvent; +use crate::render::Render; +use move_core_types::gas_algebra::{GasQuantity, InternalGas}; +use std::collections::{btree_map, BTreeMap}; + +/// Represents an aggregation of execution gas events, including the count and total gas costs for each type of event. +/// +/// The events are sorted by the amount of gas used, from high to low. +#[derive(Debug)] +pub struct AggregatedExecutionGasEvents { + pub ops: Vec<(String, usize, InternalGas)>, +} + +fn insert_or_add( + map: &mut BTreeMap)>, + key: K, + amount: GasQuantity, +) where + K: Ord, +{ + if amount.is_zero() { + return; + } + match map.entry(key) { + btree_map::Entry::Occupied(entry) => { + let r = entry.into_mut(); + r.0 += 1; + r.1 += amount; + } + btree_map::Entry::Vacant(entry) => { + entry.insert((1, amount)); + } + } +} + +fn into_sorted_vec(collection: I) -> Vec<(K, usize, N)> +where + N: Ord, + I: IntoIterator, +{ + let mut v = collection + .into_iter() + .map(|(key, (count, amount))| (key, count, amount)) + .collect::>(); + // Sort in descending order. + v.sort_by(|(_key1, _count1, amount1), (_key2, _count2, amount2)| amount2.cmp(amount1)); + v +} + +impl ExecutionAndIOCosts { + /// Counts the number of hits and aggregates the gas costs for each type of event. + pub fn aggregate_gas_events(&self) -> AggregatedExecutionGasEvents { + use ExecutionGasEvent::*; + + let mut ops = BTreeMap::new(); + let mut storage_reads = BTreeMap::new(); + + for event in self.gas_events() { + match event { + Loc(..) | Call(..) => (), + Bytecode { op, cost } => insert_or_add( + &mut ops, + format!("{:?}", op).to_ascii_lowercase().to_string(), + *cost, + ), + CallNative { + module_id, + fn_name, + ty_args, + cost, + } => insert_or_add( + &mut ops, + format!( + "{}", + Render(&(module_id, fn_name.as_ident_str(), ty_args.as_slice())), + ), + *cost, + ), + LoadResource { + addr: _addr, + ty, + cost, + } => insert_or_add(&mut storage_reads, format!("{}", ty), *cost), + CreateTy { cost } => insert_or_add(&mut ops, "create_ty".to_string(), *cost), + } + } + + AggregatedExecutionGasEvents { + ops: into_sorted_vec(ops), + } + } +} diff --git a/moveos/moveos-gas-profiling/src/erased.rs b/moveos/moveos-gas-profiling/src/erased.rs new file mode 100644 index 0000000000..ba5d42ba45 --- /dev/null +++ b/moveos/moveos-gas-profiling/src/erased.rs @@ -0,0 +1,131 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +use crate::log::{CallFrame, ExecutionAndIOCosts, ExecutionGasEvent, FrameName}; +use crate::render::Render; +use move_core_types::gas_algebra::InternalGas; +use std::ops::AddAssign; + +/// Represents a node in a general tree structure where each node is tagged with +/// some text & a numerical value. +#[derive(Clone)] +pub struct Node { + pub text: String, + pub val: N, + pub children: Vec>, +} + +#[derive(Clone)] +pub struct TypeErasedExecutionAndIoCosts { + pub total: InternalGas, + pub tree: Node, +} + +impl Node { + pub fn new(name: impl Into, data: impl Into) -> Self { + Self { + text: name.into(), + val: data.into(), + children: vec![], + } + } + + pub fn new_with_children( + name: impl Into, + data: impl Into, + children: impl IntoIterator, + ) -> Self { + Self { + text: name.into(), + val: data.into(), + children: children.into_iter().collect(), + } + } + + pub fn preorder_traversel(&self, mut f: impl FnMut(usize, &str, &N)) { + let mut stack = vec![(self, 0)]; + + while let Some((node, depth)) = stack.pop() { + f(depth, &node.text, &node.val); + stack.extend(node.children.iter().map(|child| (child, depth + 1)).rev()); + } + } +} + +impl CallFrame { + fn to_erased(&self) -> Node { + let name = match &self.name { + FrameName::Script => "script".to_string(), + FrameName::Function { + module_id, + name, + ty_args, + } => { + format!( + "{}", + Render(&(module_id, name.as_ident_str(), ty_args.as_slice())) + ) + } + }; + + let children = self + .events + .iter() + .map(|event| event.to_erased()) + .collect::>(); + + Node::new_with_children(name, 0, children) + } +} + +impl ExecutionAndIOCosts { + /// Convert the gas log into a type-erased representation. + pub fn to_erased(&self) -> TypeErasedExecutionAndIoCosts { + let nodes = vec![self.call_graph.to_erased()]; + + TypeErasedExecutionAndIoCosts { + total: self.total, + tree: Node::new_with_children("execution & IO (gas unit, full trace)", 0, nodes), + } + } +} + +impl Node +where + N: AddAssign + Copy, +{ + pub fn include_child_costs(&mut self) { + for child in &mut self.children { + child.include_child_costs(); + self.val += child.val; + } + } +} + +impl ExecutionGasEvent { + fn to_erased(&self) -> Node { + use ExecutionGasEvent::*; + + match self { + Loc(offset) => Node::new(format!("@{}", offset), 0), + Bytecode { op, cost } => Node::new(format!("{:?}", op).to_ascii_lowercase(), *cost), + Call(frame) => frame.to_erased(), + CallNative { + module_id, + fn_name, + ty_args, + cost, + } => Node::new( + format!( + "{}", + Render(&(module_id, fn_name.as_ident_str(), ty_args.as_slice())) + ), + *cost, + ), + LoadResource { addr, ty, cost } => { + Node::new(format!("load<{}::{}>", Render(addr), ty), *cost) + } + CreateTy { cost } => Node::new("create_ty", *cost), + } + } +} diff --git a/moveos/moveos-gas-profiling/src/flamegraph.rs b/moveos/moveos-gas-profiling/src/flamegraph.rs new file mode 100644 index 0000000000..a0e57409de --- /dev/null +++ b/moveos/moveos-gas-profiling/src/flamegraph.rs @@ -0,0 +1,143 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +use crate::log::{CallFrame, ExecutionAndIOCosts}; +use crate::render::Render; +use inferno::flamegraph::TextTruncateDirection; +use move_core_types::gas_algebra::InternalGas; +use regex::Captures; + +#[allow(dead_code)] +#[derive(Debug)] +struct LineBuffer(Vec); + +#[allow(dead_code)] +impl LineBuffer { + fn new() -> Self { + Self(vec![]) + } + + fn push(&mut self, item: impl AsRef, count: impl Into) { + let count: u64 = count.into(); + + if count > 0 { + self.0.push(format!("{} {}", item.as_ref(), count)); + } + } + + fn into_inner(self) -> Vec { + self.0 + } +} + +#[allow(dead_code)] +impl ExecutionAndIOCosts { + /// Convert the execution gas log into folded stack lines, which can + /// then be used to generate a flamegraph. + fn to_folded_stack_lines(&self) -> Vec { + let mut lines = LineBuffer::new(); + let mut path = vec![]; + + struct Rec<'a> { + lines: &'a mut LineBuffer, + path: &'a mut Vec, + } + + impl<'a> Rec<'a> { + fn visit(&mut self, frame: &CallFrame) { + self.path.push(format!("{}", frame.name)); + + let mut frame_cost = InternalGas::new(0); + + for event in &frame.events { + use crate::log::ExecutionGasEvent; + use ExecutionGasEvent::*; + + match event { + Loc(_) => (), + Bytecode { cost, .. } | CreateTy { cost } => frame_cost += *cost, + Call(inner_frame) => self.visit(inner_frame), + CallNative { + module_id: module, + fn_name, + ty_args, + cost, + } => self.lines.push( + format!( + "{};{}", + self.path(), + Render(&(module, fn_name.as_ident_str(), ty_args.as_slice())), + ), + *cost, + ), + LoadResource { addr, ty, cost } => self.lines.push( + format!("{};load<{}::{}>", self.path(), Render(addr), ty), + *cost, + ), + } + } + + self.lines.push(&self.path(), frame_cost); + self.path.pop(); + } + + fn path(&self) -> String { + self.path.join(";") + } + } + + Rec { + lines: &mut lines, + path: &mut path, + } + .visit(&self.call_graph); + + lines.into_inner() + } + + pub fn to_flamegraph(&self, title: String) -> anyhow::Result>> { + let lines = self.to_folded_stack_lines(); + + if lines.is_empty() { + return Ok(None); + } + + let mut options = inferno::flamegraph::Options::default(); + options.flame_chart = true; + options.text_truncate_direction = TextTruncateDirection::Right; + options.color_diffusion = true; + options.title = title; + + let mut graph_content = vec![]; + inferno::flamegraph::from_lines( + &mut options, + lines.iter().rev().map(|s| s.as_str()), + &mut graph_content, + )?; + let graph_content = String::from_utf8_lossy(&graph_content); + + // Inferno does not allow us to customize some of the text in the resulting graph, + // so we have to do it through regex replacement. + let re = regex::Regex::new("([1-9][0-9]*(,[0-9]+)*) samples") + .expect("should be able to build regex successfully"); + + let graph_content = re.replace_all(&graph_content, |caps: &Captures| { + let count: u64 = caps[1] + .replace(',', "") + .parse() + .expect("should be able parse count as u64"); + + let count_scaled = count as f64 / 1f64; + + format!( + "{} gas units", + crate::misc::strip_trailing_zeros_and_decimal_point(&format!( + "{:.8}", + count_scaled + )) + ) + }); + + Ok(Some(graph_content.as_bytes().to_vec())) + } +} diff --git a/moveos/moveos-gas-profiling/src/lib.rs b/moveos/moveos-gas-profiling/src/lib.rs new file mode 100644 index 0000000000..a64fbefd33 --- /dev/null +++ b/moveos/moveos-gas-profiling/src/lib.rs @@ -0,0 +1,11 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +pub mod aggregate; +pub mod erased; +pub mod flamegraph; +pub mod log; +pub mod misc; +pub mod profiler; +pub mod render; +pub mod report; diff --git a/moveos/moveos-gas-profiling/src/log.rs b/moveos/moveos-gas-profiling/src/log.rs new file mode 100644 index 0000000000..b4391989fe --- /dev/null +++ b/moveos/moveos-gas-profiling/src/log.rs @@ -0,0 +1,129 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +use move_binary_format::file_format::CodeOffset; +use move_binary_format::file_format_common::Opcodes; +use move_core_types::account_address::AccountAddress; +use move_core_types::gas_algebra::InternalGas; +use move_core_types::identifier::Identifier; +use move_core_types::language_storage::{ModuleId, TypeTag}; +use smallvec::{smallvec, SmallVec}; + +/// An event occurred during the execution of a function, along with the +/// gas cost associated with it, if any. +#[derive(Debug, Clone)] +pub enum ExecutionGasEvent { + /// A special event indicating that the program counter has moved to + /// a specific offset. This is emitted by the branch instructions + /// and is crucial for reconstructing the control flow. + Loc(CodeOffset), + Bytecode { + op: Opcodes, + cost: InternalGas, + }, + Call(CallFrame), + CallNative { + module_id: ModuleId, + fn_name: Identifier, + ty_args: Vec, + cost: InternalGas, + }, + LoadResource { + addr: AccountAddress, + ty: TypeTag, + cost: InternalGas, + }, + CreateTy { + cost: InternalGas, + }, +} + +/// An enum representing the name of a call frame. +/// Could be either a script or a function. +#[derive(Debug, Clone)] +pub enum FrameName { + Script, + Function { + module_id: ModuleId, + name: Identifier, + ty_args: Vec, + }, +} + +/// A struct containing information about a function call, including the name of the +/// function and all gas events that happened during the call. +#[derive(Debug, Clone)] +pub struct CallFrame { + pub name: FrameName, + pub events: Vec, +} + +impl CallFrame { + pub fn new_function(module_id: ModuleId, name: Identifier, ty_args: Vec) -> Self { + Self { + name: FrameName::Function { + module_id, + name, + ty_args, + }, + events: vec![], + } + } + + pub fn new_script() -> Self { + Self { + name: FrameName::Script, + events: vec![], + } + } +} + +#[derive(Debug, Clone)] +pub struct ExecutionAndIOCosts { + pub total: InternalGas, + pub call_graph: CallFrame, +} + +#[derive(Debug, Clone)] +pub struct TransactionGasLog { + pub exec_io: ExecutionAndIOCosts, + pub storage: InternalGas, +} + +pub struct GasEventIter<'a> { + stack: SmallVec<[(&'a CallFrame, usize); 16]>, +} + +impl<'a> Iterator for GasEventIter<'a> { + type Item = &'a ExecutionGasEvent; + + fn next(&mut self) -> Option { + loop { + match self.stack.last_mut() { + None => return None, + Some((frame, pc)) => { + if *pc >= frame.events.len() { + self.stack.pop(); + continue; + } + + let event = &frame.events[*pc]; + *pc += 1; + if let ExecutionGasEvent::Call(child_frame) = event { + self.stack.push((child_frame, 0)) + } + return Some(event); + } + } + } + } +} + +impl ExecutionAndIOCosts { + #[allow(clippy::needless_lifetimes)] + pub fn gas_events<'a>(&'a self) -> GasEventIter<'a> { + GasEventIter { + stack: smallvec![(&self.call_graph, 0)], + } + } +} diff --git a/moveos/moveos-gas-profiling/src/misc.rs b/moveos/moveos-gas-profiling/src/misc.rs new file mode 100644 index 0000000000..585269113b --- /dev/null +++ b/moveos/moveos-gas-profiling/src/misc.rs @@ -0,0 +1,15 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +pub fn strip_trailing_zeros_and_decimal_point(mut s: &str) -> &str { + loop { + match s { + "0" | ".0" => return s, + _ => match s.strip_suffix('0') { + Some(stripped) => s = stripped, + None => break, + }, + } + } + s.strip_suffix('.').unwrap_or(s) +} diff --git a/moveos/moveos-gas-profiling/src/profiler.rs b/moveos/moveos-gas-profiling/src/profiler.rs new file mode 100644 index 0000000000..c402964faf --- /dev/null +++ b/moveos/moveos-gas-profiling/src/profiler.rs @@ -0,0 +1,528 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +use crate::log::{CallFrame, ExecutionAndIOCosts, ExecutionGasEvent, FrameName, TransactionGasLog}; +use move_binary_format::file_format::CodeOffset; +use move_binary_format::file_format_common::Opcodes; +use move_core_types::account_address::AccountAddress; +use move_core_types::gas_algebra::{InternalGas, NumArgs, NumBytes}; +use move_core_types::identifier::Identifier; +use move_core_types::language_storage::{ModuleId, TypeTag}; +use move_vm_types::gas::{GasMeter, SimpleInstruction}; +use move_vm_types::natives::function::PartialVMResult; +use move_vm_types::views::{TypeView, ValueView}; +use moveos_common::types::{ClassifiedGasMeter, GasStatement, SwitchableGasMeter}; +use moveos_types::transaction::MoveAction; +use std::sync::{Arc, RwLock}; + +#[derive(Debug, Clone)] +pub struct GasProfiler { + base: G, + frames: Arc>>, + metering: bool, +} + +macro_rules! delegate_mut { + ($( + fn $fn: ident $(<$($lt: lifetime),*>)? (&mut self $(, $arg: ident : $ty: ty)* $(,)?) -> $ret_ty: ty; + )*) => { + $(fn $fn $(<$($lt)*>)? (&mut self, $($arg: $ty),*) -> $ret_ty { + self.base.$fn($($arg),*) + })* + }; +} + +macro_rules! record_bytecode { + ($( + $([$op: expr])? + fn $fn: ident $(<$($lt: lifetime),*>)? (&mut self $(, $arg: ident : $ty: ty)* $(,)?) -> PartialVMResult<()>; + )*) => { + $(fn $fn $(<$($lt)*>)? (&mut self, $($arg: $ty),*) -> PartialVMResult<()> { + #[allow(unused)] + use Opcodes::*; + + #[allow(unused)] + let (cost, res) = self.delegate_charge(|base| base.$fn($($arg),*)); + + $( + self.record_bytecode($op, cost); + )? + + res + })* + }; +} + +impl GasProfiler { + pub fn new_function( + base: G, + module_id: ModuleId, + func_name: Identifier, + ty_args: Vec, + ) -> Self { + Self { + base, + frames: Arc::new(RwLock::new(vec![CallFrame::new_function( + module_id, func_name, ty_args, + )])), + metering: true, + } + } +} + +impl GasProfiler { + fn record_gas_event(&mut self, event: ExecutionGasEvent) { + if self.metering { + self.frames + .write() + .unwrap() + .last_mut() + .unwrap() + .events + .push(event); + } + } + + fn record_bytecode(&mut self, op: Opcodes, cost: InternalGas) { + if self.metering { + self.record_gas_event(ExecutionGasEvent::Bytecode { op, cost }) + } + } + + fn record_offset(&mut self, offset: CodeOffset) { + if self.metering { + self.record_gas_event(ExecutionGasEvent::Loc(offset)) + } + } + + /// Delegate the charging call to the base gas meter and measure variation in balance. + fn delegate_charge(&mut self, charge: F) -> (InternalGas, R) + where + F: FnOnce(&mut G) -> R, + { + let old = self.base.balance_internal(); + let res = charge(&mut self.base); + let new = self.base.balance_internal(); + let cost = old.checked_sub(new).expect("gas cost must be non-negative"); + + (cost, res) + } +} + +impl GasMeter for GasProfiler { + delegate_mut! { + // Note: we only use this callback for memory tracking, not for charging gas. + fn charge_ld_const_after_deserialization(&mut self, val: impl ValueView) + -> PartialVMResult<()>; + + // Note: we don't use this to charge gas so no need to record anything. + fn charge_native_function_before_execution( + &mut self, + ty_args: impl ExactSizeIterator + Clone, + args: impl ExactSizeIterator + Clone, + ) -> PartialVMResult<()>; + + // Note: we don't use this to charge gas so no need to record anything. + fn charge_drop_frame( + &mut self, + locals: impl Iterator + Clone, + ) -> PartialVMResult<()>; + } + + record_bytecode! { + [POP] + fn charge_pop(&mut self, popped_val: impl ValueView) -> PartialVMResult<()>; + + [LD_CONST] + fn charge_ld_const(&mut self, size: NumBytes) -> PartialVMResult<()>; + + [COPY_LOC] + fn charge_copy_loc(&mut self, val: impl ValueView) -> PartialVMResult<()>; + + [MOVE_LOC] + fn charge_move_loc(&mut self, val: impl ValueView) -> PartialVMResult<()>; + + [ST_LOC] + fn charge_store_loc(&mut self, val: impl ValueView) -> PartialVMResult<()>; + + [PACK] + fn charge_pack( + &mut self, + is_generic: bool, + args: impl ExactSizeIterator + Clone, + ) -> PartialVMResult<()>; + + [UNPACK] + fn charge_unpack( + &mut self, + is_generic: bool, + args: impl ExactSizeIterator + Clone, + ) -> PartialVMResult<()>; + + [READ_REF] + fn charge_read_ref(&mut self, val: impl ValueView) -> PartialVMResult<()>; + + [WRITE_REF] + fn charge_write_ref( + &mut self, + new_val: impl ValueView, + old_val: impl ValueView, + ) -> PartialVMResult<()>; + + [EQ] + fn charge_eq(&mut self, lhs: impl ValueView, rhs: impl ValueView) -> PartialVMResult<()>; + + [NEQ] + fn charge_neq(&mut self, lhs: impl ValueView, rhs: impl ValueView) -> PartialVMResult<()>; + + [ + match (is_mut, is_generic) { + (false, false) => IMM_BORROW_GLOBAL, + (false, true) => IMM_BORROW_GLOBAL_GENERIC, + (true, false) => MUT_BORROW_GLOBAL, + (true, true) => MUT_BORROW_GLOBAL_GENERIC + } + ] + fn charge_borrow_global( + &mut self, + is_mut: bool, + is_generic: bool, + ty: impl TypeView, + is_success: bool, + ) -> PartialVMResult<()>; + + [if is_generic { EXISTS } else { EXISTS_GENERIC }] + fn charge_exists( + &mut self, + is_generic: bool, + ty: impl TypeView, + exists: bool, + ) -> PartialVMResult<()>; + + [if is_generic { MOVE_FROM } else { MOVE_FROM_GENERIC }] + fn charge_move_from( + &mut self, + is_generic: bool, + ty: impl TypeView, + val: Option, + ) -> PartialVMResult<()>; + + [if is_generic { MOVE_TO } else { MOVE_TO_GENERIC }] + fn charge_move_to( + &mut self, + is_generic: bool, + ty: impl TypeView, + val: impl ValueView, + is_success: bool, + ) -> PartialVMResult<()>; + + [VEC_PACK] + fn charge_vec_pack<'a>( + &mut self, + ty: impl TypeView + 'a, + args: impl ExactSizeIterator + Clone, + ) -> PartialVMResult<()>; + + [VEC_LEN] + fn charge_vec_len(&mut self, ty: impl TypeView) -> PartialVMResult<()>; + + [VEC_IMM_BORROW] + fn charge_vec_borrow( + &mut self, + is_mut: bool, + ty: impl TypeView, + is_success: bool, + ) -> PartialVMResult<()>; + + [VEC_PUSH_BACK] + fn charge_vec_push_back( + &mut self, + ty: impl TypeView, + val: impl ValueView, + ) -> PartialVMResult<()>; + + [VEC_POP_BACK] + fn charge_vec_pop_back( + &mut self, + ty: impl TypeView, + val: Option, + ) -> PartialVMResult<()>; + + [VEC_UNPACK] + fn charge_vec_unpack( + &mut self, + ty: impl TypeView, + expect_num_elements: NumArgs, + elems: impl ExactSizeIterator + Clone, + ) -> PartialVMResult<()>; + + [VEC_SWAP] + fn charge_vec_swap(&mut self, ty: impl TypeView) -> PartialVMResult<()>; + } + + fn balance_internal(&self) -> InternalGas { + self.base.balance_internal() + } + + fn charge_simple_instr(&mut self, instr: SimpleInstruction) -> PartialVMResult<()> { + let (cost, res) = self.delegate_charge(|base| base.charge_simple_instr(instr)); + + self.record_bytecode(instr.to_opcode(), cost); + + // If we encounter a Ret instruction, it means the function has exited, + // and we need to convert the current CallFrame into a GasEvent. + // [call_frame_1, call_frame_2, call_frame_3] + // [call_frame_1, call_frame_2(events: [Bytecode::Op, Call(call_frame_3)])] + if matches!(instr, SimpleInstruction::Ret) && self.frames.read().unwrap().len() > 1 { + let cur_frame = self + .frames + .write() + .unwrap() + .pop() + .expect("frame must exist"); + let mut call_frames = self.frames.write().unwrap(); + let last_frame = call_frames.last_mut().expect("frame must exist"); + last_frame.events.push(ExecutionGasEvent::Call(cur_frame)); + } + + res + } + + fn charge_br_true(&mut self, target_offset: Option) -> PartialVMResult<()> { + let (cost, res) = self.delegate_charge(|base| base.charge_br_true(target_offset)); + + self.record_bytecode(Opcodes::BR_TRUE, cost); + if let Some(offset) = target_offset { + self.record_offset(offset); + } + + res + } + + fn charge_br_false(&mut self, target_offset: Option) -> PartialVMResult<()> { + let (cost, res) = self.delegate_charge(|base| base.charge_br_false(target_offset)); + + self.record_bytecode(Opcodes::BR_FALSE, cost); + if let Some(offset) = target_offset { + self.record_offset(offset); + } + + res + } + + fn charge_branch(&mut self, target_offset: CodeOffset) -> PartialVMResult<()> { + let (cost, res) = self.delegate_charge(|base| base.charge_branch(target_offset)); + + self.record_bytecode(Opcodes::BRANCH, cost); + self.record_offset(target_offset); + + res + } + + fn charge_call( + &mut self, + module_id: &ModuleId, + func_name: &str, + args: impl ExactSizeIterator + Clone, + num_locals: NumArgs, + ) -> PartialVMResult<()> { + let (cost, res) = + self.delegate_charge(|base| base.charge_call(module_id, func_name, args, num_locals)); + + //println!("charge_call {:?}::{:?}", module_id, func_name); + + self.record_bytecode(Opcodes::CALL, cost); + self.frames.write().unwrap().push(CallFrame::new_function( + module_id.clone(), + Identifier::new(func_name).unwrap(), + vec![], + )); + + res + } + + fn charge_call_generic( + &mut self, + module_id: &ModuleId, + func_name: &str, + ty_args: impl ExactSizeIterator + Clone, + args: impl ExactSizeIterator + Clone, + num_locals: NumArgs, + ) -> PartialVMResult<()> { + let ty_tags = ty_args + .clone() + .map(|ty| ty.to_type_tag()) + .collect::>(); + + let (cost, res) = self.delegate_charge(|base| { + base.charge_call_generic(module_id, func_name, ty_args, args, num_locals) + }); + + self.record_bytecode(Opcodes::CALL_GENERIC, cost); + self.frames.write().unwrap().push(CallFrame::new_function( + module_id.clone(), + Identifier::new(func_name).unwrap(), + ty_tags, + )); + + res + } + + fn charge_load_resource( + &mut self, + addr: AccountAddress, + ty: impl TypeView, + val: Option, + bytes_loaded: NumBytes, + ) -> PartialVMResult<()> { + let ty_tag = ty.to_type_tag(); + + let (cost, res) = + self.delegate_charge(|base| base.charge_load_resource(addr, ty, val, bytes_loaded)); + + self.record_gas_event(ExecutionGasEvent::LoadResource { + addr, + ty: ty_tag, + cost, + }); + + res + } + + fn charge_native_function( + &mut self, + amount: InternalGas, + ret_vals: Option + Clone>, + ) -> PartialVMResult<()> { + let (cost, res) = + self.delegate_charge(|base| base.charge_native_function(amount, ret_vals)); + + // Whenever a function gets called, the VM will notify the gas profiler + // via `charge_call/charge_call_generic`. + // + // At this point of time, the gas profiler does not yet have an efficient way to determine + // whether the function is a native or not, so it will blindly create a new frame. + // + // Later when it realizes the function is native, it will transform the original frame + // into a native-specific event that does not contain recursive structures. + let cur = self + .frames + .write() + .unwrap() + .pop() + .expect("frame must exist"); + let (module_id, name, ty_args) = match cur.name { + FrameName::Function { + module_id, + name, + ty_args, + } => (module_id, name, ty_args), + FrameName::Script => unreachable!(), + }; + // The following line of code is needed for correctness. + // + // This is because additional gas events may be produced after the frame has been + // created and these events need to be preserved. + self.frames + .write() + .unwrap() + .last_mut() + .unwrap() + .events + .extend(cur.events); + + self.record_gas_event(ExecutionGasEvent::CallNative { + module_id, + fn_name: name, + ty_args, + cost, + }); + + res + } +} + +pub trait ProfileGasMeter { + fn finish(&mut self) -> TransactionGasLog; +} + +impl ProfileGasMeter for GasProfiler { + fn finish(&mut self) -> TransactionGasLog { + while self.frames.read().unwrap().len() > 1 { + let cur = self + .frames + .write() + .unwrap() + .pop() + .expect("frame must exist"); + let mut call_frames = self.frames.write().unwrap(); + let last = call_frames.last_mut().expect("frame must exist"); + last.events.push(ExecutionGasEvent::Call(cur)); + } + + let exec_io = ExecutionAndIOCosts { + total: self.base.balance_internal(), + call_graph: self + .frames + .write() + .unwrap() + .pop() + .expect("frame must exist"), + }; + + self.stop_metering(); + + TransactionGasLog { + exec_io, + storage: InternalGas::zero(), + } + } +} + +impl ClassifiedGasMeter for GasProfiler { + fn charge_execution(&mut self, _gas_cost: u64) -> PartialVMResult<()> { + Ok(()) + } + + fn charge_io_write(&mut self, _data_size: u64) -> PartialVMResult<()> { + Ok(()) + } + + fn check_constrains(&self, _max_gas_amount: u64) -> PartialVMResult<()> { + Ok(()) + } + + fn gas_statement(&self) -> GasStatement { + GasStatement { + execution_gas_used: InternalGas::zero(), + storage_gas_used: InternalGas::zero(), + } + } +} + +impl SwitchableGasMeter for GasProfiler { + fn stop_metering(&mut self) { + self.metering = false; + } + + fn start_metering(&mut self) { + self.metering = true; + } + + fn is_metering(&self) -> bool { + self.metering + } +} + +pub fn new_gas_profiler(action: MoveAction, base_gas_meter: G) -> GasProfiler { + match action { + MoveAction::Script(_) => unreachable!("Script payload is not supported yet"), + MoveAction::Function(call) => GasProfiler::new_function( + base_gas_meter, + call.function_id.module_id, + call.function_id.function_name, + call.ty_args, + ), + MoveAction::ModuleBundle(_) => unreachable!("ModuleBundle payload is not supported yet"), + } +} diff --git a/moveos/moveos-gas-profiling/src/render.rs b/moveos/moveos-gas-profiling/src/render.rs new file mode 100644 index 0000000000..cb6c239de5 --- /dev/null +++ b/moveos/moveos-gas-profiling/src/render.rs @@ -0,0 +1,67 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +use crate::log::FrameName; +use move_core_types::account_address::AccountAddress; +use move_core_types::identifier::IdentStr; +use move_core_types::language_storage::{ModuleId, TypeTag}; +use std::fmt; +use std::fmt::Display; + +/// Wrapper to help render the underlying data in human readable formats that are +/// desirable for textual outputs and flamegraphs. +pub(crate) struct Render<'a, T>(pub &'a T); + +impl<'a> Display for Render<'a, AccountAddress> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let addr_short = self.0.short_str_lossless(); + write!(f, "0x")?; + if addr_short.len() > 4 { + write!(f, "{}..", &addr_short[..4]) + } else { + write!(f, "{}", addr_short) + } + } +} + +impl<'a> Display for Render<'a, ModuleId> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}::{}", Render(self.0.address()), self.0.name()) + } +} + +impl<'a> Display for Render<'a, (&'a ModuleId, &'a IdentStr, &'a [TypeTag])> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}::{}", Render(self.0 .0), self.0 .1)?; + if !self.0 .2.is_empty() { + write!( + f, + "<{}>", + self.0 + .2 + .iter() + .map(|ty| format!("{}", ty)) + .collect::>() + .join(",") + )?; + } + Ok(()) + } +} + +impl Display for FrameName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Script => write!(f, "