From 808a26afbf36dafa688c3c2e1d36fbb793cf8113 Mon Sep 17 00:00:00 2001 From: Preston Evans <32944016+preston-evans98@users.noreply.github.com> Date: Thu, 28 Sep 2023 23:57:04 -0700 Subject: [PATCH 01/12] Make genesis config serializable (#956) * Make genesis config serializable * Evm config serde * Fix chain state integ test * fix vec-setter --- Cargo.lock | 1 + .../methods/guest-celestia/Cargo.lock | 7 +++++++ .../demo-prover/methods/guest-mock/Cargo.lock | 7 +++++++ examples/simple-nft-module/Cargo.toml | 5 ++--- examples/simple-nft-module/src/lib.rs | 2 ++ .../examples/sov-value-setter/Cargo.toml | 4 ++-- .../examples/sov-value-setter/src/lib.rs | 5 +---- .../examples/sov-vec-setter/Cargo.toml | 4 ++-- .../examples/sov-vec-setter/src/lib.rs | 2 ++ .../integration-tests/Cargo.toml | 1 + .../module-template/Cargo.toml | 4 ++-- .../module-template/src/lib.rs | 2 ++ .../sov-accounts/Cargo.toml | 4 ++-- .../sov-accounts/src/lib.rs | 3 ++- .../module-implementations/sov-bank/Cargo.toml | 4 ++-- .../module-implementations/sov-bank/src/lib.rs | 3 +++ .../sov-bank/src/token.rs | 7 ++++--- .../sov-chain-state/src/lib.rs | 1 + .../module-implementations/sov-evm/src/lib.rs | 4 ++-- .../sov-nft-module/Cargo.toml | 5 ++--- .../sov-nft-module/src/lib.rs | 2 ++ .../sov-prover-incentives/Cargo.toml | 4 ++-- .../sov-prover-incentives/src/lib.rs | 2 ++ .../sov-sequencer-registry/Cargo.toml | 3 +-- .../sov-sequencer-registry/src/lib.rs | 2 ++ module-system/sov-modules-api/Cargo.toml | 3 +-- .../sov-modules-api/src/default_signature.rs | 7 ++----- module-system/sov-modules-api/src/lib.rs | 17 +++++++++++------ .../sov-modules-macros/src/dispatch/genesis.rs | 1 + 29 files changed, 73 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3be97958ed..2ba866a41a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3542,6 +3542,7 @@ version = "0.2.0" dependencies = [ "anyhow", "borsh", + "serde", "sov-chain-state", "sov-data-generators", "sov-modules-api", diff --git a/examples/demo-prover/methods/guest-celestia/Cargo.lock b/examples/demo-prover/methods/guest-celestia/Cargo.lock index e377f11e3d..611c388d72 100644 --- a/examples/demo-prover/methods/guest-celestia/Cargo.lock +++ b/examples/demo-prover/methods/guest-celestia/Cargo.lock @@ -384,6 +384,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60f6d271ca33075c88028be6f04d502853d63a5ece419d269c15315d4fc1cf1d" dependencies = [ "pkcs8", + "serde", "signature", ] @@ -408,6 +409,7 @@ checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980" dependencies = [ "curve25519-dalek", "ed25519", + "serde", "sha2 0.10.6", ] @@ -1274,6 +1276,7 @@ version = "0.2.0" dependencies = [ "anyhow", "borsh", + "serde", "sov-modules-api", "sov-state", "thiserror", @@ -1285,6 +1288,7 @@ version = "0.2.0" dependencies = [ "anyhow", "borsh", + "serde", "sov-modules-api", "sov-rollup-interface", "sov-state", @@ -1419,6 +1423,7 @@ version = "0.2.0" dependencies = [ "anyhow", "borsh", + "serde", "sov-modules-api", "sov-modules-macros", "sov-state", @@ -1459,6 +1464,7 @@ version = "0.2.0" dependencies = [ "anyhow", "borsh", + "serde", "sov-bank", "sov-modules-api", "sov-state", @@ -1500,6 +1506,7 @@ version = "0.2.0" dependencies = [ "anyhow", "borsh", + "serde", "sov-modules-api", "sov-state", "thiserror", diff --git a/examples/demo-prover/methods/guest-mock/Cargo.lock b/examples/demo-prover/methods/guest-mock/Cargo.lock index 7ad1f92f1d..df398c1a33 100644 --- a/examples/demo-prover/methods/guest-mock/Cargo.lock +++ b/examples/demo-prover/methods/guest-mock/Cargo.lock @@ -309,6 +309,7 @@ version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60f6d271ca33075c88028be6f04d502853d63a5ece419d269c15315d4fc1cf1d" dependencies = [ + "serde", "signature", ] @@ -320,6 +321,7 @@ checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980" dependencies = [ "curve25519-dalek", "ed25519", + "serde", "sha2", ] @@ -860,6 +862,7 @@ version = "0.2.0" dependencies = [ "anyhow", "borsh", + "serde", "sov-modules-api", "sov-state", "thiserror", @@ -871,6 +874,7 @@ version = "0.2.0" dependencies = [ "anyhow", "borsh", + "serde", "sov-modules-api", "sov-rollup-interface", "sov-state", @@ -978,6 +982,7 @@ version = "0.2.0" dependencies = [ "anyhow", "borsh", + "serde", "sov-modules-api", "sov-modules-macros", "sov-state", @@ -1019,6 +1024,7 @@ version = "0.2.0" dependencies = [ "anyhow", "borsh", + "serde", "sov-bank", "sov-modules-api", "sov-state", @@ -1060,6 +1066,7 @@ version = "0.2.0" dependencies = [ "anyhow", "borsh", + "serde", "sov-modules-api", "sov-state", "thiserror", diff --git a/examples/simple-nft-module/Cargo.toml b/examples/simple-nft-module/Cargo.toml index a4d6446c87..651b9b0348 100644 --- a/examples/simple-nft-module/Cargo.toml +++ b/examples/simple-nft-module/Cargo.toml @@ -12,7 +12,7 @@ publish = false [dependencies] anyhow = { workspace = true } borsh = { workspace = true, features = ["rc"] } -serde = { workspace = true, optional = true } +serde = { workspace = true } sov-modules-api = { path = "../../module-system/sov-modules-api" } sov-state = { path = "../../module-system/sov-state" } @@ -27,6 +27,5 @@ simple-nft-module = { version = "*", features = ["native"], path = "." } [features] default = [] -serde = ["dep:serde"] -native = ["serde", "sov-state/native", "sov-modules-api/native", "jsonrpsee"] +native = ["sov-state/native", "sov-modules-api/native", "jsonrpsee"] test = ["native"] diff --git a/examples/simple-nft-module/src/lib.rs b/examples/simple-nft-module/src/lib.rs index 70ea4b156f..16bce9c15c 100644 --- a/examples/simple-nft-module/src/lib.rs +++ b/examples/simple-nft-module/src/lib.rs @@ -8,6 +8,7 @@ mod genesis; mod query; #[cfg(feature = "native")] pub use query::*; +use serde::{Deserialize, Serialize}; use sov_modules_api::{CallResponse, Context, Error, Module, ModuleInfo, WorkingSet}; #[derive(ModuleInfo, Clone)] @@ -29,6 +30,7 @@ pub struct NonFungibleToken { /// Config for the NonFungibleToken module. /// Sets admin and existing owners. +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct NonFungibleTokenConfig { /// Admin of the NonFungibleToken module. pub admin: C::Address, diff --git a/module-system/module-implementations/examples/sov-value-setter/Cargo.toml b/module-system/module-implementations/examples/sov-value-setter/Cargo.toml index 392fbcf4ac..0b11abf11f 100644 --- a/module-system/module-implementations/examples/sov-value-setter/Cargo.toml +++ b/module-system/module-implementations/examples/sov-value-setter/Cargo.toml @@ -22,7 +22,7 @@ anyhow = { workspace = true } sov-modules-api = { path = "../../../sov-modules-api" } sov-state = { path = "../../../sov-state" } schemars = { workspace = true, optional = true } -serde = { workspace = true, optional = true } +serde = { workspace = true } serde_json = { workspace = true, optional = true } thiserror = { workspace = true } borsh = { workspace = true, features = ["rc"] } @@ -31,4 +31,4 @@ clap = { workspace = true, optional = true } [features] default = [] -native = ["serde", "serde_json", "jsonrpsee", "schemars", "clap", "sov-modules-api/native", "sov-state/native"] +native = ["serde_json", "jsonrpsee", "schemars", "clap", "sov-modules-api/native", "sov-state/native"] diff --git a/module-system/module-implementations/examples/sov-value-setter/src/lib.rs b/module-system/module-implementations/examples/sov-value-setter/src/lib.rs index a49519240a..a66addb136 100644 --- a/module-system/module-implementations/examples/sov-value-setter/src/lib.rs +++ b/module-system/module-implementations/examples/sov-value-setter/src/lib.rs @@ -15,10 +15,7 @@ pub use query::*; use sov_modules_api::{Error, ModuleInfo, WorkingSet}; /// Initial configuration for sov-value-setter module. -#[cfg_attr( - feature = "native", - derive(serde::Serialize, serde::Deserialize, Debug, PartialEq) -)] +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)] pub struct ValueSetterConfig { /// Admin of the module. pub admin: C::Address, diff --git a/module-system/module-implementations/examples/sov-vec-setter/Cargo.toml b/module-system/module-implementations/examples/sov-vec-setter/Cargo.toml index 0f3d77719c..5adfa4f79f 100644 --- a/module-system/module-implementations/examples/sov-vec-setter/Cargo.toml +++ b/module-system/module-implementations/examples/sov-vec-setter/Cargo.toml @@ -22,7 +22,7 @@ sov-modules-api = { path = "../../../sov-modules-api", default-features = false, sov-state = { path = "../../../sov-state", default-features = false } sov-rollup-interface = { path = "../../../../rollup-interface" } schemars = { workspace = true, optional = true } -serde = { workspace = true, optional = true } +serde = { workspace = true } serde_json = { workspace = true, optional = true } thiserror = { workspace = true } borsh = { workspace = true, features = ["rc"] } @@ -31,4 +31,4 @@ clap = { workspace = true, optional = true } [features] default = ["native"] -native = ["serde", "serde_json", "jsonrpsee", "schemars", "clap", "sov-modules-api/native"] +native = ["serde_json", "jsonrpsee", "schemars", "clap", "sov-modules-api/native"] diff --git a/module-system/module-implementations/examples/sov-vec-setter/src/lib.rs b/module-system/module-implementations/examples/sov-vec-setter/src/lib.rs index 7a74628ebd..a444dffa42 100644 --- a/module-system/module-implementations/examples/sov-vec-setter/src/lib.rs +++ b/module-system/module-implementations/examples/sov-vec-setter/src/lib.rs @@ -9,9 +9,11 @@ mod query; pub use call::CallMessage; #[cfg(feature = "native")] pub use query::*; +use serde::{Deserialize, Serialize}; use sov_modules_api::{Error, ModuleInfo, WorkingSet}; /// Initial configuration for sov-vec-setter module. +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct VecSetterConfig { /// Admin of the module. pub admin: C::Address, diff --git a/module-system/module-implementations/integration-tests/Cargo.toml b/module-system/module-implementations/integration-tests/Cargo.toml index 269e24e59a..27fc047d3a 100644 --- a/module-system/module-implementations/integration-tests/Cargo.toml +++ b/module-system/module-implementations/integration-tests/Cargo.toml @@ -15,6 +15,7 @@ resolver = "2" anyhow = { workspace = true } borsh = { workspace = true, features = ["rc"] } tempfile = { workspace = true } +serde = { workspace = true } sov-modules-api = { path = "../../sov-modules-api", features = ["native"] } sov-state = { path = "../../sov-state", features = ["native"] } diff --git a/module-system/module-implementations/module-template/Cargo.toml b/module-system/module-implementations/module-template/Cargo.toml index c6ca48549e..38974ba2da 100644 --- a/module-system/module-implementations/module-template/Cargo.toml +++ b/module-system/module-implementations/module-template/Cargo.toml @@ -16,7 +16,7 @@ anyhow = { workspace = true } borsh = { workspace = true, features = ["rc"] } thiserror = { workspace = true } schemars = { workspace = true, optional = true } -serde = { workspace = true, optional = true } +serde = { workspace = true } serde_json = { workspace = true, optional = true } sov-bank = { path = "../sov-bank" } @@ -31,4 +31,4 @@ module-template = { path = ".", version = "*", features = ["native"] } [features] default = [] -native = ["serde", "serde_json", "schemars", "sov-modules-api/native"] +native = ["serde_json", "schemars", "sov-modules-api/native"] diff --git a/module-system/module-implementations/module-template/src/lib.rs b/module-system/module-implementations/module-template/src/lib.rs index 1daf9e8cf7..00c63a402a 100644 --- a/module-system/module-implementations/module-template/src/lib.rs +++ b/module-system/module-implementations/module-template/src/lib.rs @@ -5,8 +5,10 @@ mod query; pub use call::CallMessage; #[cfg(feature = "native")] pub use query::*; +use serde::{Deserialize, Serialize}; use sov_modules_api::{Error, ModuleInfo, WorkingSet}; +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExampleModuleConfig {} /// A new module: diff --git a/module-system/module-implementations/sov-accounts/Cargo.toml b/module-system/module-implementations/sov-accounts/Cargo.toml index 35ab123d66..fb0aac2861 100644 --- a/module-system/module-implementations/sov-accounts/Cargo.toml +++ b/module-system/module-implementations/sov-accounts/Cargo.toml @@ -16,7 +16,7 @@ anyhow = { workspace = true } arbitrary = { workspace = true, optional = true } borsh = { workspace = true, features = ["rc"] } schemars = { workspace = true, optional = true } -serde = { workspace = true, optional = true } +serde = { workspace = true } serde_json = { workspace = true, optional = true } thiserror = { workspace = true } clap = { workspace = true, optional = true } @@ -33,4 +33,4 @@ tempfile = { workspace = true } [features] default = [] arbitrary = ["dep:arbitrary", "sov-state/arbitrary", "sov-modules-api/arbitrary"] -native = ["serde", "serde_json", "jsonrpsee", "schemars", "clap", "sov-state/native", "sov-modules-api/native"] +native = ["serde_json", "jsonrpsee", "schemars", "clap", "sov-state/native", "sov-modules-api/native"] diff --git a/module-system/module-implementations/sov-accounts/src/lib.rs b/module-system/module-implementations/sov-accounts/src/lib.rs index 9f9c987b39..922fce9f4d 100644 --- a/module-system/module-implementations/sov-accounts/src/lib.rs +++ b/module-system/module-implementations/sov-accounts/src/lib.rs @@ -15,7 +15,8 @@ pub use call::{CallMessage, UPDATE_ACCOUNT_MSG}; use sov_modules_api::{Context, Error, ModuleInfo, WorkingSet}; /// Initial configuration for sov-accounts module. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(bound = "C::PublicKey: serde::Serialize + serde::de::DeserializeOwned")] pub struct AccountConfig { /// Public keys to initialize the rollup. pub pub_keys: Vec, diff --git a/module-system/module-implementations/sov-bank/Cargo.toml b/module-system/module-implementations/sov-bank/Cargo.toml index 03906f8976..18a95f3435 100644 --- a/module-system/module-implementations/sov-bank/Cargo.toml +++ b/module-system/module-implementations/sov-bank/Cargo.toml @@ -17,7 +17,7 @@ borsh = { workspace = true, features = ["rc"] } clap = { workspace = true, optional = true, features = ["derive"] } jsonrpsee = { workspace = true, features = ["macros", "client-core", "server"], optional = true } schemars = { workspace = true, optional = true } -serde = { workspace = true, optional = true } +serde = { workspace = true } serde_json = { workspace = true, optional = true } thiserror = { workspace = true } @@ -32,5 +32,5 @@ tempfile = { workspace = true } [features] default = [] -native = ["serde", "serde_json", "jsonrpsee", "clap", "schemars", "sov-state/native", "sov-modules-api/native", ] +native = ["serde_json", "jsonrpsee", "clap", "schemars", "sov-state/native", "sov-modules-api/native", ] cli = ["native"] diff --git a/module-system/module-implementations/sov-bank/src/lib.rs b/module-system/module-implementations/sov-bank/src/lib.rs index cd664fd3c2..e8d3425e40 100644 --- a/module-system/module-implementations/sov-bank/src/lib.rs +++ b/module-system/module-implementations/sov-bank/src/lib.rs @@ -12,6 +12,7 @@ pub mod utils; /// Specifies the call methods using in that module. pub use call::CallMessage; +use serde::{Deserialize, Serialize}; use sov_modules_api::{CallResponse, Error, ModuleInfo, WorkingSet}; use token::Token; /// Specifies an interface to interact with tokens. @@ -21,6 +22,7 @@ pub use utils::{get_genesis_token_address, get_token_address}; /// [`TokenConfig`] specifies a configuration used when generating a token for the bank /// module. +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct TokenConfig { /// The name of the token. pub token_name: String, @@ -33,6 +35,7 @@ pub struct TokenConfig { } /// Initial configuration for sov-bank module. +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct BankConfig { /// A list of configurations for the initial tokens. pub tokens: Vec>, diff --git a/module-system/module-implementations/sov-bank/src/token.rs b/module-system/module-implementations/sov-bank/src/token.rs index c552d1b4f8..8ad62943a3 100644 --- a/module-system/module-implementations/sov-bank/src/token.rs +++ b/module-system/module-implementations/sov-bank/src/token.rs @@ -6,6 +6,7 @@ use std::fmt::Formatter; use std::num::ParseIntError; use anyhow::{bail, Context, Result}; +use serde::{Deserialize, Serialize}; use sov_modules_api::WorkingSet; use sov_state::Prefix; #[cfg(feature = "native")] @@ -21,13 +22,13 @@ pub type Amount = u64; /// (type [`sov_modules_api::Spec::Address`]). #[cfg_attr( feature = "native", - derive(serde::Serialize), - derive(serde::Deserialize), derive(clap::Parser), derive(schemars::JsonSchema), schemars(bound = "C::Address: ::schemars::JsonSchema", rename = "Coins") )] -#[derive(borsh::BorshDeserialize, borsh::BorshSerialize, Debug, PartialEq, Clone)] +#[derive( + borsh::BorshDeserialize, borsh::BorshSerialize, Debug, PartialEq, Clone, Serialize, Deserialize, +)] pub struct Coins { /// An `amount` of coins stored. pub amount: Amount, diff --git a/module-system/module-implementations/sov-chain-state/src/lib.rs b/module-system/module-implementations/sov-chain-state/src/lib.rs index f7bcce225c..4de0fbe14c 100644 --- a/module-system/module-implementations/sov-chain-state/src/lib.rs +++ b/module-system/module-implementations/sov-chain-state/src/lib.rs @@ -147,6 +147,7 @@ pub struct ChainState } /// Initial configuration of the chain state +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ChainStateConfig { /// Initial slot height pub initial_slot_height: TransitionHeight, diff --git a/module-system/module-implementations/sov-evm/src/lib.rs b/module-system/module-implementations/sov-evm/src/lib.rs index ea65e39bee..b256b2ae52 100644 --- a/module-system/module-implementations/sov-evm/src/lib.rs +++ b/module-system/module-implementations/sov-evm/src/lib.rs @@ -48,7 +48,7 @@ mod experimental { }; /// Evm account. - #[derive(Clone, Debug)] + #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct AccountData { /// Account address. pub address: Address, @@ -75,7 +75,7 @@ mod experimental { } /// Genesis configuration. - #[derive(Clone, Debug)] + #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct EvmConfig { /// Genesis accounts. pub data: Vec, diff --git a/module-system/module-implementations/sov-nft-module/Cargo.toml b/module-system/module-implementations/sov-nft-module/Cargo.toml index c11afd5293..a063098d05 100644 --- a/module-system/module-implementations/sov-nft-module/Cargo.toml +++ b/module-system/module-implementations/sov-nft-module/Cargo.toml @@ -12,7 +12,7 @@ publish = false [dependencies] anyhow = { workspace = true } borsh = { workspace = true, features = ["rc"] } -serde = { workspace = true, optional = true } +serde = { workspace = true } schemars = { workspace = true, optional = true } serde_json = { workspace = true, optional = true } jsonrpsee = { workspace = true, features = ["macros", "client-core", "server"], optional = true } @@ -29,6 +29,5 @@ sov-nft-module = { version = "*", features = ["native"], path = "." } [features] default = [] -serde = ["dep:serde"] -native = ["serde", "serde_json", "jsonrpsee", "schemars", "sov-state/native", "sov-modules-api/native", ] +native = ["serde_json", "jsonrpsee", "schemars", "sov-state/native", "sov-modules-api/native", ] test = ["native"] diff --git a/module-system/module-implementations/sov-nft-module/src/lib.rs b/module-system/module-implementations/sov-nft-module/src/lib.rs index 3470fec3fd..65df31d343 100644 --- a/module-system/module-implementations/sov-nft-module/src/lib.rs +++ b/module-system/module-implementations/sov-nft-module/src/lib.rs @@ -14,6 +14,7 @@ use nft::*; mod query; #[cfg(feature = "native")] pub use query::*; +use serde::{Deserialize, Serialize}; use sov_modules_api::{CallResponse, Context, Error, Module, ModuleInfo, StateMap, WorkingSet}; /// Utility functions. pub mod utils; @@ -38,6 +39,7 @@ pub struct NonFungibleToken { /// Config for the NonFungibleToken module. /// Sets admin and existing owners. +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct NonFungibleTokenConfig {} impl Module for NonFungibleToken { diff --git a/module-system/module-implementations/sov-prover-incentives/Cargo.toml b/module-system/module-implementations/sov-prover-incentives/Cargo.toml index 11255e5fe2..381d4aa609 100644 --- a/module-system/module-implementations/sov-prover-incentives/Cargo.toml +++ b/module-system/module-implementations/sov-prover-incentives/Cargo.toml @@ -22,7 +22,7 @@ anyhow = { workspace = true } borsh = { workspace = true, features = ["rc"] } bincode = { workspace = true } schemars = { workspace = true, optional = true } -serde = { workspace = true, optional = true } +serde = { workspace = true } serde_json = { workspace = true, optional = true } sov-bank = { path = "../sov-bank", version = "0.2" } @@ -32,4 +32,4 @@ sov-state = { path = "../../sov-state", version = "0.2" } [features] default = [] -native = ["serde", "serde_json", "schemars", "sov-state/native", "sov-modules-api/native"] +native = ["serde_json", "schemars", "sov-state/native", "sov-modules-api/native"] diff --git a/module-system/module-implementations/sov-prover-incentives/src/lib.rs b/module-system/module-implementations/sov-prover-incentives/src/lib.rs index 2fa93e06ed..44cd7414b4 100644 --- a/module-system/module-implementations/sov-prover-incentives/src/lib.rs +++ b/module-system/module-implementations/sov-prover-incentives/src/lib.rs @@ -14,6 +14,7 @@ pub use call::CallMessage; /// The response type used by RPC queries. #[cfg(feature = "native")] pub use query::*; +use serde::{Deserialize, Serialize}; use sov_modules_api::{Context, Error, ModuleInfo, WorkingSet, Zkvm}; use sov_state::codec::BcsCodec; @@ -21,6 +22,7 @@ use sov_state::codec::BcsCodec; /// address of the bonding token, the minimum bond, the commitment to /// the allowed verifier method and a set of initial provers with their /// bonding amount. +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProverIncentivesConfig { /// The address of the token to be used for bonding. bonding_token_address: C::Address, diff --git a/module-system/module-implementations/sov-sequencer-registry/Cargo.toml b/module-system/module-implementations/sov-sequencer-registry/Cargo.toml index ff677b951f..c073ec2fa6 100644 --- a/module-system/module-implementations/sov-sequencer-registry/Cargo.toml +++ b/module-system/module-implementations/sov-sequencer-registry/Cargo.toml @@ -24,7 +24,7 @@ sov-bank = { path = "../sov-bank", version = "0.2" } sov-modules-api = { path = "../../sov-modules-api", version = "0.2" } sov-state = { path = "../../sov-state", version = "0.2" } schemars = { workspace = true, optional = true } -serde = { workspace = true, optional = true } +serde = { workspace = true } serde_json = { workspace = true, optional = true } borsh = { workspace = true, features = ["rc"] } jsonrpsee = { workspace = true, features = ["macros", "client-core", "server"], optional = true } @@ -37,7 +37,6 @@ sov-zk-cycle-utils = { path = "../../../utils/zk-cycle-utils", version = "0.2", bench = ["sov-zk-cycle-macros/bench", "risc0-zkvm", "risc0-zkvm-platform", "sov-zk-cycle-utils"] default = [] native = [ - "serde", "serde_json", "jsonrpsee", "schemars", diff --git a/module-system/module-implementations/sov-sequencer-registry/src/lib.rs b/module-system/module-implementations/sov-sequencer-registry/src/lib.rs index 7df00cb9e4..a74447e317 100644 --- a/module-system/module-implementations/sov-sequencer-registry/src/lib.rs +++ b/module-system/module-implementations/sov-sequencer-registry/src/lib.rs @@ -16,6 +16,7 @@ mod query; pub use call::CallMessage; #[cfg(feature = "native")] pub use query::*; +use serde::{Deserialize, Serialize}; use sov_modules_api::{CallResponse, Error, ModuleInfo, StateMap, StateValue, WorkingSet}; use sov_state::codec::BcsCodec; @@ -25,6 +26,7 @@ use sov_state::codec::BcsCodec; /// [`Module::genesis`](sov_modules_api::Module::genesis). /// // TODO: Should we allow multiple sequencers in genesis? +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct SequencerConfig { /// The rollup address of the sequencer. pub seq_rollup_address: C::Address, diff --git a/module-system/sov-modules-api/Cargo.toml b/module-system/sov-modules-api/Cargo.toml index 682dd08d41..ab42a5c5fc 100644 --- a/module-system/sov-modules-api/Cargo.toml +++ b/module-system/sov-modules-api/Cargo.toml @@ -32,7 +32,7 @@ hex = { workspace = true, optional = true } clap = { workspace = true, optional = true } schemars = { workspace = true, optional = true, features = [] } -ed25519-dalek = { version = "2.0.0", default-features = false } +ed25519-dalek = { version = "2.0.0", default-features = false, features = ["serde"] } rand = { version = "0.8", optional = true } sov-zk-cycle-macros = { path = "../../utils/zk-cycle-macros", version = "0.2", optional = true } @@ -55,7 +55,6 @@ native = [ "hex", "schemars", "ed25519-dalek/default", - "ed25519-dalek/serde", "ed25519-dalek/rand_core", "clap", "jsonrpsee", diff --git a/module-system/sov-modules-api/src/default_signature.rs b/module-system/sov-modules-api/src/default_signature.rs index ab7be5a2b7..7745b74516 100644 --- a/module-system/sov-modules-api/src/default_signature.rs +++ b/module-system/sov-modules-api/src/default_signature.rs @@ -166,11 +166,8 @@ pub mod private_key { } } -#[cfg_attr( - feature = "native", - derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema) -)] -#[derive(PartialEq, Eq, Clone, Debug)] +#[cfg_attr(feature = "native", derive(schemars::JsonSchema))] +#[derive(PartialEq, Eq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct DefaultPublicKey { #[cfg_attr( feature = "native", diff --git a/module-system/sov-modules-api/src/lib.rs b/module-system/sov-modules-api/src/lib.rs index b5e73d0567..7ef5293b85 100644 --- a/module-system/sov-modules-api/src/lib.rs +++ b/module-system/sov-modules-api/src/lib.rs @@ -153,7 +153,16 @@ pub enum NonInstantiable {} /// PublicKey used in the Module System. pub trait PublicKey: - borsh::BorshDeserialize + borsh::BorshSerialize + Eq + Hash + Clone + Debug + Send + Sync + borsh::BorshDeserialize + + borsh::BorshSerialize + + Eq + + Hash + + Clone + + Debug + + Send + + Sync + + Serialize + + for<'a> Deserialize<'a> { fn to_address(&self) -> A; } @@ -210,11 +219,7 @@ pub trait Spec { /// The public key used for digital signatures #[cfg(feature = "native")] - type PublicKey: PublicKey - + Serialize - + for<'a> Deserialize<'a> - + ::schemars::JsonSchema - + FromStr; + type PublicKey: PublicKey + ::schemars::JsonSchema + FromStr; #[cfg(not(feature = "native"))] type PublicKey: PublicKey; diff --git a/module-system/sov-modules-macros/src/dispatch/genesis.rs b/module-system/sov-modules-macros/src/dispatch/genesis.rs index 2530ee86dd..b41306336d 100644 --- a/module-system/sov-modules-macros/src/dispatch/genesis.rs +++ b/module-system/sov-modules-macros/src/dispatch/genesis.rs @@ -101,6 +101,7 @@ impl GenesisMacro { quote::quote! { #[doc = "Initial configuration for the rollup."] + #[derive(::serde::Deserialize, ::serde::Serialize)] pub struct GenesisConfig #impl_generics #where_clause{ #(#[doc = "Module configuration"] pub #fields)* } From e481346d65d3b681b9926c4252fd359f605f0fbf Mon Sep 17 00:00:00 2001 From: Blazej Kolad Date: Fri, 29 Sep 2023 09:28:30 +0200 Subject: [PATCH 02/12] Introduce: `PublicKeyHex` in `sov-modules-api` (#954) * Add PublicKeyHex * PubKeyHex impl * Update DefaultPublicKey::from_str * Add tests * Remove println * Add doc --- .../methods/guest-celestia/Cargo.lock | 1 + .../demo-prover/methods/guest-mock/Cargo.lock | 1 + module-system/sov-modules-api/Cargo.toml | 3 +- .../sov-modules-api/src/default_signature.rs | 11 +- module-system/sov-modules-api/src/lib.rs | 1 + .../sov-modules-api/src/pub_key_hex.rs | 124 ++++++++++++++++++ 6 files changed, 130 insertions(+), 11 deletions(-) create mode 100644 module-system/sov-modules-api/src/pub_key_hex.rs diff --git a/examples/demo-prover/methods/guest-celestia/Cargo.lock b/examples/demo-prover/methods/guest-celestia/Cargo.lock index 611c388d72..da5807d78e 100644 --- a/examples/demo-prover/methods/guest-celestia/Cargo.lock +++ b/examples/demo-prover/methods/guest-celestia/Cargo.lock @@ -1378,6 +1378,7 @@ dependencies = [ "borsh", "derive_more", "ed25519-dalek", + "hex", "jmt", "serde", "sha2 0.10.6", diff --git a/examples/demo-prover/methods/guest-mock/Cargo.lock b/examples/demo-prover/methods/guest-mock/Cargo.lock index df398c1a33..db5de049cc 100644 --- a/examples/demo-prover/methods/guest-mock/Cargo.lock +++ b/examples/demo-prover/methods/guest-mock/Cargo.lock @@ -937,6 +937,7 @@ dependencies = [ "borsh", "derive_more", "ed25519-dalek", + "hex", "jmt", "serde", "sha2", diff --git a/module-system/sov-modules-api/Cargo.toml b/module-system/sov-modules-api/Cargo.toml index ab42a5c5fc..6e2e256e9f 100644 --- a/module-system/sov-modules-api/Cargo.toml +++ b/module-system/sov-modules-api/Cargo.toml @@ -28,7 +28,7 @@ bech32 = { workspace = true } derive_more = { workspace = true } jmt = { workspace = true } serde_json = { workspace = true, optional = true } -hex = { workspace = true, optional = true } +hex = { workspace = true } clap = { workspace = true, optional = true } schemars = { workspace = true, optional = true, features = [] } @@ -52,7 +52,6 @@ default = ["macros"] native = [ "serde_json", "rand", - "hex", "schemars", "ed25519-dalek/default", "ed25519-dalek/rand_core", diff --git a/module-system/sov-modules-api/src/default_signature.rs b/module-system/sov-modules-api/src/default_signature.rs index 7745b74516..48c886f499 100644 --- a/module-system/sov-modules-api/src/default_signature.rs +++ b/module-system/sov-modules-api/src/default_signature.rs @@ -254,15 +254,8 @@ impl FromStr for DefaultPublicKey { type Err = anyhow::Error; fn from_str(s: &str) -> Result { - let bytes = hex::decode(s)?; - - let bytes: [u8; PUBLIC_KEY_LENGTH] = bytes - .try_into() - .map_err(|_| anyhow::anyhow!("Invalid public key size"))?; - - let pub_key = DalekPublicKey::from_bytes(&bytes) - .map_err(|_| anyhow::anyhow!("Invalid public key"))?; - Ok(DefaultPublicKey { pub_key }) + let pk_hex = crate::pub_key_hex::PublicKeyHex::try_from(s)?; + pk_hex.try_into() } } diff --git a/module-system/sov-modules-api/src/lib.rs b/module-system/sov-modules-api/src/lib.rs index 7ef5293b85..053fdbd476 100644 --- a/module-system/sov-modules-api/src/lib.rs +++ b/module-system/sov-modules-api/src/lib.rs @@ -10,6 +10,7 @@ mod dispatch; mod encode; mod error; pub mod hooks; +mod pub_key_hex; #[cfg(feature = "macros")] mod reexport_macros; diff --git a/module-system/sov-modules-api/src/pub_key_hex.rs b/module-system/sov-modules-api/src/pub_key_hex.rs new file mode 100644 index 0000000000..3c5d390c57 --- /dev/null +++ b/module-system/sov-modules-api/src/pub_key_hex.rs @@ -0,0 +1,124 @@ +use derive_more::Display; +use ed25519_dalek::{VerifyingKey as DalekPublicKey, PUBLIC_KEY_LENGTH}; + +/// A hexadecimal representation of a PublicKey. +use crate::default_signature::DefaultPublicKey; +#[derive( + serde::Serialize, + serde::Deserialize, + borsh::BorshDeserialize, + borsh::BorshSerialize, + Debug, + PartialEq, + Clone, + Eq, + Display, +)] +#[serde(try_from = "String", into = "String")] +#[display(fmt = "{}", "hex")] +pub struct PublicKeyHex { + hex: String, +} + +impl TryFrom<&str> for PublicKeyHex { + type Error = anyhow::Error; + + fn try_from(hex: &str) -> Result { + Self::try_from(hex.to_owned()) + } +} + +impl TryFrom for PublicKeyHex { + type Error = anyhow::Error; + + fn try_from(hex: String) -> Result { + if hex.len() & 1 != 0 { + anyhow::bail!("Bad hex conversion: odd input length") + } + + if let Some((index, c)) = hex.chars().enumerate().find(|(_, c)| { + !(matches!(c, '0'..='9' | 'a'..='f') || matches!(c, '0'..='9' | 'A'..='F')) + }) { + anyhow::bail!( + "Bad hex conversion: wrong character `{}` at index {}", + c, + index + ) + } + + Ok(Self { hex }) + } +} + +impl From for String { + fn from(pub_key: PublicKeyHex) -> Self { + pub_key.hex + } +} + +impl From for PublicKeyHex { + fn from(pub_key: DefaultPublicKey) -> Self { + let hex = hex::encode(pub_key.pub_key.as_bytes()); + Self { hex } + } +} + +impl TryFrom for DefaultPublicKey { + type Error = anyhow::Error; + + fn try_from(pub_key: PublicKeyHex) -> Result { + let bytes = hex::decode(pub_key.hex)?; + + let bytes: [u8; PUBLIC_KEY_LENGTH] = bytes + .try_into() + .map_err(|_| anyhow::anyhow!("Invalid public key size"))?; + + let pub_key = DalekPublicKey::from_bytes(&bytes) + .map_err(|_| anyhow::anyhow!("Invalid public key"))?; + + Ok(DefaultPublicKey { pub_key }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::default_signature::private_key::DefaultPrivateKey; + use crate::PrivateKey; + + #[test] + fn test_pub_key_hex() { + let pub_key = DefaultPrivateKey::generate().pub_key(); + let pub_key_hex = PublicKeyHex::try_from(pub_key.clone()).unwrap(); + let converted_pub_key = DefaultPublicKey::try_from(pub_key_hex).unwrap(); + assert_eq!(pub_key, converted_pub_key); + } + + #[test] + fn test_pub_key_hex_str() { + let key = "022e229198d957bf0c0a504e7d7bcec99a1d62cccc7861ed2452676ad0323ad8"; + let pub_key_hex_lower: PublicKeyHex = key.try_into().unwrap(); + let pub_key_hex_upper: PublicKeyHex = key.to_uppercase().try_into().unwrap(); + + let pub_key_lower = DefaultPublicKey::try_from(pub_key_hex_lower).unwrap(); + let pub_key_upper = DefaultPublicKey::try_from(pub_key_hex_upper).unwrap(); + + assert_eq!(pub_key_lower, pub_key_upper) + } + + #[test] + fn test_bad_pub_key_hex_str() { + let key = "022e229198d957Zf0c0a504e7d7bcec99a1d62cccc7861ed2452676ad0323ad8"; + let err = PublicKeyHex::try_from(key).unwrap_err(); + + assert_eq!( + err.to_string(), + "Bad hex conversion: wrong character `Z` at index 14" + ); + + let key = "022"; + let err = PublicKeyHex::try_from(key).unwrap_err(); + + assert_eq!(err.to_string(), "Bad hex conversion: odd input length") + } +} From 91c3423c61c4d1bf39c4916fe0b1d0d371c9f58a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Orkun=20Mahir=20K=C4=B1l=C4=B1=C3=A7?= Date: Fri, 29 Sep 2023 12:46:50 +0300 Subject: [PATCH 03/12] estimate-gas signed (#947) --- examples/demo-rollup/tests/evm/mod.rs | 30 ++- full-node/sov-ethereum/src/lib.rs | 80 ++++++- .../module-implementations/sov-evm/src/lib.rs | 4 + .../sov-evm/src/query.rs | 213 +++++++++++++++++- 4 files changed, 308 insertions(+), 19 deletions(-) diff --git a/examples/demo-rollup/tests/evm/mod.rs b/examples/demo-rollup/tests/evm/mod.rs index cc867c47ae..cebe97a4d5 100644 --- a/examples/demo-rollup/tests/evm/mod.rs +++ b/examples/demo-rollup/tests/evm/mod.rs @@ -92,17 +92,15 @@ impl TestClient { contract_address: H160, set_arg: u32, ) -> PendingTransaction<'_, Http> { - let nonce = self.eth_get_transaction_count(self.from_addr).await; - + // Tx without gas_limit should estimate and include it in send_transaction endpoint + // Tx without nonce should fetch and include it in send_transaction endpoint let req = Eip1559TransactionRequest::new() .from(self.from_addr) .to(contract_address) .chain_id(self.chain_id) - .nonce(nonce) .data(self.contract.set_call_data(set_arg)) .max_priority_fee_per_gas(10u64) - .max_fee_per_gas(MAX_FEE_PER_GAS) - .gas(900000u64); + .max_fee_per_gas(MAX_FEE_PER_GAS); let typed_transaction = TypedTransaction::Eip1559(req); @@ -148,9 +146,17 @@ impl TestClient { .chain_id(self.chain_id) .nonce(nonce) .data(self.contract.set_call_data(set_arg)) - .gas_price(10u64) - .gas(900000u64); + .gas_price(10u64); + + let typed_transaction = TypedTransaction::Legacy(req.clone()); + // Estimate gas on rpc + let gas = self + .eth_estimate_gas(typed_transaction, Some("latest".to_owned())) + .await; + + // Call with the estimated gas + let req = req.gas(gas); let typed_transaction = TypedTransaction::Legacy(req); let response = self @@ -268,6 +274,16 @@ impl TestClient { .map_err(|e| e.into()) } + async fn eth_estimate_gas(&self, tx: TypedTransaction, block_number: Option) -> u64 { + let gas: ethereum_types::U64 = self + .http_client + .request("eth_estimateGas", rpc_params![tx, block_number]) + .await + .unwrap(); + + gas.as_u64() + } + async fn execute(self) -> Result<(), Box> { // Nonce should be 0 in genesis let nonce = self.eth_get_transaction_count(self.from_addr).await; diff --git a/full-node/sov-ethereum/src/lib.rs b/full-node/sov-ethereum/src/lib.rs index b34e57cb34..264e5420a0 100644 --- a/full-node/sov-ethereum/src/lib.rs +++ b/full-node/sov-ethereum/src/lib.rs @@ -18,9 +18,9 @@ pub mod experimental { use jsonrpsee::types::ErrorObjectOwned; use jsonrpsee::RpcModule; use reth_primitives::{ - Address as RethAddress, TransactionSignedNoHash as RethTransactionSignedNoHash, + Address as RethAddress, TransactionSignedNoHash as RethTransactionSignedNoHash, U128, U256, }; - use reth_rpc_types::{TransactionRequest, TypedTransactionRequest}; + use reth_rpc_types::{CallRequest, TransactionRequest, TypedTransactionRequest}; use sov_evm::{CallMessage, Evm, RlpEvmTransaction}; use sov_modules_api::transaction::Transaction; use sov_modules_api::utils::to_jsonrpsee_error_object; @@ -211,6 +211,8 @@ pub mod experimental { let raw_evm_tx = { let mut working_set = WorkingSet::::new(ethereum.storage.clone()); + + // set nonce if none if transaction_request.nonce.is_none() { let nonce = evm .get_transaction_count(from, None, &mut working_set) @@ -219,27 +221,43 @@ pub mod experimental { transaction_request.nonce = Some(nonce); } + // get current chain id let chain_id = evm .chain_id(&mut working_set) .expect("Failed to get chain id") .map(|id| id.as_u64()) .unwrap_or(1); - // TODO: implement gas logic after gas estimation (#906) is implemented - // https://github.com/Sovereign-Labs/sovereign-sdk/issues/906 + // get call request to estimate gas and gas prices + let (call_request, gas_price, max_fee_per_gas) = + get_call_request_and_params(from, chain_id, &transaction_request); + + // estimate gas limit + let gas_limit = U256::from( + evm.eth_estimate_gas(call_request, None, &mut working_set)? + .as_u64(), + ); + + // get typed transaction request let transaction_request = match transaction_request.into_typed_request() { Some(TypedTransactionRequest::Legacy(mut m)) => { m.chain_id = Some(chain_id); + m.gas_limit = gas_limit; + m.gas_price = gas_price; TypedTransactionRequest::Legacy(m) } Some(TypedTransactionRequest::EIP2930(mut m)) => { m.chain_id = chain_id; + m.gas_limit = gas_limit; + m.gas_price = gas_price; TypedTransactionRequest::EIP2930(m) } Some(TypedTransactionRequest::EIP1559(mut m)) => { m.chain_id = chain_id; + m.gas_limit = gas_limit; + m.max_fee_per_gas = max_fee_per_gas; TypedTransactionRequest::EIP1559(m) } @@ -251,10 +269,12 @@ pub mod experimental { } }; + // get raw transaction let transaction = into_transaction(transaction_request).map_err(|_| { to_jsonrpsee_error_object("Invalid types in transaction request", ETH_RPC_ERROR) })?; + // sign transaction let signed_tx = ethereum .eth_rpc_config .eth_signer @@ -338,4 +358,56 @@ pub mod experimental { let bytes: [u8; 16] = bytes[16..].try_into()?; Ok(u128::from_be_bytes(bytes)) } + + fn get_call_request_and_params( + from: reth_primitives::H160, + chain_id: u64, + transaction_request: &TransactionRequest, + ) -> (CallRequest, U128, U128) { + // TODO: we need an oracle to fetch the gas price of the current chain + // https://github.com/Sovereign-Labs/sovereign-sdk/issues/883 + let gas_price = transaction_request.gas_price.unwrap_or_default(); + let max_fee_per_gas = transaction_request.max_fee_per_gas.unwrap_or_default(); + + // TODO: Generate call request better according to the transaction type + // https://github.com/Sovereign-Labs/sovereign-sdk/issues/946 + let call_request = CallRequest { + from: Some(from), + to: transaction_request.to, + gas: transaction_request.gas, + gas_price: { + if transaction_request.max_priority_fee_per_gas.is_some() { + // eip 1559 + None + } else { + // legacy + Some(U256::from(gas_price)) + } + }, + max_fee_per_gas: Some(U256::from(max_fee_per_gas)), + value: transaction_request.value, + input: transaction_request.data.clone().into(), + nonce: transaction_request.nonce, + chain_id: Some(chain_id.into()), + access_list: transaction_request.access_list.clone(), + max_priority_fee_per_gas: { + if transaction_request.max_priority_fee_per_gas.is_some() { + // eip 1559 + Some(U256::from( + transaction_request + .max_priority_fee_per_gas + .unwrap_or(max_fee_per_gas), + )) + } else { + // legacy + None + } + }, + transaction_type: None, + blob_versioned_hashes: vec![], + max_fee_per_blob_gas: None, + }; + + (call_request, gas_price, max_fee_per_gas) + } } diff --git a/module-system/module-implementations/sov-evm/src/lib.rs b/module-system/module-implementations/sov-evm/src/lib.rs index b256b2ae52..a53d197aa7 100644 --- a/module-system/module-implementations/sov-evm/src/lib.rs +++ b/module-system/module-implementations/sov-evm/src/lib.rs @@ -47,6 +47,10 @@ mod experimental { Block, BlockEnv, Receipt, SealedBlock, TransactionSignedAndRecovered, }; + // Gas per transaction not creating a contract. + pub(crate) const MIN_TRANSACTION_GAS: u64 = 21_000u64; + pub(crate) const MIN_CREATE_GAS: u64 = 53_000u64; + /// Evm account. #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct AccountData { diff --git a/module-system/module-implementations/sov-evm/src/query.rs b/module-system/module-implementations/sov-evm/src/query.rs index e2314465ae..183cb87f5e 100644 --- a/module-system/module-implementations/sov-evm/src/query.rs +++ b/module-system/module-implementations/sov-evm/src/query.rs @@ -1,18 +1,24 @@ +use std::array::TryFromSliceError; + use ethereum_types::U64; use jsonrpsee::core::RpcResult; use reth_primitives::contract::create_address; use reth_primitives::TransactionKind::{Call, Create}; use reth_primitives::{TransactionSignedEcRecovered, U128, U256}; +use revm::primitives::{ + EVMError, ExecutionResult, Halt, InvalidTransaction, TransactTo, KECCAK_EMPTY, +}; use sov_modules_api::macros::rpc_gen; use sov_modules_api::WorkingSet; use tracing::info; use crate::call::get_cfg_env; -use crate::error::rpc::ensure_success; +use crate::error::rpc::{ensure_success, RevertError, RpcInvalidTransactionError}; use crate::evm::db::EvmDb; use crate::evm::primitive_types::{BlockEnv, Receipt, SealedBlock, TransactionSignedAndRecovered}; use crate::evm::{executor, prepare_call_env}; -use crate::Evm; +use crate::experimental::{MIN_CREATE_GAS, MIN_TRANSACTION_GAS}; +use crate::{EthApiError, Evm}; #[rpc_gen(client, server, namespace = "eth")] impl Evm { @@ -260,15 +266,177 @@ impl Evm { } /// Handler for: `eth_estimateGas` - // TODO https://github.com/Sovereign-Labs/sovereign-sdk/issues/502 + // https://github.com/paradigmxyz/reth/blob/main/crates/rpc/rpc/src/eth/api/call.rs#L172 #[rpc_method(name = "estimateGas")] pub fn eth_estimate_gas( &self, - _data: reth_rpc_types::CallRequest, - _block_number: Option, - _working_set: &mut WorkingSet, - ) -> RpcResult { - unimplemented!("eth_estimateGas not implemented") + request: reth_rpc_types::CallRequest, + block_number: Option, + working_set: &mut WorkingSet, + ) -> RpcResult { + info!("evm module: eth_estimateGas"); + let mut block_env = match block_number { + Some(ref block_number) if block_number == "pending" => { + self.block_env.get(working_set).unwrap_or_default().clone() + } + _ => { + let block = self.get_sealed_block_by_number(block_number, working_set); + BlockEnv::from(&block) + } + }; + + let tx_env = prepare_call_env(&block_env, request.clone()).unwrap(); + + let cfg = self.cfg.get(working_set).unwrap_or_default(); + let cfg_env = get_cfg_env(&block_env, cfg, Some(get_cfg_env_template())); + + let request_gas = request.gas; + let request_gas_price = request.gas_price; + let env_gas_limit = block_env.gas_limit; + + // get the highest possible gas limit, either the request's set value or the currently + // configured gas limit + let mut highest_gas_limit = request.gas.unwrap_or(U256::from(env_gas_limit)); + + let account = self.accounts.get(&tx_env.caller, working_set).unwrap(); + + // if the request is a simple transfer we can optimize + if tx_env.data.is_empty() { + if let TransactTo::Call(to) = tx_env.transact_to { + let to_account = self.accounts.get(&to, working_set).unwrap(); + if KECCAK_EMPTY == to_account.info.code_hash { + // simple transfer, check if caller has sufficient funds + let available_funds = account.info.balance; + + if tx_env.value > available_funds { + return Err(RpcInvalidTransactionError::InsufficientFundsForTransfer.into()); + } + return Ok(U64::from(MIN_TRANSACTION_GAS)); + } + } + } + + // check funds of the sender + if tx_env.gas_price > U256::ZERO { + // allowance is (balance - tx.value) / tx.gas_price + let allowance = (account.info.balance - tx_env.value) / tx_env.gas_price; + + if highest_gas_limit > allowance { + // cap the highest gas limit by max gas caller can afford with given gas price + highest_gas_limit = allowance; + } + } + + // if the provided gas limit is less than computed cap, use that + let gas_limit = std::cmp::min(U256::from(tx_env.gas_limit), highest_gas_limit); + block_env.gas_limit = convert_u256_to_u64(gas_limit).unwrap(); + + let evm_db = self.get_db(working_set); + + // execute the call without writing to db + let result = executor::inspect(evm_db, &block_env, tx_env.clone(), cfg_env.clone()); + + // Exceptional case: init used too much gas, we need to increase the gas limit and try + // again + if let Err(EVMError::Transaction(InvalidTransaction::CallerGasLimitMoreThanBlock)) = result + { + // if price or limit was included in the request then we can execute the request + // again with the block's gas limit to check if revert is gas related or not + if request_gas.is_some() || request_gas_price.is_some() { + let evm_db = self.get_db(working_set); + return Err(map_out_of_gas_err(block_env, tx_env, cfg_env, evm_db).into()); + } + } + + let result = result.unwrap(); + + match result.result { + ExecutionResult::Success { .. } => { + // succeeded + } + ExecutionResult::Halt { reason, gas_used } => { + return Err(RpcInvalidTransactionError::halt(reason, gas_used).into()) + } + ExecutionResult::Revert { output, .. } => { + // if price or limit was included in the request then we can execute the request + // again with the block's gas limit to check if revert is gas related or not + return if request_gas.is_some() || request_gas_price.is_some() { + let evm_db = self.get_db(working_set); + Err(map_out_of_gas_err(block_env, tx_env, cfg_env, evm_db).into()) + } else { + // the transaction did revert + Err(RpcInvalidTransactionError::Revert(RevertError::new(output)).into()) + }; + } + } + + // at this point we know the call succeeded but want to find the _best_ (lowest) gas the + // transaction succeeds with. we find this by doing a binary search over the + // possible range NOTE: this is the gas the transaction used, which is less than the + // transaction requires to succeed + let gas_used = result.result.gas_used(); + // the lowest value is capped by the gas it takes for a transfer + let mut lowest_gas_limit = if tx_env.transact_to.is_create() { + MIN_CREATE_GAS + } else { + MIN_TRANSACTION_GAS + }; + let mut highest_gas_limit: u64 = highest_gas_limit.try_into().unwrap_or(u64::MAX); + // pick a point that's close to the estimated gas + let mut mid_gas_limit = std::cmp::min( + gas_used * 3, + ((highest_gas_limit as u128 + lowest_gas_limit as u128) / 2) as u64, + ); + // binary search + while (highest_gas_limit - lowest_gas_limit) > 1 { + let mut tx_env = tx_env.clone(); + tx_env.gas_limit = mid_gas_limit; + + let evm_db = self.get_db(working_set); + let result = executor::inspect(evm_db, &block_env, tx_env.clone(), cfg_env.clone()); + + // Exceptional case: init used too much gas, we need to increase the gas limit and try + // again + if let Err(EVMError::Transaction(InvalidTransaction::CallerGasLimitMoreThanBlock)) = + result + { + // increase the lowest gas limit + lowest_gas_limit = mid_gas_limit; + + // new midpoint + mid_gas_limit = ((highest_gas_limit as u128 + lowest_gas_limit as u128) / 2) as u64; + continue; + } + + let result = result.unwrap(); + match result.result { + ExecutionResult::Success { .. } => { + // cap the highest gas limit with succeeding gas limit + highest_gas_limit = mid_gas_limit; + } + ExecutionResult::Revert { .. } => { + // increase the lowest gas limit + lowest_gas_limit = mid_gas_limit; + } + ExecutionResult::Halt { reason, .. } => { + match reason { + Halt::OutOfGas(_) => { + // increase the lowest gas limit + lowest_gas_limit = mid_gas_limit; + } + err => { + // these should be unreachable because we know the transaction succeeds, + // but we consider these cases an error + return Err(RpcInvalidTransactionError::EvmHalt(err).into()); + } + } + } + } + // new midpoint + mid_gas_limit = ((highest_gas_limit as u128 + lowest_gas_limit as u128) / 2) as u64; + } + + Ok(U64::from(highest_gas_limit)) } /// Handler for: `eth_gasPrice` @@ -388,3 +556,32 @@ pub(crate) fn build_rpc_receipt( .collect(), } } + +fn map_out_of_gas_err( + block_env: BlockEnv, + mut tx_env: revm::primitives::TxEnv, + cfg_env: revm::primitives::CfgEnv, + db: EvmDb<'_, C>, +) -> EthApiError { + let req_gas_limit = tx_env.gas_limit; + tx_env.gas_limit = block_env.gas_limit; + let res = executor::inspect(db, &block_env, tx_env, cfg_env).unwrap(); + match res.result { + ExecutionResult::Success { .. } => { + // transaction succeeded by manually increasing the gas limit to + // highest, which means the caller lacks funds to pay for the tx + RpcInvalidTransactionError::BasicOutOfGas(U256::from(req_gas_limit)).into() + } + ExecutionResult::Revert { output, .. } => { + // reverted again after bumping the limit + RpcInvalidTransactionError::Revert(RevertError::new(output)).into() + } + ExecutionResult::Halt { reason, .. } => RpcInvalidTransactionError::EvmHalt(reason).into(), + } +} + +fn convert_u256_to_u64(u256: reth_primitives::U256) -> Result { + let bytes: [u8; 32] = u256.to_be_bytes(); + let bytes: [u8; 8] = bytes[24..].try_into()?; + Ok(u64::from_be_bytes(bytes)) +} From 3562bdf0bb5347a0506c0876511c54b44ad1a2fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Orkun=20Mahir=20K=C4=B1l=C4=B1=C3=A7?= Date: Fri, 29 Sep 2023 13:51:57 +0300 Subject: [PATCH 04/12] EVM: Implement account related RPC (#958) * implement account endpoints * test account endpoints * remove unnecessary conversions --- examples/demo-rollup/tests/evm/mod.rs | 68 ++++++++++++++++++- .../sov-evm/src/query.rs | 67 ++++++++++++++++++ 2 files changed, 132 insertions(+), 3 deletions(-) diff --git a/examples/demo-rollup/tests/evm/mod.rs b/examples/demo-rollup/tests/evm/mod.rs index cebe97a4d5..b9942c14b0 100644 --- a/examples/demo-rollup/tests/evm/mod.rs +++ b/examples/demo-rollup/tests/evm/mod.rs @@ -87,6 +87,23 @@ impl TestClient { Ok(receipt_req) } + async fn deploy_contract_call(&self) -> Result> { + let req = Eip1559TransactionRequest::new() + .from(self.from_addr) + .chain_id(self.chain_id) + .nonce(0u64) + .max_priority_fee_per_gas(10u64) + .max_fee_per_gas(MAX_FEE_PER_GAS) + .gas(900000u64) + .data(self.contract.byte_code()); + + let typed_transaction = TypedTransaction::Eip1559(req); + + let receipt_req = self.eth_call(typed_transaction, None).await?; + + Ok(receipt_req) + } + async fn set_value_unsigned( &self, contract_address: H160, @@ -236,6 +253,31 @@ impl TestClient { chain_id.as_u64() } + async fn eth_get_balance(&self, address: Address) -> ethereum_types::U256 { + self.http_client + .request("eth_getBalance", rpc_params![address, "latest"]) + .await + .unwrap() + } + + async fn eth_get_storage_at( + &self, + address: Address, + index: ethereum_types::U256, + ) -> ethereum_types::U256 { + self.http_client + .request("eth_getStorageAt", rpc_params![address, index, "latest"]) + .await + .unwrap() + } + + async fn eth_get_code(&self, address: Address) -> Bytes { + self.http_client + .request("eth_getCode", rpc_params![address, "latest"]) + .await + .unwrap() + } + async fn eth_get_transaction_count(&self, address: Address) -> u64 { let count: ethereum_types::U64 = self .http_client @@ -289,17 +331,30 @@ impl TestClient { let nonce = self.eth_get_transaction_count(self.from_addr).await; assert_eq!(0, nonce); - let contract_address = { + // Balance should be > 0 in genesis + let balance = self.eth_get_balance(self.from_addr).await; + assert!(balance > ethereum_types::U256::zero()); + + let (contract_address, runtime_code) = { + let runtime_code = self.deploy_contract_call().await?; + let deploy_contract_req = self.deploy_contract().await?; self.send_publish_batch_request().await; - deploy_contract_req + let contract_address = deploy_contract_req .await? .unwrap() .contract_address - .unwrap() + .unwrap(); + + (contract_address, runtime_code) }; + // Assert contract deployed correctly + let code = self.eth_get_code(contract_address).await; + // code has natural following 0x00 bytes, so we need to trim it + assert_eq!(code.to_vec()[..runtime_code.len()], runtime_code.to_vec()); + // Nonce should be 1 after the deploy let nonce = self.eth_get_transaction_count(self.from_addr).await; assert_eq!(1, nonce); @@ -320,6 +375,13 @@ impl TestClient { let get_arg = self.query_contract(contract_address).await?; assert_eq!(set_arg, get_arg.as_u32()); + // Assert storage slot is set + let storage_slot = 0x0; + let storage_value = self + .eth_get_storage_at(contract_address, storage_slot.into()) + .await; + assert_eq!(storage_value, ethereum_types::U256::from(set_arg)); + // Check that the second block has published // None should return the latest block // It should have a single transaction, setting the value diff --git a/module-system/module-implementations/sov-evm/src/query.rs b/module-system/module-implementations/sov-evm/src/query.rs index 183cb87f5e..a6ddd13501 100644 --- a/module-system/module-implementations/sov-evm/src/query.rs +++ b/module-system/module-implementations/sov-evm/src/query.rs @@ -100,6 +100,51 @@ impl Evm { Ok(Some(block.into())) } + /// Handler for: `eth_getBalance` + #[rpc_method(name = "getBalance")] + pub fn get_balance( + &self, + address: reth_primitives::Address, + _block_number: Option, + working_set: &mut WorkingSet, + ) -> RpcResult { + info!("evm module: eth_getBalance"); + + // TODO: Implement block_number once we have archival state #951 + // https://github.com/Sovereign-Labs/sovereign-sdk/issues/951 + + let balance = self + .accounts + .get(&address, working_set) + .map(|account| account.info.balance) + .unwrap_or_default(); + + Ok(balance) + } + + /// Handler for: `eth_getStorageAt` + #[rpc_method(name = "getStorageAt")] + pub fn get_storage_at( + &self, + address: reth_primitives::Address, + index: reth_primitives::U256, + _block_number: Option, + working_set: &mut WorkingSet, + ) -> RpcResult { + info!("evm module: eth_getStorageAt"); + + // TODO: Implement block_number once we have archival state #951 + // https://github.com/Sovereign-Labs/sovereign-sdk/issues/951 + + let storage_slot = self + .accounts + .get(&address, working_set) + .and_then(|account| account.storage.get(&index, working_set)) + .unwrap_or_default(); + + Ok(storage_slot) + } + /// Handler for: `eth_getTransactionCount` #[rpc_method(name = "getTransactionCount")] pub fn get_transaction_count( @@ -122,6 +167,28 @@ impl Evm { Ok(nonce.into()) } + /// Handler for: `eth_getCode` + #[rpc_method(name = "getCode")] + pub fn get_code( + &self, + address: reth_primitives::Address, + _block_number: Option, + working_set: &mut WorkingSet, + ) -> RpcResult { + info!("evm module: eth_getCode"); + + // TODO: Implement block_number once we have archival state #951 + // https://github.com/Sovereign-Labs/sovereign-sdk/issues/951 + + let code = self + .accounts + .get(&address, working_set) + .and_then(|account| self.code.get(&account.info.code_hash, working_set)) + .unwrap_or_default(); + + Ok(code) + } + /// Handler for: `eth_feeHistory` // TODO https://github.com/Sovereign-Labs/sovereign-sdk/issues/502 #[rpc_method(name = "feeHistory")] From ca038a7b87dd0d7a9768687ee0d009e60c0ab718 Mon Sep 17 00:00:00 2001 From: Victor Lopes Date: Fri, 29 Sep 2023 13:17:48 +0200 Subject: [PATCH 05/12] feature: gas meter (#795) * feat: add gas meter to working set This commit introduces `GasMeter`, encapsulated by `WorkingSet`. It will allow the user to consume scalar gas from the working set, and define arbitrary price parsed from a constants.json manifest file at compilation. At each compilation, the `ModuleInfo` derive macro will parse such file, and set the gas price configuration. * fix lint fmt * fix ci test expected error string * update default context to 2 dimensions --- constants.json | 11 +- .../methods/guest-celestia/Cargo.lock | 5 - .../demo-prover/methods/guest-mock/Cargo.lock | 5 - module-system/README.md | 105 ++++++++++ .../sov-bank/src/lib.rs | 30 ++- module-system/sov-modules-api/README.md | 6 + .../sov-modules-api/src/default_context.rs | 6 +- module-system/sov-modules-api/src/gas.rs | 92 ++++++++ module-system/sov-modules-api/src/lib.rs | 17 ++ .../sov-modules-api/src/state/scratchpad.rs | 19 ++ module-system/sov-modules-macros/src/lib.rs | 2 +- .../sov-modules-macros/src/manifest.rs | 196 ++++++++++-------- .../sov-modules-macros/src/module_info.rs | 52 ++++- .../field_missing_attribute.stderr | 2 +- 14 files changed, 442 insertions(+), 106 deletions(-) create mode 100644 module-system/sov-modules-api/src/gas.rs diff --git a/constants.json b/constants.json index 96620c6e7b..9beb309f4a 100644 --- a/constants.json +++ b/constants.json @@ -1,3 +1,12 @@ { - "comment": "Sovereign SDK constants" + "comment": "Sovereign SDK constants", + "gas": { + "Bank": { + "create_token": [4, 4], + "transfer": [5, 5], + "burn": [2, 2], + "mint": [2, 2], + "freeze": [1, 1] + } + } } diff --git a/examples/demo-prover/methods/guest-celestia/Cargo.lock b/examples/demo-prover/methods/guest-celestia/Cargo.lock index da5807d78e..b4574b135a 100644 --- a/examples/demo-prover/methods/guest-celestia/Cargo.lock +++ b/examples/demo-prover/methods/guest-celestia/Cargo.lock @@ -1854,8 +1854,3 @@ dependencies = [ "quote", "syn 2.0.37", ] - -[[patch.unused]] -name = "cc" -version = "1.0.79" -source = "git+https://github.com/rust-lang/cc-rs?rev=e5bbdfa#e5bbdfa1fa468c028cb38fee6c35a3cf2e5a2736" diff --git a/examples/demo-prover/methods/guest-mock/Cargo.lock b/examples/demo-prover/methods/guest-mock/Cargo.lock index db5de049cc..d4302d1f06 100644 --- a/examples/demo-prover/methods/guest-mock/Cargo.lock +++ b/examples/demo-prover/methods/guest-mock/Cargo.lock @@ -1272,8 +1272,3 @@ name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[patch.unused]] -name = "cc" -version = "1.0.79" -source = "git+https://github.com/rust-lang/cc-rs?rev=e5bbdfa#e5bbdfa1fa468c028cb38fee6c35a3cf2e5a2736" diff --git a/module-system/README.md b/module-system/README.md index 4d8bf8d432..9812c4b1c8 100644 --- a/module-system/README.md +++ b/module-system/README.md @@ -44,6 +44,111 @@ This has several consequences. First, it means that modules are always cheap to always yields the same result as calling `MyModule::new()`. Finally, it means that every method of the module which reads or modifies state needs to take a `WorkingSet` as an argument. +### Gas configuration + +The module might contain a field for the gas configuration. If annotated with `#[gas]` under a struct that derives `ModuleInfo`, it will attempt to read a `constants.json` file from the root of the project, and inject it into the `Default::default()` implementation of the module. + +Here is an example `constants.json` file: + +```json +{ + "gas": { + "create_token": 4, + "transfer": 5, + "burn": 2, + "mint": 2, + "freeze": 1 + } +} +``` + +The `ModuleInfo` macro will look for a `gas` field inside the JSON, that must be an object, and will look for the name of the module inside of the `gas` object. If present, it will parse that object as gas configuration; otherwise, it will parse the `gas` object directly. On the example above, it will attempt to parse a structure that looks like this: + +```rust +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct BankGasConfig { + pub create_token: GU, + pub transfer: GU, + pub burn: GU, + pub mint: GU, + pub freeze: GU, +} +``` + +The `GasUnit` generic type will be defined by the runtime `Context`. For `DefaultContext`, we use `TupleGasUnit<2>` - that is, a gas unit with a two dimensions. The same setup is defined for `ZkDefaultContext`. Here is an example of a `constants.json` file, specific to the `Bank` module: + +```json +{ + "gas": { + "comment": "this field will be ignored, as there is a matching module field", + "Bank": { + "create_token": [4, 19], + "transfer": [5, 25], + "burn": [2, 7], + "mint": [2, 6], + "freeze": [1, 4] + } + } +} +``` + +As you can see above, the fields can be either array, numeric, or boolean. If boolean, it will be converted to either `0` or `1`. If array, each element is expected to be either a numeric or boolean. The example above will create a gas unit of two dimensions. If the `Context` requires less dimensions than available, it will pick the first ones of relevance, and ignore the rest. That is: with a `Context` of one dimension, , the effective config will be expanded to: + +```rust +BankGasConfig { + create_token: [4], + transfer: [5], + burn: [2], + mint: [2], + freeze: [1], +} +``` + +In order to charge gas from the working set, the function `charge_gas` can be used. + +```rust +fn call( + &self, + msg: Self::CallMessage, + context: &Self::Context, + working_set: &mut WorkingSet, +) -> Result { + match msg { + call::CallMessage::CreateToken { + salt, + token_name, + initial_balance, + minter_address, + authorized_minters, + } => { + self.charge_gas(working_set, &self.gas.create_token)?; + // Implementation elided... +} +``` + +On the example above, we charge the configured unit from the working set. Concretely, we will charge a unit of `[4, 19]` from both `DefaultContext` and `ZkDefaultContext`. The working set will be the responsible to perform a scalar conversion from the dimensions to a single funds value. It will perform an inner product of the loaded price, with the provided unit. + +Let's assume we have a working set with the loaded price `[3, 2]`. The charged gas of the operation above will be `[3] · [4] = 3 × 4 = 12` for a single dimension context, and `[3, 2] · [4, 19] = 3 × 4 + 2 × 19 = 50` for both `DefaultContext` and `ZkDefaultContext`. This approach is intended to unlock [Dynamic Pricing](https://arxiv.org/abs/2208.07919). + +The aforementioned `Bank` struct, with the gas configuration, will look like this: + +```rust +#[derive(ModuleInfo)] +pub struct Bank { + /// The address of the bank module. + #[address] + pub(crate) address: C::Address, + + /// The gas configuration of the sov-bank module. + #[gas] + pub(crate) gas: BankGasConfig, + + /// A mapping of addresses to tokens in the bank. + #[state] + pub(crate) tokens: sov_state::StateMap>, +} +``` + ### Public Functions: The Module-to-Module Interface The first interface that modules expose is defined by the public methods from the rollup's `impl`. These methods are diff --git a/module-system/module-implementations/sov-bank/src/lib.rs b/module-system/module-implementations/sov-bank/src/lib.rs index e8d3425e40..7f3cece428 100644 --- a/module-system/module-implementations/sov-bank/src/lib.rs +++ b/module-system/module-implementations/sov-bank/src/lib.rs @@ -13,7 +13,7 @@ pub mod utils; /// Specifies the call methods using in that module. pub use call::CallMessage; use serde::{Deserialize, Serialize}; -use sov_modules_api::{CallResponse, Error, ModuleInfo, WorkingSet}; +use sov_modules_api::{CallResponse, Error, GasUnit, ModuleInfo, WorkingSet}; use token::Token; /// Specifies an interface to interact with tokens. pub use token::{Amount, Coins}; @@ -41,6 +41,25 @@ pub struct BankConfig { pub tokens: Vec>, } +/// Gas configuration for the bank module +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct BankGasConfig { + /// Gas price multiplier for the create token operation + pub create_token: GU, + + /// Gas price multiplier for the transfer operation + pub transfer: GU, + + /// Gas price multiplier for the burn operation + pub burn: GU, + + /// Gas price multiplier for the mint operation + pub mint: GU, + + /// Gas price multiplier for the freeze operation + pub freeze: GU, +} + /// The sov-bank module manages user balances. It provides functionality for: /// - Token creation. /// - Token transfers. @@ -52,6 +71,10 @@ pub struct Bank { #[address] pub(crate) address: C::Address, + /// The gas configuration of the sov-bank module. + #[gas] + pub(crate) gas: BankGasConfig, + /// A mapping of addresses to tokens in the sov-bank. #[state] pub(crate) tokens: sov_modules_api::StateMap>, @@ -82,6 +105,7 @@ impl sov_modules_api::Module for Bank { minter_address, authorized_minters, } => { + self.charge_gas(working_set, &self.gas.create_token)?; self.create_token( token_name, salt, @@ -95,10 +119,12 @@ impl sov_modules_api::Module for Bank { } call::CallMessage::Transfer { to, coins } => { + self.charge_gas(working_set, &self.gas.create_token)?; Ok(self.transfer(to, coins, context, working_set)?) } call::CallMessage::Burn { coins } => { + self.charge_gas(working_set, &self.gas.burn)?; Ok(self.burn_from_eoa(coins, context, working_set)?) } @@ -106,11 +132,13 @@ impl sov_modules_api::Module for Bank { coins, minter_address, } => { + self.charge_gas(working_set, &self.gas.mint)?; self.mint_from_eoa(&coins, &minter_address, context, working_set)?; Ok(CallResponse::default()) } call::CallMessage::Freeze { token_address } => { + self.charge_gas(working_set, &self.gas.freeze)?; Ok(self.freeze(token_address, context, working_set)?) } } diff --git a/module-system/sov-modules-api/README.md b/module-system/sov-modules-api/README.md index 5753e7eb5b..9db055ed77 100644 --- a/module-system/sov-modules-api/README.md +++ b/module-system/sov-modules-api/README.md @@ -14,6 +14,9 @@ crate: - Interaction with user messages: The module must define the `call` method and the `CallMessage` type, which handle user messages. These messages typically result in changes to the module's state. + - Gas configuration: The module may use a `GasConfig` type, annotated by `#[gas]`, that will be loaded from the + constants manifest configuration. + 1. The `ModuleInfo` trait: Provides additional information related to a module. This trait is automatically derived. 1. The `Spec` trait: It defines all the types that modules are generic over. This separation allows the module logic to @@ -29,3 +32,6 @@ crate: 1. The `DispatchCall` trait: Defines how messages are forwarded to the appropriate module and how the call message is executed. The implementation of this trait can be generated automatically using a macro. + +1. The `GasUnit` trait: Defines how the scalar gas value is deducted from the working set. This is implemented for + `[u64; N]`, and can be customized by the user. diff --git a/module-system/sov-modules-api/src/default_context.rs b/module-system/sov-modules-api/src/default_context.rs index a07f6b7355..8fccbcc412 100644 --- a/module-system/sov-modules-api/src/default_context.rs +++ b/module-system/sov-modules-api/src/default_context.rs @@ -9,7 +9,7 @@ use sov_state::{ArrayWitness, DefaultStorageSpec, ZkStorage}; #[cfg(feature = "native")] use crate::default_signature::private_key::DefaultPrivateKey; use crate::default_signature::{DefaultPublicKey, DefaultSignature}; -use crate::{Address, Context, PublicKey, Spec}; +use crate::{Address, Context, PublicKey, Spec, TupleGasUnit}; #[cfg(feature = "native")] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] @@ -31,6 +31,8 @@ impl Spec for DefaultContext { #[cfg(feature = "native")] impl Context for DefaultContext { + type GasUnit = TupleGasUnit<2>; + fn sender(&self) -> &Self::Address { &self.sender } @@ -58,6 +60,8 @@ impl Spec for ZkDefaultContext { } impl Context for ZkDefaultContext { + type GasUnit = TupleGasUnit<2>; + fn sender(&self) -> &Self::Address { &self.sender } diff --git a/module-system/sov-modules-api/src/gas.rs b/module-system/sov-modules-api/src/gas.rs new file mode 100644 index 0000000000..071af99feb --- /dev/null +++ b/module-system/sov-modules-api/src/gas.rs @@ -0,0 +1,92 @@ +use core::fmt; + +use anyhow::Result; + +/// A gas unit that provides scalar conversion from complex, multi-dimensional types. +pub trait GasUnit: fmt::Debug + Clone { + /// A zeroed instance of the unit. + const ZEROED: Self; + + /// Creates a unit from a multi-dimensional unit with arbitrary dimension. + fn from_arbitrary_dimensions(dimensions: &[u64]) -> Self; + + /// Converts the unit into a scalar value, given a price. + fn value(&self, price: &Self) -> u64; +} + +/// A multi-dimensional gas unit. +pub type TupleGasUnit = [u64; N]; + +impl GasUnit for TupleGasUnit { + const ZEROED: Self = [0; N]; + + fn from_arbitrary_dimensions(dimensions: &[u64]) -> Self { + // as demonstrated on the link below, the compiler can easily optimize the conversion as if + // it is a transparent type. + // + // https://rust.godbolt.org/z/rPhaxnPEY + let mut unit = Self::ZEROED; + unit.iter_mut() + .zip(dimensions.iter().copied()) + .for_each(|(a, b)| *a = b); + unit + } + + fn value(&self, price: &Self) -> u64 { + self.iter() + .zip(price.iter().copied()) + .map(|(a, b)| a.saturating_mul(b)) + .fold(0, |a, b| a.saturating_add(b)) + } +} + +/// A gas meter. +pub struct GasMeter +where + GU: GasUnit, +{ + remaining_funds: u64, + gas_price: GU, +} + +impl Default for GasMeter +where + GU: GasUnit, +{ + fn default() -> Self { + Self { + remaining_funds: 0, + gas_price: GU::ZEROED, + } + } +} + +impl GasMeter +where + GU: GasUnit, +{ + /// Creates a new instance of the gas meter with the provided price. + pub fn new(remaining_funds: u64, gas_price: GU) -> Self { + Self { + remaining_funds, + gas_price, + } + } + + /// Returns the remaining gas funds. + pub const fn remaining_funds(&self) -> u64 { + self.remaining_funds + } + + /// Deducts the provided gas unit from the remaining funds, computing the scalar value of the + /// funds from the price of the instance. + pub fn charge_gas(&mut self, gas: &GU) -> Result<()> { + let gas = gas.value(&self.gas_price); + self.remaining_funds = self + .remaining_funds + .checked_sub(gas) + .ok_or_else(|| anyhow::anyhow!("Not enough gas"))?; + + Ok(()) + } +} diff --git a/module-system/sov-modules-api/src/lib.rs b/module-system/sov-modules-api/src/lib.rs index 053fdbd476..38ec31cfd6 100644 --- a/module-system/sov-modules-api/src/lib.rs +++ b/module-system/sov-modules-api/src/lib.rs @@ -9,6 +9,7 @@ pub mod default_signature; mod dispatch; mod encode; mod error; +mod gas; pub mod hooks; mod pub_key_hex; @@ -46,6 +47,7 @@ use digest::Digest; pub use dispatch::CliWallet; pub use dispatch::{DispatchCall, EncodeCall, Genesis}; pub use error::Error; +pub use gas::{GasUnit, TupleGasUnit}; pub use prefix::Prefix; pub use response::CallResponse; #[cfg(feature = "native")] @@ -256,6 +258,9 @@ pub trait Spec { /// instance of the state transition function. By making modules generic over a `Context`, developers /// can easily update their cryptography to conform to the needs of different zk-proof systems. pub trait Context: Spec + Clone + Debug + PartialEq + 'static { + /// Gas unit for the gas price computation. + type GasUnit: GasUnit; + /// Sender of the transaction. fn sender(&self) -> &Self::Address; @@ -311,6 +316,17 @@ pub trait Module { ) -> Result { unreachable!() } + + /// Attempts to charge the provided amount of gas from the working set. + /// + /// The scalar gas value will be computed from the price defined on the working set. + fn charge_gas( + &self, + working_set: &mut WorkingSet, + gas: &::GasUnit, + ) -> anyhow::Result<()> { + working_set.charge_gas(gas) + } } /// A [`Module`] that has a well-defined and known [JSON @@ -327,6 +343,7 @@ pub trait ModuleCallJsonSchema: Module { /// Every module has to implement this trait. pub trait ModuleInfo { + /// Execution context. type Context: Context; /// Returns address of the module. diff --git a/module-system/sov-modules-api/src/state/scratchpad.rs b/module-system/sov-modules-api/src/state/scratchpad.rs index 11c32f281e..62070c9d0a 100644 --- a/module-system/sov-modules-api/src/state/scratchpad.rs +++ b/module-system/sov-modules-api/src/state/scratchpad.rs @@ -7,6 +7,7 @@ use sov_state::codec::{EncodeKeyLike, StateCodec, StateValueCodec}; use sov_state::storage::{Storage, StorageKey, StorageValue}; use sov_state::{OrderedReadsAndWrites, Prefix, StorageInternalCache}; +use crate::gas::GasMeter; use crate::{Context, Spec}; /// A working set accumulates reads and writes on top of the underlying DB, @@ -145,6 +146,7 @@ impl StateCheckpoint { delta: RevertableWriter::new(self.delta), accessory_delta: RevertableWriter::new(self.accessory_delta), events: Default::default(), + gas_meter: GasMeter::default(), } } @@ -181,6 +183,7 @@ pub struct WorkingSet { delta: RevertableWriter>, accessory_delta: RevertableWriter>, events: Vec, + gas_meter: GasMeter, } impl WorkingSet { @@ -249,6 +252,22 @@ impl WorkingSet { pub fn backing(&self) -> &::Storage { &self.delta.inner.inner } + + /// Returns the remaining gas funds. + pub const fn gas_remaining_funds(&self) -> u64 { + self.gas_meter.remaining_funds() + } + + /// Overrides the current gas settings with the provided values. + pub fn set_gas(&mut self, funds: u64, gas_price: C::GasUnit) { + self.gas_meter = GasMeter::new(funds, gas_price); + } + + /// Attempts to charge the provided gas unit from the gas meter, using the internal price to + /// compute the scalar value. + pub fn charge_gas(&mut self, gas: &C::GasUnit) -> anyhow::Result<()> { + self.gas_meter.charge_gas(gas) + } } impl StateReaderAndWriter for WorkingSet { diff --git a/module-system/sov-modules-macros/src/lib.rs b/module-system/sov-modules-macros/src/lib.rs index 4db6d96afd..40a942af5a 100644 --- a/module-system/sov-modules-macros/src/lib.rs +++ b/module-system/sov-modules-macros/src/lib.rs @@ -34,7 +34,7 @@ use proc_macro::TokenStream; use rpc::ExposeRpcMacro; use syn::{parse_macro_input, DeriveInput}; -#[proc_macro_derive(ModuleInfo, attributes(state, module, address))] +#[proc_macro_derive(ModuleInfo, attributes(state, module, address, gas))] pub fn module_info(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input); diff --git a/module-system/sov-modules-macros/src/manifest.rs b/module-system/sov-modules-macros/src/manifest.rs index 601010fce7..5f778620fa 100644 --- a/module-system/sov-modules-macros/src/manifest.rs +++ b/module-system/sov-modules-macros/src/manifest.rs @@ -1,19 +1,18 @@ -// TODO remove once consumed -#![allow(dead_code)] - use std::path::{Path, PathBuf}; use std::{env, fmt, fs, ops}; use proc_macro2::{Ident, TokenStream}; use serde_json::Value; +use syn::{PathArguments, Type, TypePath}; #[derive(Debug, Clone)] -pub struct Manifest { +pub struct Manifest<'a> { + parent: &'a Ident, path: PathBuf, value: Value, } -impl ops::Deref for Manifest { +impl<'a> ops::Deref for Manifest<'a> { type Target = Value; fn deref(&self) -> &Self::Target { @@ -21,20 +20,24 @@ impl ops::Deref for Manifest { } } -impl Manifest { +impl<'a> Manifest<'a> { /// Parse a manifest file from a string. /// /// The provided path will be used to feedback error to the user, if any. /// /// The `parent` is used to report the errors to the correct span location. - pub fn read_str(manifest: S, path: PathBuf, parent: &Ident) -> Result + pub fn read_str(manifest: S, path: PathBuf, parent: &'a Ident) -> Result where S: AsRef, { let value = serde_json::from_str(manifest.as_ref()) .map_err(|e| Self::err(&path, parent, format!("failed to parse manifest: {e}")))?; - Ok(Self { path, value }) + Ok(Self { + parent, + path, + value, + }) } /// Reads a `constants.json` manifest file, recursing from the target directory that builds the @@ -43,7 +46,7 @@ impl Manifest { /// If the environment variable `CONSTANTS_MANIFEST` is set, it will use that instead. /// /// The `parent` is used to report the errors to the correct span location. - pub fn read_constants(parent: &Ident) -> Result { + pub fn read_constants(parent: &'a Ident) -> Result { let manifest = "constants.json"; let initial_path = match env::var("CONSTANTS_MANIFEST") { Ok(p) => PathBuf::from(&p).canonicalize().map_err(|e| { @@ -123,19 +126,18 @@ impl Manifest { /// /// The `gas` field resolution will first attempt to query `gas.parent`, and then fallback to /// `gas`. They must be objects with arrays of integers as fields. - pub fn parse_gas_config( - &self, - parent: &Ident, - ) -> Result<(Ident, TokenStream, TokenStream), syn::Error> { - let root = self + pub fn parse_gas_config(&self, ty: &Type, field: &Ident) -> Result { + let map = self .value .as_object() - .ok_or_else(|| Self::err(&self.path, parent, "manifest is not an object"))? + .ok_or_else(|| Self::err(&self.path, field, "manifest is not an object"))?; + + let root = map .get("gas") .ok_or_else(|| { Self::err( &self.path, - parent, + field, "manifest does not contain a `gas` attribute", ) })? @@ -143,87 +145,95 @@ impl Manifest { .ok_or_else(|| { Self::err( &self.path, - parent, - format!("`gas` attribute of `{}` is not an object", parent), + field, + format!("`gas` attribute of `{}` is not an object", field), ) })?; - let root = match root.get(&parent.to_string()) { + let root = match root.get(&self.parent.to_string()) { Some(Value::Object(m)) => m, Some(_) => { return Err(Self::err( &self.path, - parent, - format!( - "matching constants entry `{}` is not an object", - &parent.to_string() - ), + field, + format!("matching constants entry `{}` is not an object", field), )) } None => root, }; let mut field_values = vec![]; - let mut fields = vec![]; for (k, v) in root { let k: Ident = syn::parse_str(k).map_err(|e| { Self::err( &self.path, - parent, + field, format!("failed to parse key attribyte `{}`: {}", k, e), ) })?; - let v = v - .as_array() - .ok_or_else(|| { - Self::err( - &self.path, - parent, - format!("`{}` attribute is not an array", k), - ) - })? - .iter() - .map(|v| { - v.as_u64().ok_or_else(|| { + let v = match v { + Value::Array(a) => a + .iter() + .map(|v| match v { + Value::Bool(b) => Ok(*b as u64), + Value::Number(n) => n.as_u64().ok_or_else(|| { + Self::err( + &self.path, + field, + format!( + "the value of the field `{k}` must be an array of valid `u64`" + ), + ) + }), + _ => Err(Self::err( + &self.path, + field, + format!( + "the value of the field `{k}` must be an array of numbers, or booleans" + ), + )), + }) + .collect::>()?, + Value::Number(n) => n + .as_u64() + .ok_or_else(|| { Self::err( &self.path, - parent, - format!("`{}` attribute is not an array of integers", k), + field, + format!("the value of the field `{k}` must be a `u64`"), ) }) - }) - .collect::, _>>()?; + .map(|n| vec![n])?, + Value::Bool(b) => vec![*b as u64], - let n = v.len(); - fields.push(quote::quote!(pub #k: [u64; #n])); - field_values.push(quote::quote!(#k: [#(#v,)*])); + _ => { + return Err(Self::err( + &self.path, + field, + format!( + "the value of the field `{k}` must be an array, number, or boolean" + ), + )) + } + }; + + field_values.push(quote::quote!(#k: <<::Context as ::sov_modules_api::Context>::GasUnit as ::sov_modules_api::GasUnit>::from_arbitrary_dimensions(&[#(#v,)*]))); } - let ty = format!("{parent}GasConfig"); - let ty = syn::parse_str(&ty).map_err(|e| { - Self::err( - &self.path, - parent, - format!("failed to parse type name `{}`: {}", ty, e), - ) - })?; - - let def = quote::quote! { - #[allow(missing_docs)] - #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] - pub struct #ty { - #(#fields,)* + // remove generics, if any + let mut ty = ty.clone(); + if let Type::Path(TypePath { path, .. }) = &mut ty { + if let Some(p) = path.segments.last_mut() { + p.arguments = PathArguments::None; } - }; + } - let decl = quote::quote! { - #ty { + Ok(quote::quote! { + let #field = #ty { #(#field_values,)* - } - }; - - Ok((ty, def, decl)) + }; + }) } fn err(path: P, ident: &syn::Ident, msg: T) -> syn::Error @@ -274,42 +284,54 @@ fn parse_gas_config_works() { }"#; let parent = Ident::new("Foo", proc_macro2::Span::call_site()); - let (ty, def, decl) = Manifest::read_str(input, PathBuf::from("foo.json"), &parent) + let gas_config: Type = syn::parse_str("FooGasConfig").unwrap(); + let field: Ident = syn::parse_str("foo_gas_config").unwrap(); + + let decl = Manifest::read_str(input, PathBuf::from("foo.json"), &parent) .unwrap() - .parse_gas_config(&parent) + .parse_gas_config(&gas_config, &field) .unwrap(); #[rustfmt::skip] assert_eq!( - ty.to_string(), + decl.to_string(), quote::quote!( - FooGasConfig + let foo_gas_config = FooGasConfig { + complex_math_operation: <<::Context as ::sov_modules_api::Context>::GasUnit as ::sov_modules_api::GasUnit>::from_arbitrary_dimensions(&[1u64, 2u64, 3u64, ]), + some_other_operation: <<::Context as ::sov_modules_api::Context>::GasUnit as ::sov_modules_api::GasUnit>::from_arbitrary_dimensions(&[4u64, 5u64, 6u64, ]), + }; ) .to_string() ); +} - #[rustfmt::skip] - assert_eq!( - def.to_string(), - quote::quote!( - #[allow(missing_docs)] - #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] - pub struct FooGasConfig { - pub complex_math_operation: [u64; 3usize], - pub some_other_operation: [u64; 3usize], - } - ) - .to_string() - ); +#[test] +fn parse_gas_config_single_dimension_works() { + let input = r#"{ + "comment": "Sovereign SDK constants", + "gas": { + "complex_math_operation": 1, + "some_other_operation": 2 + } + }"#; + + let parent = Ident::new("Foo", proc_macro2::Span::call_site()); + let gas_config: Type = syn::parse_str("FooGasConfig").unwrap(); + let field: Ident = syn::parse_str("foo_gas_config").unwrap(); + + let decl = Manifest::read_str(input, PathBuf::from("foo.json"), &parent) + .unwrap() + .parse_gas_config(&gas_config, &field) + .unwrap(); #[rustfmt::skip] assert_eq!( decl.to_string(), quote::quote!( - FooGasConfig { - complex_math_operation: [1u64, 2u64, 3u64, ], - some_other_operation: [4u64, 5u64, 6u64, ], - } + let foo_gas_config = FooGasConfig { + complex_math_operation: <<::Context as ::sov_modules_api::Context>::GasUnit as ::sov_modules_api::GasUnit>::from_arbitrary_dimensions(&[1u64, ]), + some_other_operation: <<::Context as ::sov_modules_api::Context>::GasUnit as ::sov_modules_api::GasUnit>::from_arbitrary_dimensions(&[2u64, ]), + }; ) .to_string() ); diff --git a/module-system/sov-modules-macros/src/module_info.rs b/module-system/sov-modules-macros/src/module_info.rs index 8339478240..c199fbc006 100644 --- a/module-system/sov-modules-macros/src/module_info.rs +++ b/module-system/sov-modules-macros/src/module_info.rs @@ -5,6 +5,7 @@ use syn::{ use self::parsing::{ModuleField, ModuleFieldAttribute, StructDef}; use crate::common::get_generics_type_param; +use crate::manifest::Manifest; pub(crate) fn derive_module_info( input: DeriveInput, @@ -84,6 +85,10 @@ fn impl_module_info(struct_def: &StructDef) -> Result { + impl_self_init.push(make_init_gas_config(ident, field)?); + impl_self_body.push(&field.ident); + } }; } @@ -93,7 +98,6 @@ fn impl_module_info(struct_def: &StructDef) -> Result Self { #(#impl_self_init)* @@ -213,6 +217,16 @@ fn make_init_module(field: &ModuleField) -> Result Result { + let field_ident = &field.ident; + let ty = &field.ty; + + Manifest::read_constants(parent)?.parse_gas_config(ty, field_ident) +} + fn make_module_prefix_fn(struct_ident: &Ident) -> proc_macro2::TokenStream { let body = make_module_prefix_fn_body(struct_ident); quote::quote! { @@ -270,6 +284,7 @@ pub mod parsing { let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl(); let fields = parse_module_fields(&input.data)?; check_exactly_one_address(&fields)?; + check_zero_or_one_gas(&fields)?; Ok(StructDef { ident, @@ -301,6 +316,7 @@ pub mod parsing { Module, State { codec_builder: Option }, Address, + Gas, } impl ModuleFieldAttribute { @@ -327,6 +343,16 @@ pub mod parsing { } } "state" => parse_state_attr(attr), + "gas" => { + if attr.tokens.is_empty() { + Ok(Self::Gas) + } else { + Err(syn::Error::new_spanned( + attr, + "The `#[gas]` attribute does not accept any arguments.", + )) + } + } _ => unreachable!("attribute names were validated already; this is a bug"), } } @@ -407,6 +433,24 @@ pub mod parsing { } } + fn check_zero_or_one_gas(fields: &[ModuleField]) -> syn::Result<()> { + let gas_fields = fields + .iter() + .filter(|field| matches!(field.attr, ModuleFieldAttribute::Gas)) + .collect::>(); + + match gas_fields.len() { + 0 | 1 => Ok(()), + _ => Err(syn::Error::new_spanned( + gas_fields[1].ident.clone(), + format!( + "The `gas` attribute is defined more than once, revisit field: {}", + gas_fields[1].ident, + ), + )), + } + } + fn data_to_struct(data: &syn::Data) -> syn::Result<&DataStruct> { match data { syn::Data::Struct(data_struct) => Ok(data_struct), @@ -433,9 +477,9 @@ pub mod parsing { let mut attr = None; for a in field.attrs.iter() { match a.path.segments[0].ident.to_string().as_str() { - "state" | "module" | "address" => { + "state" | "module" | "address" | "gas" => { if attr.is_some() { - return Err(syn::Error::new_spanned(ident, "Only one attribute out of `#[module]`, `#[state]` and `#[address]` is allowed per field.")); + return Err(syn::Error::new_spanned(ident, "Only one attribute out of `#[module]`, `#[state]`, `#[address]`, and #[gas] is allowed per field.")); } else { attr = Some(a); } @@ -449,7 +493,7 @@ pub mod parsing { } else { Err(syn::Error::new_spanned( ident, - format!("The field `{}` is missing an attribute: add `#[module]`, `#[state]` or `#[address]`.", ident), + format!("The field `{}` is missing an attribute: add `#[module]`, `#[state]`, `#[address]`, or #[gas].", ident), )) } } diff --git a/module-system/sov-modules-macros/tests/module_info/field_missing_attribute.stderr b/module-system/sov-modules-macros/tests/module_info/field_missing_attribute.stderr index 4cdbb45b5e..6045d71481 100644 --- a/module-system/sov-modules-macros/tests/module_info/field_missing_attribute.stderr +++ b/module-system/sov-modules-macros/tests/module_info/field_missing_attribute.stderr @@ -1,4 +1,4 @@ -error: The field `test_state1` is missing an attribute: add `#[module]`, `#[state]` or `#[address]`. +error: The field `test_state1` is missing an attribute: add `#[module]`, `#[state]`, `#[address]`, or #[gas]. --> tests/module_info/field_missing_attribute.rs:8:5 | 8 | test_state1: StateMap, From 7229226354c1cfe221fded444d9c808edfab10d7 Mon Sep 17 00:00:00 2001 From: Blazej Kolad Date: Fri, 29 Sep 2023 15:08:03 +0200 Subject: [PATCH 06/12] Read `accounts` genesis from a file. (#959) * PrivateKeyHex in accounts * Add test_config_serialization * Add accounts.json * fix ci * fix CI * cleanup * Arbitrary PublicKeyHex * PublicKeyHex impl --- examples/demo-stf/src/genesis_config.rs | 14 +++++++--- examples/test-data/genesis/accounts.json | 3 ++ fuzz/fuzz_targets/accounts_call.rs | 2 +- .../sov-accounts/src/genesis.rs | 7 +++-- .../sov-accounts/src/lib.rs | 21 ++++++-------- .../sov-accounts/src/tests.rs | 28 +++++++++++++++++-- .../sov-modules-api/src/default_signature.rs | 2 +- module-system/sov-modules-api/src/lib.rs | 3 +- .../sov-modules-api/src/pub_key_hex.rs | 20 +++++++++---- 9 files changed, 68 insertions(+), 32 deletions(-) create mode 100644 examples/test-data/genesis/accounts.json diff --git a/examples/demo-stf/src/genesis_config.rs b/examples/demo-stf/src/genesis_config.rs index f0fe2b1106..2443216026 100644 --- a/examples/demo-stf/src/genesis_config.rs +++ b/examples/demo-stf/src/genesis_config.rs @@ -1,6 +1,7 @@ use anyhow::Context as AnyhowContext; #[cfg(feature = "experimental")] use reth_primitives::Bytes; +use sov_accounts::AccountConfig; use sov_chain_state::ChainStateConfig; use sov_cli::wallet_state::PrivateKeyAndAddress; #[cfg(feature = "experimental")] @@ -85,12 +86,17 @@ fn create_genesis_config( let value_setter_genesis_path = "../test-data/genesis/value_setter.json"; let value_setter_data = std::fs::read_to_string(value_setter_genesis_path) .with_context(|| format!("Failed to read genesis from {}", value_setter_genesis_path))?; - - let nft_config = sov_nft_module::NonFungibleTokenConfig {}; - let value_setter_config: ValueSetterConfig = serde_json::from_str(&value_setter_data) .with_context(|| format!("Failed to parse genesis from {}", value_setter_genesis_path))?; + let accounts_genesis_path = "../test-data/genesis/accounts.json"; + let accounts_data = std::fs::read_to_string(accounts_genesis_path) + .with_context(|| format!("Failed to read genesis from {}", accounts_genesis_path))?; + + let accounts_config: AccountConfig = serde_json::from_str(&accounts_data) + .with_context(|| format!("Failed to parse genesis from {}", accounts_genesis_path))?; + + let nft_config = sov_nft_module::NonFungibleTokenConfig {}; // This will be read from a file: #872 let chain_state_config = ChainStateConfig { // TODO: Put actual value @@ -104,7 +110,7 @@ fn create_genesis_config( (), chain_state_config, value_setter_config, - sov_accounts::AccountConfig { pub_keys: vec![] }, + accounts_config, #[cfg(feature = "experimental")] get_evm_config(evm_genesis_addresses), nft_config, diff --git a/examples/test-data/genesis/accounts.json b/examples/test-data/genesis/accounts.json new file mode 100644 index 0000000000..3a134d04d3 --- /dev/null +++ b/examples/test-data/genesis/accounts.json @@ -0,0 +1,3 @@ +{ + "pub_keys":[] +} \ No newline at end of file diff --git a/fuzz/fuzz_targets/accounts_call.rs b/fuzz/fuzz_targets/accounts_call.rs index df64afe768..ceec6c61e7 100644 --- a/fuzz/fuzz_targets/accounts_call.rs +++ b/fuzz/fuzz_targets/accounts_call.rs @@ -42,7 +42,7 @@ fuzz_target!(|input: (u16, [u8; 32], Vec)| -> Corpus { let storage = ::Storage::with_path(tmpdir.path()).unwrap(); let working_set = &mut WorkingSet::new(storage); - let config: AccountConfig = keys.iter().map(|k| k.pub_key()).collect(); + let config: AccountConfig = keys.iter().map(|k| k.pub_key()).collect(); let accounts: Accounts = Accounts::default(); accounts.genesis(&config, working_set).unwrap(); diff --git a/module-system/module-implementations/sov-accounts/src/genesis.rs b/module-system/module-implementations/sov-accounts/src/genesis.rs index 9d9bb8f5d6..5b80b98578 100644 --- a/module-system/module-implementations/sov-accounts/src/genesis.rs +++ b/module-system/module-implementations/sov-accounts/src/genesis.rs @@ -9,12 +9,13 @@ impl Accounts { config: &::Config, working_set: &mut WorkingSet, ) -> Result<()> { - for pub_key in config.pub_keys.iter() { - if self.accounts.get(pub_key, working_set).is_some() { + for pub_key_hex in config.pub_keys.iter() { + let pub_key = pub_key_hex.try_into()?; + if self.accounts.get(&pub_key, working_set).is_some() { bail!("Account already exists") } - self.create_default_account(pub_key.clone(), working_set)?; + self.create_default_account(pub_key, working_set)?; } Ok(()) diff --git a/module-system/module-implementations/sov-accounts/src/lib.rs b/module-system/module-implementations/sov-accounts/src/lib.rs index 922fce9f4d..7fa0a1e667 100644 --- a/module-system/module-implementations/sov-accounts/src/lib.rs +++ b/module-system/module-implementations/sov-accounts/src/lib.rs @@ -12,18 +12,17 @@ pub use query::*; mod tests; pub use call::{CallMessage, UPDATE_ACCOUNT_MSG}; -use sov_modules_api::{Context, Error, ModuleInfo, WorkingSet}; +use sov_modules_api::{Context, Error, ModuleInfo, PublicKeyHex, WorkingSet}; /// Initial configuration for sov-accounts module. #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -#[serde(bound = "C::PublicKey: serde::Serialize + serde::de::DeserializeOwned")] -pub struct AccountConfig { +pub struct AccountConfig { /// Public keys to initialize the rollup. - pub pub_keys: Vec, + pub pub_keys: Vec, } -impl FromIterator for AccountConfig { - fn from_iter>(iter: T) -> Self { +impl FromIterator for AccountConfig { + fn from_iter>(iter: T) -> Self { Self { pub_keys: iter.into_iter().collect(), } @@ -59,7 +58,7 @@ pub struct Accounts { impl sov_modules_api::Module for Accounts { type Context = C; - type Config = AccountConfig; + type Config = AccountConfig; type CallMessage = call::CallMessage; @@ -95,11 +94,7 @@ where } #[cfg(feature = "arbitrary")] -impl<'a, C> arbitrary::Arbitrary<'a> for AccountConfig -where - C: Context, - C::PublicKey: arbitrary::Arbitrary<'a>, -{ +impl<'a> arbitrary::Arbitrary<'a> for AccountConfig { fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { // TODO we might want a dedicated struct that will generate the private key counterpart so // payloads can be signed and verified @@ -123,7 +118,7 @@ where ) -> arbitrary::Result { use sov_modules_api::Module; - let config: AccountConfig = u.arbitrary()?; + let config: AccountConfig = u.arbitrary()?; let accounts = Accounts::default(); accounts diff --git a/module-system/module-implementations/sov-accounts/src/tests.rs b/module-system/module-implementations/sov-accounts/src/tests.rs index c57be2c659..9b6804cba7 100644 --- a/module-system/module-implementations/sov-accounts/src/tests.rs +++ b/module-system/module-implementations/sov-accounts/src/tests.rs @@ -1,5 +1,8 @@ +use std::str::FromStr; + use sov_modules_api::default_context::DefaultContext; use sov_modules_api::default_signature::private_key::DefaultPrivateKey; +use sov_modules_api::default_signature::DefaultPublicKey; use sov_modules_api::{AddressBech32, Context, Module, PrivateKey, PublicKey, Spec, WorkingSet}; use sov_state::ProverStorage; @@ -7,15 +10,34 @@ use crate::query::{self, Response}; use crate::{call, AccountConfig, Accounts}; type C = DefaultContext; +#[test] +fn test_config_serialization() { + let pub_key = &DefaultPublicKey::from_str( + "1cd4e2d9d5943e6f3d12589d31feee6bb6c11e7b8cd996a393623e207da72cbf", + ) + .unwrap(); + + let config = AccountConfig { + pub_keys: vec![pub_key.clone().try_into().unwrap()], + }; + + let data = r#" + { + "pub_keys":["1cd4e2d9d5943e6f3d12589d31feee6bb6c11e7b8cd996a393623e207da72cbf"] + }"#; + + let parsed_config: AccountConfig = serde_json::from_str(data).unwrap(); + assert_eq!(parsed_config, config); +} + #[test] fn test_config_account() { let priv_key = DefaultPrivateKey::generate(); - let init_pub_key = priv_key.pub_key(); let init_pub_key_addr = init_pub_key.to_address::<::Address>(); - let account_config = AccountConfig:: { - pub_keys: vec![init_pub_key.clone()], + let account_config = AccountConfig { + pub_keys: vec![init_pub_key.clone().try_into().unwrap()], }; let accounts = &mut Accounts::::default(); diff --git a/module-system/sov-modules-api/src/default_signature.rs b/module-system/sov-modules-api/src/default_signature.rs index 48c886f499..75b11777d1 100644 --- a/module-system/sov-modules-api/src/default_signature.rs +++ b/module-system/sov-modules-api/src/default_signature.rs @@ -254,7 +254,7 @@ impl FromStr for DefaultPublicKey { type Err = anyhow::Error; fn from_str(s: &str) -> Result { - let pk_hex = crate::pub_key_hex::PublicKeyHex::try_from(s)?; + let pk_hex = &crate::pub_key_hex::PublicKeyHex::try_from(s)?; pk_hex.try_into() } } diff --git a/module-system/sov-modules-api/src/lib.rs b/module-system/sov-modules-api/src/lib.rs index 38ec31cfd6..22d430ab9e 100644 --- a/module-system/sov-modules-api/src/lib.rs +++ b/module-system/sov-modules-api/src/lib.rs @@ -28,8 +28,8 @@ pub mod transaction; #[cfg(feature = "native")] pub mod utils; +pub use pub_key_hex::PublicKeyHex; pub use state::*; - #[cfg(feature = "macros")] extern crate sov_modules_macros; @@ -166,6 +166,7 @@ pub trait PublicKey: + Sync + Serialize + for<'a> Deserialize<'a> + + for<'a> TryFrom<&'a PublicKeyHex, Error = anyhow::Error> { fn to_address(&self) -> A; } diff --git a/module-system/sov-modules-api/src/pub_key_hex.rs b/module-system/sov-modules-api/src/pub_key_hex.rs index 3c5d390c57..07667ca3c6 100644 --- a/module-system/sov-modules-api/src/pub_key_hex.rs +++ b/module-system/sov-modules-api/src/pub_key_hex.rs @@ -63,11 +63,11 @@ impl From for PublicKeyHex { } } -impl TryFrom for DefaultPublicKey { +impl TryFrom<&PublicKeyHex> for DefaultPublicKey { type Error = anyhow::Error; - fn try_from(pub_key: PublicKeyHex) -> Result { - let bytes = hex::decode(pub_key.hex)?; + fn try_from(pub_key: &PublicKeyHex) -> Result { + let bytes = hex::decode(&pub_key.hex)?; let bytes: [u8; PUBLIC_KEY_LENGTH] = bytes .try_into() @@ -90,7 +90,7 @@ mod tests { fn test_pub_key_hex() { let pub_key = DefaultPrivateKey::generate().pub_key(); let pub_key_hex = PublicKeyHex::try_from(pub_key.clone()).unwrap(); - let converted_pub_key = DefaultPublicKey::try_from(pub_key_hex).unwrap(); + let converted_pub_key = DefaultPublicKey::try_from(&pub_key_hex).unwrap(); assert_eq!(pub_key, converted_pub_key); } @@ -100,8 +100,8 @@ mod tests { let pub_key_hex_lower: PublicKeyHex = key.try_into().unwrap(); let pub_key_hex_upper: PublicKeyHex = key.to_uppercase().try_into().unwrap(); - let pub_key_lower = DefaultPublicKey::try_from(pub_key_hex_lower).unwrap(); - let pub_key_upper = DefaultPublicKey::try_from(pub_key_hex_upper).unwrap(); + let pub_key_lower = DefaultPublicKey::try_from(&pub_key_hex_lower).unwrap(); + let pub_key_upper = DefaultPublicKey::try_from(&pub_key_hex_upper).unwrap(); assert_eq!(pub_key_lower, pub_key_upper) } @@ -122,3 +122,11 @@ mod tests { assert_eq!(err.to_string(), "Bad hex conversion: odd input length") } } + +#[cfg(feature = "arbitrary")] +impl<'a> arbitrary::Arbitrary<'a> for PublicKeyHex { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let hex: String = hex::encode(String::arbitrary(u)?); + Ok(PublicKeyHex::try_from(hex).unwrap()) + } +} From 47650f19efe2f600133a2ff23837ea29f86aac05 Mon Sep 17 00:00:00 2001 From: dubbelosix Date: Fri, 29 Sep 2023 22:23:07 +0530 Subject: [PATCH 07/12] offchain processing macro and example for sov-nft-module (#939) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * offchain processing * BlockScout integration fixes (#917) * Fixes * retesteth config Signed-off-by: Filippo Costa --------- Signed-off-by: Filippo Costa * modules_api: simplify public key definition (#918) * simplify publi key definition * private key * `EVM`: Add missing docs (#914) * rename pending_block to block_env * make evm internals private * refactor query.rs * AccountData doc * query.rs docs * fix exports * add missing docs * cargo fmt * Add readme * Update README.md * Update README.md * better docs * Cleanup prover docs/logs (#919) * Cleanup prever docs/logs * fmt * Bump markdown from 1.0.0-alpha.13 to 1.0.0-alpha.14 (#924) Bumps [markdown](https://github.com/wooorm/markdown-rs) from 1.0.0-alpha.13 to 1.0.0-alpha.14. - [Release notes](https://github.com/wooorm/markdown-rs/releases) - [Commits](https://github.com/wooorm/markdown-rs/compare/1.0.0-alpha.13...1.0.0-alpha.14) --- updated-dependencies: - dependency-name: markdown dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump rayon from 1.7.0 to 1.8.0 (#923) Bumps [rayon](https://github.com/rayon-rs/rayon) from 1.7.0 to 1.8.0. - [Changelog](https://github.com/rayon-rs/rayon/blob/master/RELEASES.md) - [Commits](https://github.com/rayon-rs/rayon/compare/rayon-core-v1.7.0...rayon-core-v1.8.0) --- updated-dependencies: - dependency-name: rayon dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump prost-build from 0.11.9 to 0.12.1 (#930) Bumps [prost-build](https://github.com/tokio-rs/prost) from 0.11.9 to 0.12.1. - [Release notes](https://github.com/tokio-rs/prost/releases) - [Commits](https://github.com/tokio-rs/prost/compare/v0.11.9...v0.12.1) --- updated-dependencies: - dependency-name: prost-build dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump parking_lot from 0.11.2 to 0.12.1 (#927) Bumps [parking_lot](https://github.com/Amanieu/parking_lot) from 0.11.2 to 0.12.1. - [Changelog](https://github.com/Amanieu/parking_lot/blob/master/CHANGELOG.md) - [Commits](https://github.com/Amanieu/parking_lot/compare/0.11.2...0.12.1) --- updated-dependencies: - dependency-name: parking_lot dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feat: use json as manifest constants instead of toml (#922) * feat: use json as manifest constants instead of toml Currently, we parse the manifest file as TOML. However, we will use JSON for the genesis format. For improved consistency, it is desirable to have the constants manifest file with the same format. * fix CI lints * use parent as argument for error reporting * refactor gas config parse to return declaration * fix ci lints * Bump tungstenite from 0.20.0 to 0.20.1 (#931) Bumps [tungstenite](https://github.com/snapview/tungstenite-rs) from 0.20.0 to 0.20.1. - [Changelog](https://github.com/snapview/tungstenite-rs/blob/master/CHANGELOG.md) - [Commits](https://github.com/snapview/tungstenite-rs/compare/v0.20.0...v0.20.1) --- updated-dependencies: - dependency-name: tungstenite dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * EVM: Implement eth_call (#921) * implement eth_call * implement tests and error handling for eth_call * move errors inside evm crate * cleanup result * fix call env * comment test differences * rebase * Revert "rebase" This reverts commit 44e41b23dac2a68839cad896ef6c46c20ca410cc. * fix unused imports * fix review notes * use mix_hash as prevrandao * improve test s * cargo lock * some fixes * move common code into lib * added issue to the nft script * add nft cli binary to nft-utils * Revert "add nft cli binary to nft-utils" This reverts commit d443009db9b438112d388e4e78e19d2dfc19fe3f. * move nft-utils back to sovereign/nft-utils and move sql to sql.rs * remove un-necessary deps in nft-utils * Run tests with prover enabled. (#932) * Remove demo-rollup dep from demo-prover * add methods to demo-rollup * fix cargo hack * fix cargo hack * Cargo.toml * install toolchain * install toolchain * modify workflow * update cargo.toml * rust.yaml * Add guest-mock * use real prover in bank tests * native * fix lint * update yaml * fic coverage * ELF in CI * update bench get_guest_options() * update build.rs * remove local from demo-rollup * fix lint * remove ide_setup * Remove riscv32im-risc0-zkvm-elf * fix: require `native` when applicable for all targets (#937) * fix: require `native` when applicable for all targets Prior to this commit, some checks of all-targets without the feature `native` would break. This commit introduces a fix for every workspace member to be consistent with the feature set. * change feature requirement to self-dev-dep * update deps * fix dupl celestia dependencies * Remove default trait bound from Module (#941) * Remove demo-rollup-local job from CI (#943) * Delta's go first, remove `get` from StateCheckpoint (#953) * Make genesis config serializable (#956) * Make genesis config serializable * Evm config serde * Fix chain state integ test * fix vec-setter * Introduce: `PublicKeyHex` in `sov-modules-api` (#954) * Add PublicKeyHex * PubKeyHex impl * Update DefaultPublicKey::from_str * Add tests * Remove println * Add doc * estimate-gas signed (#947) * remove include and make it a module * fix packages.yml * EVM: Implement account related RPC (#958) * implement account endpoints * test account endpoints * remove unnecessary conversions * feature: gas meter (#795) * feat: add gas meter to working set This commit introduces `GasMeter`, encapsulated by `WorkingSet`. It will allow the user to consume scalar gas from the working set, and define arbitrary price parsed from a constants.json manifest file at compilation. At each compilation, the `ModuleInfo` derive macro will parse such file, and set the gas price configuration. * fix lint fmt * fix ci test expected error string * update default context to 2 dimensions * Read `accounts` genesis from a file. (#959) * PrivateKeyHex in accounts * Add test_config_serialization * Add accounts.json * fix ci * fix CI * cleanup * Arbitrary PublicKeyHex * PublicKeyHex impl * fix a wrong merge conflict --------- Signed-off-by: Filippo Costa Signed-off-by: dependabot[bot] Co-authored-by: Filippo Neysofu Costa Co-authored-by: Blazej Kolad Co-authored-by: Preston Evans <32944016+preston-evans98@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Victor Lopes Co-authored-by: Orkun Mahir Kılıç Co-authored-by: Nikolai Golub --- Cargo.lock | 112 ++++++++++ Cargo.toml | 2 + .../methods/guest-celestia/Cargo.lock | 5 + .../demo-prover/methods/guest-mock/Cargo.lock | 5 + examples/demo-rollup/Cargo.toml | 2 +- examples/demo-stf/Cargo.toml | 1 + full-node/sov-sequencer/src/utils.rs | 33 +++ full-node/sov-stf-runner/src/batch_builder.rs | 8 +- .../sov-nft-module/Cargo.toml | 6 + .../sov-nft-module/offchain_readme.md | 60 ++++++ .../sov-nft-module/src/call.rs | 12 +- .../sov-nft-module/src/init_db.sql | 50 +++++ .../sov-nft-module/src/lib.rs | 3 + .../sov-nft-module/src/offchain.rs | 123 +++++++++++ .../sov-nft-module/src/sql.rs | 30 +++ .../sov-modules-api/src/transaction.rs | 4 +- module-system/sov-modules-macros/src/lib.rs | 39 +++- .../sov-modules-macros/src/offchain.rs | 31 +++ packages_to_publish.yml | 1 + utils/nft-utils/Cargo.toml | 25 +++ utils/nft-utils/src/lib.rs | 201 ++++++++++++++++++ utils/nft-utils/src/main.rs | 105 +++++++++ 22 files changed, 850 insertions(+), 8 deletions(-) create mode 100644 module-system/module-implementations/sov-nft-module/offchain_readme.md create mode 100644 module-system/module-implementations/sov-nft-module/src/init_db.sql create mode 100644 module-system/module-implementations/sov-nft-module/src/offchain.rs create mode 100644 module-system/module-implementations/sov-nft-module/src/sql.rs create mode 100644 module-system/sov-modules-macros/src/offchain.rs create mode 100644 utils/nft-utils/Cargo.toml create mode 100644 utils/nft-utils/src/lib.rs create mode 100644 utils/nft-utils/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 2ba866a41a..8d7dd54a4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2626,6 +2626,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0870c84016d4b481be5c9f323c24f65e31e901ae618f0e80f4308fb00de1d2d" +[[package]] +name = "finl_unicode" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" + [[package]] name = "fixed-hash" version = "0.8.0" @@ -4273,6 +4279,19 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" +[[package]] +name = "nft-utils" +version = "0.2.0" +dependencies = [ + "borsh", + "demo-stf", + "sov-modules-api", + "sov-nft-module", + "sov-rollup-interface", + "sov-sequencer", + "tokio", +] + [[package]] name = "nibble_vec" version = "0.1.0" @@ -4982,6 +5001,49 @@ dependencies = [ "serde", ] +[[package]] +name = "postgres" +version = "0.19.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7915b33ed60abc46040cbcaa25ffa1c7ec240668e0477c4f3070786f5916d451" +dependencies = [ + "bytes", + "fallible-iterator", + "futures-util", + "log", + "tokio", + "tokio-postgres", +] + +[[package]] +name = "postgres-protocol" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49b6c5ef183cd3ab4ba005f1ca64c21e8bd97ce4699cfea9e8d9a2c4958ca520" +dependencies = [ + "base64 0.21.4", + "byteorder", + "bytes", + "fallible-iterator", + "hmac", + "md-5", + "memchr", + "rand 0.8.5", + "sha2 0.10.7", + "stringprep", +] + +[[package]] +name = "postgres-types" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d2234cdee9408b523530a9b6d2d6b373d1db34f6a8e51dc03ded1828d7fb67c" +dependencies = [ + "bytes", + "fallible-iterator", + "postgres-protocol", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -7441,6 +7503,7 @@ dependencies = [ "anyhow", "borsh", "jsonrpsee", + "postgres", "schemars", "serde", "serde_json", @@ -7451,6 +7514,8 @@ dependencies = [ "sov-rollup-interface", "sov-state", "tempfile", + "tokio", + "tracing", ] [[package]] @@ -7728,6 +7793,17 @@ dependencies = [ "precomputed-hash", ] +[[package]] +name = "stringprep" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" +dependencies = [ + "finl_unicode", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "strsim" version = "0.9.3" @@ -8138,6 +8214,32 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-postgres" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d340244b32d920260ae7448cb72b6e238bddc3d4f7603394e7dd46ed8e48f5b8" +dependencies = [ + "async-trait", + "byteorder", + "bytes", + "fallible-iterator", + "futures-channel", + "futures-util", + "log", + "parking_lot 0.12.1", + "percent-encoding", + "phf", + "pin-project-lite", + "postgres-protocol", + "postgres-types", + "rand 0.8.5", + "socket2 0.5.4", + "tokio", + "tokio-util", + "whoami", +] + [[package]] name = "tokio-rustls" version = "0.24.1" @@ -8830,6 +8932,16 @@ dependencies = [ "rustix", ] +[[package]] +name = "whoami" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wildmatch" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 729d602898..aa556b2267 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,9 @@ members = [ "utils/zk-cycle-macros", "utils/zk-cycle-utils", + "utils/nft-utils", "utils/bashtestmd", + "utils/nft-utils", "module-system/sov-cli", "module-system/sov-modules-stf-template", diff --git a/examples/demo-prover/methods/guest-celestia/Cargo.lock b/examples/demo-prover/methods/guest-celestia/Cargo.lock index b4574b135a..da5807d78e 100644 --- a/examples/demo-prover/methods/guest-celestia/Cargo.lock +++ b/examples/demo-prover/methods/guest-celestia/Cargo.lock @@ -1854,3 +1854,8 @@ dependencies = [ "quote", "syn 2.0.37", ] + +[[patch.unused]] +name = "cc" +version = "1.0.79" +source = "git+https://github.com/rust-lang/cc-rs?rev=e5bbdfa#e5bbdfa1fa468c028cb38fee6c35a3cf2e5a2736" diff --git a/examples/demo-prover/methods/guest-mock/Cargo.lock b/examples/demo-prover/methods/guest-mock/Cargo.lock index d4302d1f06..db5de049cc 100644 --- a/examples/demo-prover/methods/guest-mock/Cargo.lock +++ b/examples/demo-prover/methods/guest-mock/Cargo.lock @@ -1272,3 +1272,8 @@ name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[patch.unused]] +name = "cc" +version = "1.0.79" +source = "git+https://github.com/rust-lang/cc-rs?rev=e5bbdfa#e5bbdfa1fa468c028cb38fee6c35a3cf2e5a2736" diff --git a/examples/demo-rollup/Cargo.toml b/examples/demo-rollup/Cargo.toml index b014a7afc6..2814df35ec 100644 --- a/examples/demo-rollup/Cargo.toml +++ b/examples/demo-rollup/Cargo.toml @@ -79,7 +79,7 @@ native = ["anyhow", "jsonrpsee", "serde", "serde_json", "tracing", "tokio", "tra "sov-state/native", "sov-cli", "clap", "sov-celestia-adapter/native", "sov-db", "sov-sequencer", "sov-stf-runner/native", "sov-modules-api/native", "sov-rollup-interface/native"] bench = ["native", "async-trait", "borsh", "hex"] - +offchain = ["demo-stf/offchain"] [[bench]] name = "rollup_bench" diff --git a/examples/demo-stf/Cargo.toml b/examples/demo-stf/Cargo.toml index 17babeb8fc..28a93152ed 100644 --- a/examples/demo-stf/Cargo.toml +++ b/examples/demo-stf/Cargo.toml @@ -51,6 +51,7 @@ rand = "0.8" [features] default = [] +offchain = ["sov-nft-module/offchain"] experimental = ["sov-evm/experimental", "reth-primitives"] native = [ "sov-stf-runner/native", diff --git a/full-node/sov-sequencer/src/utils.rs b/full-node/sov-sequencer/src/utils.rs index a4a828ee9d..56a42311e6 100644 --- a/full-node/sov-sequencer/src/utils.rs +++ b/full-node/sov-sequencer/src/utils.rs @@ -38,6 +38,39 @@ impl SimpleClient { Ok(()) } + /// Sends multiple transactions to the sequencer for immediate publication. + pub async fn send_transactions( + &self, + txs: Vec, + chunk_size: Option, + ) -> Result<(), anyhow::Error> { + let serialized_txs: Vec> = txs + .into_iter() + .map(|tx| tx.try_to_vec()) + .collect::>()?; + + match chunk_size { + Some(batch_size) => { + for chunk in serialized_txs.chunks(batch_size) { + let response: String = self + .http_client + .request("sequencer_publishBatch", chunk.to_vec()) + .await?; + info!("publish batch response for chunk: {:?}", response); + } + } + None => { + let response: String = self + .http_client + .request("sequencer_publishBatch", serialized_txs) + .await?; + info!("publish batch response: {:?}", response); + } + } + + Ok(()) + } + /// Get a reference to the underlying [`HttpClient`] pub fn http(&self) -> &HttpClient { &self.http_client diff --git a/full-node/sov-stf-runner/src/batch_builder.rs b/full-node/sov-stf-runner/src/batch_builder.rs index 47c935c52a..e6b3cd009a 100644 --- a/full-node/sov-stf-runner/src/batch_builder.rs +++ b/full-node/sov-stf-runner/src/batch_builder.rs @@ -125,14 +125,14 @@ where current_batch_size += tx_len; } - if txs.is_empty() { - bail!("No valid transactions are available"); - } - for (tx, err) in dismissed { warn!("Transaction 0x{} was dismissed: {:?}", hex::encode(tx), err); } + if txs.is_empty() { + bail!("No valid transactions are available"); + } + Ok(txs) } } diff --git a/module-system/module-implementations/sov-nft-module/Cargo.toml b/module-system/module-implementations/sov-nft-module/Cargo.toml index a063098d05..d1f4e0d9fa 100644 --- a/module-system/module-implementations/sov-nft-module/Cargo.toml +++ b/module-system/module-implementations/sov-nft-module/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "sov-nft-module" +description = "A Sovereign SDK module for managing non-fungible tokens" version = { workspace = true } edition = { workspace = true } authors = { workspace = true } @@ -21,6 +22,10 @@ sov-modules-api = { path = "../../sov-modules-api" } sov-modules-macros = {path = "../../sov-modules-macros"} sov-state = { path = "../../sov-state" } +postgres = { version = "0.19.7", optional = true } +tokio = { version = "1.32.0", features=["full"], optional = true } +tracing = { workspace = true, optional = true } + [dev-dependencies] sov-rollup-interface = { path = "../../../rollup-interface" } sov-data-generators = { path = "../../utils/sov-data-generators" } @@ -29,5 +34,6 @@ sov-nft-module = { version = "*", features = ["native"], path = "." } [features] default = [] +offchain = ["postgres","tokio","tracing"] native = ["serde_json", "jsonrpsee", "schemars", "sov-state/native", "sov-modules-api/native", ] test = ["native"] diff --git a/module-system/module-implementations/sov-nft-module/offchain_readme.md b/module-system/module-implementations/sov-nft-module/offchain_readme.md new file mode 100644 index 0000000000..3a4345ddf2 --- /dev/null +++ b/module-system/module-implementations/sov-nft-module/offchain_readme.md @@ -0,0 +1,60 @@ +## Offchain testing + +### Introduction +This readme outlines the steps to demonstrate the offchain processing functionality that is part of the `sov-nft-module` + +### Steps +* Install postgres on your system +* Start the postgres terminal +* Create the tables necessary for offchain processing + +```bash +psql postgres -f sovereign/module-system/module-implementations/sov-nft-module/src/init_db.sql +``` +* The above command runs the `init_db.sql` script which creates 3 tables + * `collections` - tracking the NFT collections that have been created, their supply and other info + * `nfts` - tracking the individual NFTs, the `token_uri` pointing to offchain state and other info + * `top_owners` - tracks the number of NFTs of each collection that a user owns + * running the following query can show the top owners for a specific collection + ```sql + SELECT owner, count + FROM top_owners + WHERE collection_address = ( + SELECT collection_address + FROM collections + WHERE collection_name = 'your_collection_name' + ) + ORDER BY count DESC + LIMIT 5; + ``` + * running the following query can show the largest owner for each collection + ```sql + SELECT + collection_name, + owner, + count + FROM ( + SELECT c.collection_name, t.owner, t.count, + RANK() OVER (PARTITION BY t.collection_address ORDER BY t.count DESC) as rank + FROM top_owners t + INNER JOIN collections c ON c.collection_address = t.collection_address + ) sub + WHERE rank = 1; + ``` + +* Run the demo rollup in offchain mode +```bash +rm -rf demo_data; POSTGRES_CONNECTION_STRING="postgresql://username:password@localhost/postgres" cargo run --features offchain -- --da-layer mock +``` +* Explanation of the above command + * `rm -rf demo_data` is to wipe the rollup state. For testing its better to start with clean state + * `POSTGRES_CONNECTION_STRING` is to allow the offchain component of the `sov-nft-module` to connect to postgres instance + * `--features offchain` is necessary to enable offchain processing. Without the feature, the functions are no-ops + * `--da-layer mock` is used to run an in-memory local DA layer +* Run the NFT minting script +```bash +$ cd sovereign/utils/nft-utils +$ cargo run +``` + * The above script creates 3 NFT collections, mints some NFTs to each collection + * The tables can be explored by connecting to postgres and running sample queries from above \ No newline at end of file diff --git a/module-system/module-implementations/sov-nft-module/src/call.rs b/module-system/module-implementations/sov-nft-module/src/call.rs index 095942e783..028d624983 100644 --- a/module-system/module-implementations/sov-nft-module/src/call.rs +++ b/module-system/module-implementations/sov-nft-module/src/call.rs @@ -2,6 +2,7 @@ use anyhow::Result; use sov_modules_api::{CallResponse, Context, WorkingSet}; use crate::address::UserAddress; +use crate::offchain::{update_collection, update_nft}; use crate::{Collection, CollectionAddress, Nft, NftIdentifier, NonFungibleToken, TokenId}; #[cfg_attr( @@ -87,6 +88,7 @@ impl NonFungibleToken { )?; self.collections .set(&collection_address, &collection, working_set); + update_collection(&collection); Ok(CallResponse::default()) } @@ -107,6 +109,7 @@ impl NonFungibleToken { collection.set_collection_uri(collection_uri); self.collections .set(&collection_address, collection.inner(), working_set); + update_collection(collection.inner()); Ok(CallResponse::default()) } @@ -126,6 +129,7 @@ impl NonFungibleToken { collection.freeze(); self.collections .set(&collection_address, collection.inner(), working_set); + update_collection(collection.inner()); Ok(CallResponse::default()) } @@ -165,6 +169,9 @@ impl NonFungibleToken { self.collections .set(&collection_address, collection.inner(), working_set); + update_collection(collection.inner()); + update_nft(&new_nft, None); + Ok(CallResponse::default()) } @@ -178,12 +185,14 @@ impl NonFungibleToken { ) -> Result { let mut owned_nft = Nft::get_owned_nft(nft_id, collection_address, &self.nfts, context, working_set)?; + let original_owner = owned_nft.inner().get_owner().clone(); owned_nft.set_owner(to); self.nfts.set( &NftIdentifier(nft_id, collection_address.clone()), owned_nft.inner(), working_set, ); + update_nft(owned_nft.inner(), Some(original_owner.clone())); Ok(CallResponse::default()) } @@ -211,10 +220,11 @@ impl NonFungibleToken { mutable_nft.update_token_uri(&uri); } self.nfts.set( - &NftIdentifier(token_id, collection_address), + &NftIdentifier(token_id, collection_address.clone()), mutable_nft.inner(), working_set, ); + update_nft(mutable_nft.inner(), None); Ok(CallResponse::default()) } } diff --git a/module-system/module-implementations/sov-nft-module/src/init_db.sql b/module-system/module-implementations/sov-nft-module/src/init_db.sql new file mode 100644 index 0000000000..ed89fddad2 --- /dev/null +++ b/module-system/module-implementations/sov-nft-module/src/init_db.sql @@ -0,0 +1,50 @@ +-- Drop existing tables if they exist +DROP TABLE IF EXISTS top_owners CASCADE; +DROP TABLE IF EXISTS nfts CASCADE; +DROP TABLE IF EXISTS collections CASCADE; + +-- Create collection table +CREATE TABLE collections +( + collection_address TEXT PRIMARY KEY, + collection_name TEXT NOT NULL, + creator_address TEXT NOT NULL, + frozen BOOLEAN NOT NULL, + metadata_url TEXT, + supply BIGINT NOT NULL +); + +-- Create index on creator_address to quickly find collections by a creator +CREATE INDEX idx_creator ON collections (creator_address); + +-- Create nft table +CREATE TABLE nfts +( + collection_address TEXT NOT NULL, + nft_id BIGINT NOT NULL, + metadata_url TEXT, + owner TEXT NOT NULL, + frozen BOOLEAN NOT NULL, + PRIMARY KEY (collection_address, nft_id) +); + +-- Create index on owner to quickly find NFTs owned by a particular address +CREATE INDEX idx_nft_owner ON nfts (owner); + +-- Create index on collection_address to quickly find NFTs belonging to a particular collection +CREATE INDEX idx_nft_collection ON nfts (collection_address); + +-- Create top_owners table +CREATE TABLE top_owners +( + owner TEXT NOT NULL, + collection_address TEXT NOT NULL, + count BIGINT NOT NULL, + PRIMARY KEY (owner, collection_address) +); + +-- Create index on collection_address to quickly find top owners in a particular collection +CREATE INDEX idx_top_owners_collection ON top_owners (collection_address); + +-- Create index on count to quickly find top owners by count (optional) +CREATE INDEX idx_top_owners_count ON top_owners (count); diff --git a/module-system/module-implementations/sov-nft-module/src/lib.rs b/module-system/module-implementations/sov-nft-module/src/lib.rs index 65df31d343..b1d26129bd 100644 --- a/module-system/module-implementations/sov-nft-module/src/lib.rs +++ b/module-system/module-implementations/sov-nft-module/src/lib.rs @@ -16,6 +16,9 @@ mod query; pub use query::*; use serde::{Deserialize, Serialize}; use sov_modules_api::{CallResponse, Context, Error, Module, ModuleInfo, StateMap, WorkingSet}; +mod offchain; +#[cfg(feature = "offchain")] +mod sql; /// Utility functions. pub mod utils; diff --git a/module-system/module-implementations/sov-nft-module/src/offchain.rs b/module-system/module-implementations/sov-nft-module/src/offchain.rs new file mode 100644 index 0000000000..a2d6a192c7 --- /dev/null +++ b/module-system/module-implementations/sov-nft-module/src/offchain.rs @@ -0,0 +1,123 @@ +#[cfg(feature = "offchain")] +use postgres::NoTls; +use sov_modules_macros::offchain; + +#[cfg(feature = "offchain")] +use crate::sql::*; +#[cfg(feature = "offchain")] +use crate::utils::get_collection_address; +#[cfg(feature = "offchain")] +use crate::CollectionAddress; +use crate::{Collection, Nft, OwnerAddress}; + +/// Syncs a collection to the corresponding table "collections" in postgres +#[offchain] +pub fn update_collection(collection: &Collection) { + // Extract the necessary metadata from the collection + let collection_name = collection.get_name(); + let creator_address = collection.get_creator(); + let frozen = collection.is_frozen(); + let metadata_url = collection.get_collection_uri(); + let supply = collection.get_supply(); + let collection_address: CollectionAddress = + get_collection_address(collection_name, creator_address.as_ref()); + let collection_address_str = collection_address.to_string(); + let creator_address_str = creator_address.to_string(); + // postgres insert + tokio::task::block_in_place(|| { + if let Ok(conn_string) = std::env::var("POSTGRES_CONNECTION_STRING") { + match postgres::Client::connect(&conn_string, NoTls) { + Ok(mut client) => { + let result = client.execute( + INSERT_OR_UPDATE_COLLECTION, + &[ + &collection_address_str, + &collection_name, + &creator_address_str, + &frozen, + &metadata_url, + &(supply as i64), + ], + ); + if let Err(e) = result { + tracing::error!("Failed to execute query: {}", e); + } + } + Err(e) => { + tracing::error!("Failed to connect to the database: {}", e); + } + } + } else { + tracing::error!("Environment variable POSTGRES_CONNECTION_STRING is not set"); + } + }) +} + +/// Syncs an NFT to the corresponding table "nfts" in postgres +/// Additionally, this function also has logic to track the counts of NFTs held by each user +/// in each collection. +#[offchain] +pub fn update_nft(nft: &Nft, old_owner: Option>) { + let collection_address = nft.get_collection_address().to_string(); + let nft_id = nft.get_token_id(); + let new_owner_str = nft.get_owner().to_string(); + let frozen = nft.is_frozen(); + let metadata_url = nft.get_token_uri(); + let old_owner_address = old_owner.map(|x| x.to_string()); + + tokio::task::block_in_place(|| { + if let Ok(conn_string) = std::env::var("POSTGRES_CONNECTION_STRING") { + let mut client = postgres::Client::connect(&conn_string, NoTls).unwrap(); + + // Check current owner in the database for the NFT + let rows = client + .query( + QUERY_OWNER_FROM_NFTS, + &[&collection_address, &(nft_id as i64)], + ) + .unwrap(); + + let db_owner: Option = rows.get(0).map(|row| row.get(0)); + + // Handle ownership change logic for top_owners table + if let Some(db_owner_str) = db_owner { + if old_owner_address.is_none() { + // This means it's a mint operation but the NFT already exists in the table. + // Do nothing as we shouldn't increment in this scenario. + } else if old_owner_address.as_ref() != Some(&new_owner_str) { + // Transfer occurred + + // Decrement count for the database owner (which would be the old owner in a transfer scenario) + let _ = client.execute( + DECREMENT_COUNT_FOR_OLD_OWNER, + &[&db_owner_str, &collection_address], + ); + + // Increment count for new owner + let _ = client.execute( + INCREMENT_OR_UPDATE_COUNT_FOR_NEW_OWNER, + &[&new_owner_str, &collection_address], + ); + } + } else if old_owner_address.is_none() { + // Mint operation, and NFT doesn't exist in the database. Increment for the new owner. + let _ = client.execute( + INCREMENT_OR_UPDATE_COUNT_FOR_NEW_OWNER, + &[&new_owner_str, &collection_address], + ); + } + + // Update NFT information after handling top_owners logic + let _ = client.execute( + INSERT_OR_UPDATE_NFT, + &[ + &collection_address, + &(nft_id as i64), + &metadata_url, + &new_owner_str, + &frozen, + ], + ); + } + }) +} diff --git a/module-system/module-implementations/sov-nft-module/src/sql.rs b/module-system/module-implementations/sov-nft-module/src/sql.rs new file mode 100644 index 0000000000..6b521a877a --- /dev/null +++ b/module-system/module-implementations/sov-nft-module/src/sql.rs @@ -0,0 +1,30 @@ +pub const INSERT_OR_UPDATE_COLLECTION: &str = "INSERT INTO collections (\ + collection_address, collection_name, creator_address,\ + frozen, metadata_url, supply)\ + VALUES ($1, $2, $3, $4, $5, $6)\ + ON CONFLICT (collection_address)\ + DO UPDATE SET collection_name = EXCLUDED.collection_name,\ + creator_address = EXCLUDED.creator_address,\ + frozen = EXCLUDED.frozen,\ + metadata_url = EXCLUDED.metadata_url,\ + supply = EXCLUDED.supply"; + +pub const QUERY_OWNER_FROM_NFTS: &str = + "SELECT owner FROM nfts WHERE collection_address = $1 AND nft_id = $2"; + +pub const DECREMENT_COUNT_FOR_OLD_OWNER: &str = "UPDATE top_owners SET count = count - 1 \ + WHERE owner = $1 AND collection_address = $2 AND count > 0"; + +pub const INCREMENT_OR_UPDATE_COUNT_FOR_NEW_OWNER: &str = + "INSERT INTO top_owners (owner, collection_address, count) VALUES ($1, $2, 1) \ + ON CONFLICT (owner, collection_address) \ + DO UPDATE SET count = top_owners.count + 1"; + +pub const INSERT_OR_UPDATE_NFT: &str = "INSERT INTO nfts (\ + collection_address, nft_id, metadata_url,\ + owner, frozen)\ + VALUES ($1, $2, $3, $4, $5)\ + ON CONFLICT (collection_address, nft_id)\ + DO UPDATE SET metadata_url = EXCLUDED.metadata_url,\ + owner = EXCLUDED.owner,\ + frozen = EXCLUDED.frozen"; diff --git a/module-system/sov-modules-api/src/transaction.rs b/module-system/sov-modules-api/src/transaction.rs index 1810fbd86a..bad6da5274 100644 --- a/module-system/sov-modules-api/src/transaction.rs +++ b/module-system/sov-modules-api/src/transaction.rs @@ -6,7 +6,9 @@ use crate::PrivateKey; use crate::{Context, Signature}; /// A Transaction object that is compatible with the module-system/sov-default-stf. -#[derive(Debug, PartialEq, Eq, Clone, borsh::BorshDeserialize, borsh::BorshSerialize)] +#[derive( + Debug, PartialEq, Eq, Clone, borsh::BorshDeserialize, borsh::BorshSerialize, serde::Serialize, +)] pub struct Transaction { signature: C::Signature, pub_key: C::PublicKey, diff --git a/module-system/sov-modules-macros/src/lib.rs b/module-system/sov-modules-macros/src/lib.rs index 40a942af5a..c5bd5d1ca1 100644 --- a/module-system/sov-modules-macros/src/lib.rs +++ b/module-system/sov-modules-macros/src/lib.rs @@ -18,6 +18,7 @@ mod manifest; mod module_call_json_schema; mod module_info; mod new_types; +mod offchain; #[cfg(feature = "native")] mod rpc; @@ -29,10 +30,11 @@ use dispatch::genesis::GenesisMacro; use dispatch::message_codec::MessageCodec; use module_call_json_schema::derive_module_call_json_schema; use new_types::address_type_helper; +use offchain::offchain_generator; use proc_macro::TokenStream; #[cfg(feature = "native")] use rpc::ExposeRpcMacro; -use syn::{parse_macro_input, DeriveInput}; +use syn::{parse_macro_input, DeriveInput, ItemFn}; #[proc_macro_derive(ModuleInfo, attributes(state, module, address, gas))] pub fn module_info(input: TokenStream) -> TokenStream { @@ -263,3 +265,38 @@ pub fn address_type(_attr: TokenStream, item: TokenStream) -> TokenStream { let input = parse_macro_input!(item as DeriveInput); handle_macro_error(address_type_helper(input)) } + +/// The offchain macro is used to annotate functions that should only be executed by the rollup +/// when the "offchain" feature flag is passed. The macro produces one of two functions depending on +/// the presence flag. +/// "offchain" feature enabled: function is present as defined +/// "offchain" feature absent: function body is replaced with an empty definition +/// +/// The idea here is that offchain computation is optionally enabled for a full node and is not +/// part of chain state and does not impact consensus, prover or anything else. +/// +/// ## Example +/// ``` +/// use sov_modules_macros::offchain; +/// #[offchain] +/// fn redis_insert(count: u64){ +/// println!("Inserting {} to redis", count); +/// } +/// ``` +/// +/// This is exactly equivalent to hand-writing +///``` +/// #[cfg(feature = "offchain")] +/// fn redis_insert(count: u64){ +/// println!("Inserting {} to redis", count); +/// } +/// +/// #[cfg(not(feature = "offchain"))] +/// fn redis_insert(count: u64){ +/// } +///``` +#[proc_macro_attribute] +pub fn offchain(_attr: TokenStream, item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as ItemFn); + handle_macro_error(offchain_generator(input)) +} diff --git a/module-system/sov-modules-macros/src/offchain.rs b/module-system/sov-modules-macros/src/offchain.rs new file mode 100644 index 0000000000..274021ad9e --- /dev/null +++ b/module-system/sov-modules-macros/src/offchain.rs @@ -0,0 +1,31 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::ItemFn; + +pub fn offchain_generator(function: ItemFn) -> Result { + let visibility = &function.vis; + let name = &function.sig.ident; + let inputs = &function.sig.inputs; + let output = &function.sig.output; + let block = &function.block; + let generics = &function.sig.generics; + let where_clause = &function.sig.generics.where_clause; + let asyncness = &function.sig.asyncness; + + let output = quote! { + // The "real" function + #[cfg(feature = "offchain")] + #visibility #asyncness fn #name #generics(#inputs) #output #where_clause { + #block + } + + // The no-op function + #[cfg(not(feature = "offchain"))] + #[allow(unused_variables)] + #visibility #asyncness fn #name #generics(#inputs) #output #where_clause { + // Do nothing. Should be optimized away + } + }; + + Ok(output.into()) +} diff --git a/packages_to_publish.yml b/packages_to_publish.yml index cf287870f7..2143d509a2 100644 --- a/packages_to_publish.yml +++ b/packages_to_publish.yml @@ -17,6 +17,7 @@ - sov-prover-incentives - sov-chain-state - sov-blob-storage +- sov-nft-module # Adapters - sov-risc0-adapter diff --git a/utils/nft-utils/Cargo.toml b/utils/nft-utils/Cargo.toml new file mode 100644 index 0000000000..6375f1ee69 --- /dev/null +++ b/utils/nft-utils/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "nft-utils" +authors = { workspace = true } +description = "Utils for NFTs" +edition = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +rust-version = { workspace = true } +version = { workspace = true } +readme = "README.md" +resolver = "2" +autotests = false +publish = false + +[dependencies] +borsh = { workspace = true } +tokio = { workspace = true } + +sov-nft-module = { path = "../../module-system/module-implementations/sov-nft-module", features = ["native"]} +sov-sequencer = { path = "../../full-node/sov-sequencer"} +demo-stf = {path = "../../examples/demo-stf"} +sov-rollup-interface = {path = "../../rollup-interface"} + +sov-modules-api = {path = "../../module-system/sov-modules-api", features = ["native"]} diff --git a/utils/nft-utils/src/lib.rs b/utils/nft-utils/src/lib.rs new file mode 100644 index 0000000000..bb280d5982 --- /dev/null +++ b/utils/nft-utils/src/lib.rs @@ -0,0 +1,201 @@ +use borsh::ser::BorshSerialize; +use demo_stf::runtime::RuntimeCall; +use sov_modules_api::default_context::DefaultContext; +use sov_modules_api::default_signature::private_key::DefaultPrivateKey; +use sov_modules_api::transaction::Transaction; +use sov_modules_api::{Address, PrivateKey}; +use sov_nft_module::utils::get_collection_address; +use sov_nft_module::{CallMessage, CollectionAddress, UserAddress}; +use sov_rollup_interface::mocks::MockDaSpec; + +fn get_collection_metadata_url(base_url: &str, collection_address: &str) -> String { + format!("{}/collection/{}", base_url, collection_address) +} + +fn get_nft_metadata_url(base_url: &str, collection_address: &str, nft_id: u64) -> String { + format!("{}/nft/{}/{}", base_url, collection_address, nft_id) +} + +/// Convenience and readability wrapper for build_create_collection_transaction +pub fn build_create_collection_transactions( + creator_pk: &DefaultPrivateKey, + start_nonce: &mut u64, + base_uri: &str, + collections: &[&str], +) -> Vec> { + collections + .iter() + .map(|&collection_name| { + let tx = build_create_collection_transaction( + creator_pk, + *start_nonce, + collection_name, + base_uri, + ); + *start_nonce += 1; + tx + }) + .collect() +} + +/// Constructs a transaction to create a new NFT collection. +/// +/// # Arguments +/// +/// * `signer`: The private key used for signing the transaction. +/// * `nonce`: The nonce to be used for the transaction. +/// * `collection_name`: The name of the collection to be created. +/// +/// # Returns +/// +/// Returns a signed transaction for creating a new NFT collection. +pub fn build_create_collection_transaction( + signer: &DefaultPrivateKey, + nonce: u64, + collection_name: &str, + base_uri: &str, +) -> Transaction { + let collection_address = get_collection_address::( + collection_name, + signer.default_address().as_ref(), + ); + + let collection_uri = get_collection_metadata_url(base_uri, &collection_address.to_string()); + let create_collection_message = RuntimeCall::::nft( + CallMessage::::CreateCollection { + name: collection_name.to_string(), + collection_uri, + }, + ); + Transaction::::new_signed_tx( + signer, + create_collection_message.try_to_vec().unwrap(), + nonce, + ) +} + +/// Convenience and readability wrapper for build_mint_nft_transaction +pub fn build_mint_transactions( + creator_pk: &DefaultPrivateKey, + start_nonce: &mut u64, + collection: &str, + start_nft_id: &mut u64, + num: usize, + base_uri: &str, + owner_pk: &DefaultPrivateKey, +) -> Vec> { + (0..num) + .map(|_| { + let tx = build_mint_nft_transaction( + creator_pk, + *start_nonce, + collection, + *start_nft_id, + base_uri, + &owner_pk.default_address(), + ); + *start_nft_id += 1; + *start_nonce += 1; + tx + }) + .collect() +} + +/// Constructs a transaction to mint a new NFT. +/// +/// # Arguments +/// +/// * `signer`: The private key used for signing the transaction. +/// * `nonce`: The nonce to be used for the transaction. +/// * `collection_name`: The name of the collection to which the NFT belongs. +/// * `token_id`: The unique identifier for the new NFT. +/// * `owner`: The address of the user to whom the NFT will be minted. +/// +/// # Returns +/// +/// Returns a signed transaction for minting a new NFT to a specified user. +pub fn build_mint_nft_transaction( + signer: &DefaultPrivateKey, + nonce: u64, + collection_name: &str, + token_id: u64, + base_uri: &str, + owner: &Address, +) -> Transaction { + let collection_address = get_collection_address::( + collection_name, + signer.default_address().as_ref(), + ); + let token_uri = get_nft_metadata_url(base_uri, &collection_address.to_string(), token_id); + let mint_nft_message = + RuntimeCall::::nft(CallMessage::::MintNft { + collection_name: collection_name.to_string(), + token_uri, + token_id, + owner: UserAddress::new(owner), + frozen: false, + }); + Transaction::::new_signed_tx( + signer, + mint_nft_message.try_to_vec().unwrap(), + nonce, + ) +} + +/// Convenience and readability wrapper for build_transfer_nft_transaction +pub fn build_transfer_transactions( + signer: &DefaultPrivateKey, + start_nonce: &mut u64, + collection_address: &CollectionAddress, + nft_ids: Vec, +) -> Vec> { + nft_ids + .into_iter() + .map(|nft_id| { + let new_owner = DefaultPrivateKey::generate().default_address(); + let tx = build_transfer_nft_transaction( + signer, + *start_nonce, + collection_address, + nft_id, + &new_owner, + ); + *start_nonce += 1; + tx + }) + .collect() +} + +/// Constructs a transaction to transfer an NFT to another user. +/// +/// # Arguments +/// +/// * `signer`: The private key used for signing the transaction. +/// * `nonce`: The nonce to be used for the transaction. +/// * `collection_address`: The address of the collection to which the NFT belongs. +/// * `token_id`: The unique identifier for the NFT being transferred. +/// * `to`: The address of the user to whom the NFT will be transferred. +/// +/// # Returns +/// +/// Returns a signed transaction for transferring an NFT to a specified user. +pub fn build_transfer_nft_transaction( + signer: &DefaultPrivateKey, + nonce: u64, + collection_address: &CollectionAddress, + token_id: u64, + to: &Address, +) -> Transaction { + let transfer_message = RuntimeCall::::nft(CallMessage::< + DefaultContext, + >::TransferNft { + collection_address: collection_address.clone(), + token_id, + to: UserAddress::new(to), + }); + Transaction::::new_signed_tx( + signer, + transfer_message.try_to_vec().unwrap(), + nonce, + ) +} diff --git a/utils/nft-utils/src/main.rs b/utils/nft-utils/src/main.rs new file mode 100644 index 0000000000..15d01ae714 --- /dev/null +++ b/utils/nft-utils/src/main.rs @@ -0,0 +1,105 @@ +use std::thread; +use std::time::Duration; + +use nft_utils::{ + build_create_collection_transactions, build_mint_transactions, build_transfer_transactions, +}; +use sov_modules_api::default_context::DefaultContext; +use sov_modules_api::default_signature::private_key::DefaultPrivateKey; +use sov_nft_module::utils::get_collection_address; +use sov_sequencer::utils::SimpleClient; + +const COLLECTION_1: &str = "Sovereign Squirrel Syndicate"; +const COLLECTION_2: &str = "Celestial Dolphins"; +const COLLECTION_3: &str = "Risky Rhinos"; + +const DUMMY_URL: &str = "http://foobar.storage"; + +const PK1: [u8; 32] = [ + 199, 23, 116, 41, 227, 173, 69, 178, 7, 24, 164, 151, 88, 149, 52, 187, 102, 167, 163, 248, 38, + 86, 207, 66, 87, 81, 56, 66, 211, 150, 208, 155, +]; +const PK2: [u8; 32] = [ + 92, 136, 187, 3, 235, 27, 9, 215, 232, 93, 24, 78, 85, 255, 234, 60, 152, 21, 139, 246, 151, + 129, 152, 227, 231, 204, 38, 84, 159, 129, 71, 143, +]; +const PK3: [u8; 32] = [ + 233, 139, 68, 72, 169, 252, 229, 117, 72, 144, 47, 191, 13, 42, 32, 107, 190, 52, 102, 210, + 161, 208, 245, 116, 93, 84, 37, 87, 171, 44, 30, 239, +]; + +#[tokio::main] +async fn main() { + let creator_pk = DefaultPrivateKey::try_from(&PK1[..]).unwrap(); + let owner_1_pk = DefaultPrivateKey::try_from(&PK2[..]).unwrap(); + let owner_2_pk = DefaultPrivateKey::try_from(&PK3[..]).unwrap(); + + let client = SimpleClient::new("localhost", 12345).await.unwrap(); + + let mut nonce = 0; + let collections = [COLLECTION_1, COLLECTION_2, COLLECTION_3]; + let transactions = + build_create_collection_transactions(&creator_pk, &mut nonce, DUMMY_URL, &collections); + client.send_transactions(transactions, None).await.unwrap(); + + // sleep is necessary because of how the sequencer currently works + // without the sleep, there is a concurrency issue and some transactions would be ignored + // TODO: remove after https://github.com/Sovereign-Labs/sovereign-sdk/issues/949 is fixed + thread::sleep(Duration::from_millis(1000)); + + let mut nft_id = 1; + let mut transactions = build_mint_transactions( + &creator_pk, + &mut nonce, + COLLECTION_1, + &mut nft_id, + 15, + DUMMY_URL, + &owner_1_pk, + ); + + transactions.extend(build_mint_transactions( + &creator_pk, + &mut nonce, + COLLECTION_1, + &mut nft_id, + 5, + DUMMY_URL, + &owner_2_pk, + )); + let mut nft_id = 1; + transactions.extend(build_mint_transactions( + &creator_pk, + &mut nonce, + COLLECTION_2, + &mut nft_id, + 20, + DUMMY_URL, + &owner_1_pk, + )); + + client + .send_transactions(transactions.clone(), None) + .await + .unwrap(); + // TODO: remove after https://github.com/Sovereign-Labs/sovereign-sdk/issues/949 is fixed + thread::sleep(Duration::from_millis(3000)); + + let collection_1_address = get_collection_address::( + COLLECTION_1, + creator_pk.default_address().as_ref(), + ); + + let mut owner_1_nonce = 0; + let nft_ids_to_transfer: Vec = (1..=6).collect(); + transactions = build_transfer_transactions( + &owner_1_pk, + &mut owner_1_nonce, + &collection_1_address, + nft_ids_to_transfer, + ); + client + .send_transactions(transactions.clone(), None) + .await + .unwrap(); +} From a1df8c1dd56eae381c32b9296dd72b0551a144b7 Mon Sep 17 00:00:00 2001 From: Preston Evans <32944016+preston-evans98@users.noreply.github.com> Date: Sat, 30 Sep 2023 06:07:58 -0700 Subject: [PATCH 08/12] Use build script to install cargo-risczero (#960) * Use build script to install cargo-risczero * lint * clippy --- examples/demo-prover/build.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 examples/demo-prover/build.rs diff --git a/examples/demo-prover/build.rs b/examples/demo-prover/build.rs new file mode 100644 index 0000000000..4b1c195c95 --- /dev/null +++ b/examples/demo-prover/build.rs @@ -0,0 +1,15 @@ +use std::os::unix::process::ExitStatusExt; +use std::process::{Command, ExitStatus}; +fn main() { + let is_risczero_installed = Command::new("cargo") + .args(["risczero", "help"]) + .status() + .unwrap_or(ExitStatus::from_raw(1)); // If we can't execute the command, assume risczero isn't installed since duplicate install attempts are no-ops. + + if !is_risczero_installed.success() { + // If installation fails, just exit silently. The user can try again. + let _ = Command::new("cargo") + .args(["install", "cargo-risczero"]) + .status(); + } +} From f31359ef9e2bac32b3d6884d06187eb093a922fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Orkun=20Mahir=20K=C4=B1l=C4=B1=C3=A7?= Date: Mon, 2 Oct 2023 10:45:28 +0300 Subject: [PATCH 09/12] EVM: Improve EVM Implementation according to demo (#964) * fix bugs * lint * lint --- .../sov-evm/src/evm/error/rpc.rs | 12 +- .../sov-evm/src/query.rs | 163 +++++++++++------- 2 files changed, 102 insertions(+), 73 deletions(-) diff --git a/module-system/module-implementations/sov-evm/src/evm/error/rpc.rs b/module-system/module-implementations/sov-evm/src/evm/error/rpc.rs index cb78fa04f5..473ef472dc 100644 --- a/module-system/module-implementations/sov-evm/src/evm/error/rpc.rs +++ b/module-system/module-implementations/sov-evm/src/evm/error/rpc.rs @@ -1,5 +1,6 @@ //! Implementation specific Errors for the `eth_` namespace. +use std::convert::Infallible; use std::time::Duration; use jsonrpsee::core::Error as RpcError; @@ -184,18 +185,17 @@ impl From for EthApiError { } } -impl From> for EthApiError -where - T: Into, -{ - fn from(err: EVMError) -> Self { +impl From> for EthApiError { + fn from(err: EVMError) -> Self { match err { EVMError::Transaction(err) => RpcInvalidTransactionError::from(err).into(), EVMError::Header(InvalidHeader::PrevrandaoNotSet) => EthApiError::PrevrandaoNotSet, EVMError::Header(InvalidHeader::ExcessBlobGasNotSet) => { EthApiError::ExcessBlobGasNotSet } - EVMError::Database(err) => err.into(), + EVMError::Database(_) => { + EthApiError::Internal(RethError::Custom("Database error".into())) + } } } } diff --git a/module-system/module-implementations/sov-evm/src/query.rs b/module-system/module-implementations/sov-evm/src/query.rs index a6ddd13501..efb542d258 100644 --- a/module-system/module-implementations/sov-evm/src/query.rs +++ b/module-system/module-implementations/sov-evm/src/query.rs @@ -20,10 +20,25 @@ use crate::evm::{executor, prepare_call_env}; use crate::experimental::{MIN_CREATE_GAS, MIN_TRANSACTION_GAS}; use crate::{EthApiError, Evm}; -#[rpc_gen(client, server, namespace = "eth")] +#[rpc_gen(client, server)] impl Evm { + /// Handler for `net_version` + #[rpc_method(name = "net_version")] + pub fn net_version(&self, _working_set: &mut WorkingSet) -> RpcResult { + info!("evm module: net_version"); + + // Network ID is the same as chain ID for most networks + let chain_id = self + .cfg + .get(_working_set) + .expect("Evm config must be set") + .chain_id; + + Ok(chain_id.to_string()) + } + /// Handler for: `eth_chainId` - #[rpc_method(name = "chainId")] + #[rpc_method(name = "eth_chainId")] pub fn chain_id( &self, working_set: &mut WorkingSet, @@ -41,7 +56,7 @@ impl Evm { } /// Handler for: `eth_getBlockByNumber` - #[rpc_method(name = "getBlockByNumber")] + #[rpc_method(name = "eth_getBlockByNumber")] pub fn get_block_by_number( &self, block_number: Option, @@ -101,7 +116,7 @@ impl Evm { } /// Handler for: `eth_getBalance` - #[rpc_method(name = "getBalance")] + #[rpc_method(name = "eth_getBalance")] pub fn get_balance( &self, address: reth_primitives::Address, @@ -123,7 +138,7 @@ impl Evm { } /// Handler for: `eth_getStorageAt` - #[rpc_method(name = "getStorageAt")] + #[rpc_method(name = "eth_getStorageAt")] pub fn get_storage_at( &self, address: reth_primitives::Address, @@ -146,7 +161,7 @@ impl Evm { } /// Handler for: `eth_getTransactionCount` - #[rpc_method(name = "getTransactionCount")] + #[rpc_method(name = "eth_getTransactionCount")] pub fn get_transaction_count( &self, address: reth_primitives::Address, @@ -168,7 +183,7 @@ impl Evm { } /// Handler for: `eth_getCode` - #[rpc_method(name = "getCode")] + #[rpc_method(name = "eth_getCode")] pub fn get_code( &self, address: reth_primitives::Address, @@ -191,7 +206,7 @@ impl Evm { /// Handler for: `eth_feeHistory` // TODO https://github.com/Sovereign-Labs/sovereign-sdk/issues/502 - #[rpc_method(name = "feeHistory")] + #[rpc_method(name = "eth_feeHistory")] pub fn fee_history( &self, _working_set: &mut WorkingSet, @@ -207,7 +222,7 @@ impl Evm { /// Handler for: `eth_getTransactionByHash` // TODO https://github.com/Sovereign-Labs/sovereign-sdk/issues/502 - #[rpc_method(name = "getTransactionByHash")] + #[rpc_method(name = "eth_getTransactionByHash")] pub fn get_transaction_by_hash( &self, hash: reth_primitives::H256, @@ -248,7 +263,7 @@ impl Evm { /// Handler for: `eth_getTransactionReceipt` // TODO https://github.com/Sovereign-Labs/sovereign-sdk/issues/502 - #[rpc_method(name = "getTransactionReceipt")] + #[rpc_method(name = "eth_getTransactionReceipt")] pub fn get_transaction_receipt( &self, hash: reth_primitives::H256, @@ -284,7 +299,7 @@ impl Evm { /// Handler for: `eth_call` //https://github.com/paradigmxyz/reth/blob/f577e147807a783438a3f16aad968b4396274483/crates/rpc/rpc/src/eth/api/transactions.rs#L502 //https://github.com/paradigmxyz/reth/blob/main/crates/rpc/rpc-types/src/eth/call.rs#L7 - #[rpc_method(name = "call")] + #[rpc_method(name = "eth_call")] pub fn get_call( &self, request: reth_rpc_types::CallRequest, @@ -311,13 +326,16 @@ impl Evm { let evm_db: EvmDb<'_, C> = self.get_db(working_set); - let result = executor::inspect(evm_db, &block_env, tx_env, cfg_env).unwrap(); + let result = match executor::inspect(evm_db, &block_env, tx_env, cfg_env) { + Ok(result) => result.result, + Err(err) => return Err(EthApiError::from(err).into()), + }; - Ok(ensure_success(result.result)?) + Ok(ensure_success(result)?) } /// Handler for: `eth_blockNumber` - #[rpc_method(name = "blockNumber")] + #[rpc_method(name = "eth_blockNumber")] pub fn block_number( &self, working_set: &mut WorkingSet, @@ -334,7 +352,7 @@ impl Evm { /// Handler for: `eth_estimateGas` // https://github.com/paradigmxyz/reth/blob/main/crates/rpc/rpc/src/eth/api/call.rs#L172 - #[rpc_method(name = "estimateGas")] + #[rpc_method(name = "eth_estimateGas")] pub fn eth_estimate_gas( &self, request: reth_rpc_types::CallRequest, @@ -365,15 +383,23 @@ impl Evm { // configured gas limit let mut highest_gas_limit = request.gas.unwrap_or(U256::from(env_gas_limit)); - let account = self.accounts.get(&tx_env.caller, working_set).unwrap(); + let account = self + .accounts + .get(&tx_env.caller, working_set) + .map(|account| account.info) + .unwrap_or_default(); // if the request is a simple transfer we can optimize if tx_env.data.is_empty() { if let TransactTo::Call(to) = tx_env.transact_to { - let to_account = self.accounts.get(&to, working_set).unwrap(); - if KECCAK_EMPTY == to_account.info.code_hash { + let to_account = self + .accounts + .get(&to, working_set) + .map(|account| account.info) + .unwrap_or_default(); + if KECCAK_EMPTY == to_account.code_hash { // simple transfer, check if caller has sufficient funds - let available_funds = account.info.balance; + let available_funds = account.balance; if tx_env.value > available_funds { return Err(RpcInvalidTransactionError::InsufficientFundsForTransfer.into()); @@ -386,7 +412,7 @@ impl Evm { // check funds of the sender if tx_env.gas_price > U256::ZERO { // allowance is (balance - tx.value) / tx.gas_price - let allowance = (account.info.balance - tx_env.value) / tx_env.gas_price; + let allowance = (account.balance - tx_env.value) / tx_env.gas_price; if highest_gas_limit > allowance { // cap the highest gas limit by max gas caller can afford with given gas price @@ -415,33 +441,32 @@ impl Evm { } } - let result = result.unwrap(); - - match result.result { - ExecutionResult::Success { .. } => { - // succeeded - } - ExecutionResult::Halt { reason, gas_used } => { - return Err(RpcInvalidTransactionError::halt(reason, gas_used).into()) - } - ExecutionResult::Revert { output, .. } => { - // if price or limit was included in the request then we can execute the request - // again with the block's gas limit to check if revert is gas related or not - return if request_gas.is_some() || request_gas_price.is_some() { - let evm_db = self.get_db(working_set); - Err(map_out_of_gas_err(block_env, tx_env, cfg_env, evm_db).into()) - } else { - // the transaction did revert - Err(RpcInvalidTransactionError::Revert(RevertError::new(output)).into()) - }; - } - } + let result = match result { + Ok(result) => match result.result { + ExecutionResult::Success { .. } => result.result, + ExecutionResult::Halt { reason, gas_used } => { + return Err(RpcInvalidTransactionError::halt(reason, gas_used).into()) + } + ExecutionResult::Revert { output, .. } => { + // if price or limit was included in the request then we can execute the request + // again with the block's gas limit to check if revert is gas related or not + return if request_gas.is_some() || request_gas_price.is_some() { + let evm_db = self.get_db(working_set); + Err(map_out_of_gas_err(block_env, tx_env, cfg_env, evm_db).into()) + } else { + // the transaction did revert + Err(RpcInvalidTransactionError::Revert(RevertError::new(output)).into()) + }; + } + }, + Err(err) => return Err(EthApiError::from(err).into()), + }; // at this point we know the call succeeded but want to find the _best_ (lowest) gas the // transaction succeeds with. we find this by doing a binary search over the // possible range NOTE: this is the gas the transaction used, which is less than the // transaction requires to succeed - let gas_used = result.result.gas_used(); + let gas_used = result.gas_used(); // the lowest value is capped by the gas it takes for a transfer let mut lowest_gas_limit = if tx_env.transact_to.is_create() { MIN_CREATE_GAS @@ -475,30 +500,33 @@ impl Evm { continue; } - let result = result.unwrap(); - match result.result { - ExecutionResult::Success { .. } => { - // cap the highest gas limit with succeeding gas limit - highest_gas_limit = mid_gas_limit; - } - ExecutionResult::Revert { .. } => { - // increase the lowest gas limit - lowest_gas_limit = mid_gas_limit; - } - ExecutionResult::Halt { reason, .. } => { - match reason { - Halt::OutOfGas(_) => { - // increase the lowest gas limit - lowest_gas_limit = mid_gas_limit; - } - err => { - // these should be unreachable because we know the transaction succeeds, - // but we consider these cases an error - return Err(RpcInvalidTransactionError::EvmHalt(err).into()); + match result { + Ok(result) => match result.result { + ExecutionResult::Success { .. } => { + // cap the highest gas limit with succeeding gas limit + highest_gas_limit = mid_gas_limit; + } + ExecutionResult::Revert { .. } => { + // increase the lowest gas limit + lowest_gas_limit = mid_gas_limit; + } + ExecutionResult::Halt { reason, .. } => { + match reason { + Halt::OutOfGas(_) => { + // increase the lowest gas limit + lowest_gas_limit = mid_gas_limit; + } + err => { + // these should be unreachable because we know the transaction succeeds, + // but we consider these cases an error + return Err(RpcInvalidTransactionError::EvmHalt(err).into()); + } } } - } - } + }, + Err(err) => return Err(EthApiError::from(err).into()), + }; + // new midpoint mid_gas_limit = ((highest_gas_limit as u128 + lowest_gas_limit as u128) / 2) as u64; } @@ -508,7 +536,7 @@ impl Evm { /// Handler for: `eth_gasPrice` // TODO https://github.com/Sovereign-Labs/sovereign-sdk/issues/502 - #[rpc_method(name = "gasPrice")] + #[rpc_method(name = "eth_gasPrice")] pub fn gas_price(&self, _working_set: &mut WorkingSet) -> RpcResult { unimplemented!("eth_gasPrice not implemented") } @@ -529,8 +557,9 @@ impl Evm { .last(&mut working_set.accessory_state()) .expect("Head block must be set"), Some(ref block_number) => { - let block_number = - usize::from_str_radix(block_number, 16).expect("Block number must be hex"); + // hex representation may have 0x prefix + let block_number = usize::from_str_radix(block_number.trim_start_matches("0x"), 16) + .expect("Block number must be a valid hex number, with or without 0x prefix"); self.blocks .get(block_number, &mut working_set.accessory_state()) .expect("Block must be set") From 4f4e27ee8ea39ed8b4acc9792d3fde92d8fab881 Mon Sep 17 00:00:00 2001 From: Blazej Kolad Date: Mon, 2 Oct 2023 10:44:31 +0200 Subject: [PATCH 10/12] Custom serde serialization for DefaultPublicKey (#962) * Custo serde for DefaultPublicKey * fix lint * add test * Update soc-accounts * update genesis_config * Remove clone * Remove clones * Remove todo * fix make lint * fix lint --- examples/demo-stf/src/genesis_config.rs | 2 +- .../sov-accounts/src/genesis.rs | 12 ++-- .../sov-accounts/src/hooks.rs | 4 +- .../sov-accounts/src/lib.rs | 21 ++++--- .../sov-accounts/src/tests.rs | 16 +++--- .../sov-modules-api/src/default_signature.rs | 2 +- module-system/sov-modules-api/src/lib.rs | 2 +- .../sov-modules-api/src/pub_key_hex.rs | 6 +- .../sov-modules-api/src/serde_pub_key.rs | 56 +++++++++++++++++++ 9 files changed, 90 insertions(+), 31 deletions(-) create mode 100644 module-system/sov-modules-api/src/serde_pub_key.rs diff --git a/examples/demo-stf/src/genesis_config.rs b/examples/demo-stf/src/genesis_config.rs index 2443216026..9b0a64f8d3 100644 --- a/examples/demo-stf/src/genesis_config.rs +++ b/examples/demo-stf/src/genesis_config.rs @@ -93,7 +93,7 @@ fn create_genesis_config( let accounts_data = std::fs::read_to_string(accounts_genesis_path) .with_context(|| format!("Failed to read genesis from {}", accounts_genesis_path))?; - let accounts_config: AccountConfig = serde_json::from_str(&accounts_data) + let accounts_config: AccountConfig = serde_json::from_str(&accounts_data) .with_context(|| format!("Failed to parse genesis from {}", accounts_genesis_path))?; let nft_config = sov_nft_module::NonFungibleTokenConfig {}; diff --git a/module-system/module-implementations/sov-accounts/src/genesis.rs b/module-system/module-implementations/sov-accounts/src/genesis.rs index 5b80b98578..ffee617eae 100644 --- a/module-system/module-implementations/sov-accounts/src/genesis.rs +++ b/module-system/module-implementations/sov-accounts/src/genesis.rs @@ -9,9 +9,8 @@ impl Accounts { config: &::Config, working_set: &mut WorkingSet, ) -> Result<()> { - for pub_key_hex in config.pub_keys.iter() { - let pub_key = pub_key_hex.try_into()?; - if self.accounts.get(&pub_key, working_set).is_some() { + for pub_key in config.pub_keys.iter() { + if self.accounts.get(pub_key, working_set).is_some() { bail!("Account already exists") } @@ -23,7 +22,7 @@ impl Accounts { pub(crate) fn create_default_account( &self, - pub_key: C::PublicKey, + pub_key: &C::PublicKey, working_set: &mut WorkingSet, ) -> Result> { let default_address = pub_key.to_address(); @@ -34,10 +33,9 @@ impl Accounts { nonce: 0, }; - self.accounts.set(&pub_key, &new_account, working_set); + self.accounts.set(pub_key, &new_account, working_set); - self.public_keys - .set(&default_address, &pub_key, working_set); + self.public_keys.set(&default_address, pub_key, working_set); Ok(new_account) } diff --git a/module-system/module-implementations/sov-accounts/src/hooks.rs b/module-system/module-implementations/sov-accounts/src/hooks.rs index c785b7a9df..249ab8fe7e 100644 --- a/module-system/module-implementations/sov-accounts/src/hooks.rs +++ b/module-system/module-implementations/sov-accounts/src/hooks.rs @@ -12,9 +12,9 @@ impl TxHooks for Accounts { tx: &Transaction, working_set: &mut WorkingSet, ) -> anyhow::Result<::Address> { - let pub_key = tx.pub_key().clone(); + let pub_key = tx.pub_key(); - let account = match self.accounts.get(&pub_key, working_set) { + let account = match self.accounts.get(pub_key, working_set) { Some(acc) => Ok(acc), None => self.create_default_account(pub_key, working_set), }?; diff --git a/module-system/module-implementations/sov-accounts/src/lib.rs b/module-system/module-implementations/sov-accounts/src/lib.rs index 7fa0a1e667..922fce9f4d 100644 --- a/module-system/module-implementations/sov-accounts/src/lib.rs +++ b/module-system/module-implementations/sov-accounts/src/lib.rs @@ -12,17 +12,18 @@ pub use query::*; mod tests; pub use call::{CallMessage, UPDATE_ACCOUNT_MSG}; -use sov_modules_api::{Context, Error, ModuleInfo, PublicKeyHex, WorkingSet}; +use sov_modules_api::{Context, Error, ModuleInfo, WorkingSet}; /// Initial configuration for sov-accounts module. #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct AccountConfig { +#[serde(bound = "C::PublicKey: serde::Serialize + serde::de::DeserializeOwned")] +pub struct AccountConfig { /// Public keys to initialize the rollup. - pub pub_keys: Vec, + pub pub_keys: Vec, } -impl FromIterator for AccountConfig { - fn from_iter>(iter: T) -> Self { +impl FromIterator for AccountConfig { + fn from_iter>(iter: T) -> Self { Self { pub_keys: iter.into_iter().collect(), } @@ -58,7 +59,7 @@ pub struct Accounts { impl sov_modules_api::Module for Accounts { type Context = C; - type Config = AccountConfig; + type Config = AccountConfig; type CallMessage = call::CallMessage; @@ -94,7 +95,11 @@ where } #[cfg(feature = "arbitrary")] -impl<'a> arbitrary::Arbitrary<'a> for AccountConfig { +impl<'a, C> arbitrary::Arbitrary<'a> for AccountConfig +where + C: Context, + C::PublicKey: arbitrary::Arbitrary<'a>, +{ fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { // TODO we might want a dedicated struct that will generate the private key counterpart so // payloads can be signed and verified @@ -118,7 +123,7 @@ where ) -> arbitrary::Result { use sov_modules_api::Module; - let config: AccountConfig = u.arbitrary()?; + let config: AccountConfig = u.arbitrary()?; let accounts = Accounts::default(); accounts diff --git a/module-system/module-implementations/sov-accounts/src/tests.rs b/module-system/module-implementations/sov-accounts/src/tests.rs index 9b6804cba7..5731685308 100644 --- a/module-system/module-implementations/sov-accounts/src/tests.rs +++ b/module-system/module-implementations/sov-accounts/src/tests.rs @@ -17,8 +17,8 @@ fn test_config_serialization() { ) .unwrap(); - let config = AccountConfig { - pub_keys: vec![pub_key.clone().try_into().unwrap()], + let config = AccountConfig:: { + pub_keys: vec![pub_key.clone()], }; let data = r#" @@ -26,7 +26,7 @@ fn test_config_serialization() { "pub_keys":["1cd4e2d9d5943e6f3d12589d31feee6bb6c11e7b8cd996a393623e207da72cbf"] }"#; - let parsed_config: AccountConfig = serde_json::from_str(data).unwrap(); + let parsed_config: AccountConfig = serde_json::from_str(data).unwrap(); assert_eq!(parsed_config, config); } @@ -37,7 +37,7 @@ fn test_config_account() { let init_pub_key_addr = init_pub_key.to_address::<::Address>(); let account_config = AccountConfig { - pub_keys: vec![init_pub_key.clone().try_into().unwrap()], + pub_keys: vec![init_pub_key.clone()], }; let accounts = &mut Accounts::::default(); @@ -76,7 +76,7 @@ fn test_update_account() { // Test new account creation { accounts - .create_default_account(sender.clone(), native_working_set) + .create_default_account(&sender, native_working_set) .unwrap(); let query_response = accounts @@ -135,7 +135,7 @@ fn test_update_account_fails() { let sender_context_1 = C::new(sender_1.to_address()); accounts - .create_default_account(sender_1, native_working_set) + .create_default_account(&sender_1, native_working_set) .unwrap(); let priv_key = DefaultPrivateKey::generate(); @@ -143,7 +143,7 @@ fn test_update_account_fails() { let sig_2 = priv_key.sign(&call::UPDATE_ACCOUNT_MSG); accounts - .create_default_account(sender_2.clone(), native_working_set) + .create_default_account(&sender_2, native_working_set) .unwrap(); // The new public key already exists and the call fails. @@ -167,7 +167,7 @@ fn test_get_account_after_pub_key_update() { let sender_context_1 = C::new(sender_1_addr); accounts - .create_default_account(sender_1, native_working_set) + .create_default_account(&sender_1, native_working_set) .unwrap(); let priv_key = DefaultPrivateKey::generate(); diff --git a/module-system/sov-modules-api/src/default_signature.rs b/module-system/sov-modules-api/src/default_signature.rs index 75b11777d1..224c75d4bf 100644 --- a/module-system/sov-modules-api/src/default_signature.rs +++ b/module-system/sov-modules-api/src/default_signature.rs @@ -167,7 +167,7 @@ pub mod private_key { } #[cfg_attr(feature = "native", derive(schemars::JsonSchema))] -#[derive(PartialEq, Eq, Clone, Debug, serde::Serialize, serde::Deserialize)] +#[derive(PartialEq, Eq, Clone, Debug)] pub struct DefaultPublicKey { #[cfg_attr( feature = "native", diff --git a/module-system/sov-modules-api/src/lib.rs b/module-system/sov-modules-api/src/lib.rs index 22d430ab9e..1230a089a7 100644 --- a/module-system/sov-modules-api/src/lib.rs +++ b/module-system/sov-modules-api/src/lib.rs @@ -21,6 +21,7 @@ pub use reexport_macros::*; mod prefix; mod response; mod serde_address; +mod serde_pub_key; mod state; #[cfg(test)] mod tests; @@ -166,7 +167,6 @@ pub trait PublicKey: + Sync + Serialize + for<'a> Deserialize<'a> - + for<'a> TryFrom<&'a PublicKeyHex, Error = anyhow::Error> { fn to_address(&self) -> A; } diff --git a/module-system/sov-modules-api/src/pub_key_hex.rs b/module-system/sov-modules-api/src/pub_key_hex.rs index 07667ca3c6..97f6445307 100644 --- a/module-system/sov-modules-api/src/pub_key_hex.rs +++ b/module-system/sov-modules-api/src/pub_key_hex.rs @@ -56,8 +56,8 @@ impl From for String { } } -impl From for PublicKeyHex { - fn from(pub_key: DefaultPublicKey) -> Self { +impl From<&DefaultPublicKey> for PublicKeyHex { + fn from(pub_key: &DefaultPublicKey) -> Self { let hex = hex::encode(pub_key.pub_key.as_bytes()); Self { hex } } @@ -89,7 +89,7 @@ mod tests { #[test] fn test_pub_key_hex() { let pub_key = DefaultPrivateKey::generate().pub_key(); - let pub_key_hex = PublicKeyHex::try_from(pub_key.clone()).unwrap(); + let pub_key_hex = PublicKeyHex::try_from(&pub_key).unwrap(); let converted_pub_key = DefaultPublicKey::try_from(&pub_key_hex).unwrap(); assert_eq!(pub_key, converted_pub_key); } diff --git a/module-system/sov-modules-api/src/serde_pub_key.rs b/module-system/sov-modules-api/src/serde_pub_key.rs new file mode 100644 index 0000000000..e669dcad4b --- /dev/null +++ b/module-system/sov-modules-api/src/serde_pub_key.rs @@ -0,0 +1,56 @@ +use ed25519_dalek::VerifyingKey as DalekPublicKey; + +use crate::default_signature::DefaultPublicKey; +use crate::PublicKeyHex; + +impl serde::Serialize for DefaultPublicKey { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if serializer.is_human_readable() { + serde::Serialize::serialize(&PublicKeyHex::from(self), serializer) + } else { + serde::Serialize::serialize(&self.pub_key, serializer) + } + } +} + +impl<'de> serde::Deserialize<'de> for DefaultPublicKey { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + if deserializer.is_human_readable() { + let pub_key_hex: PublicKeyHex = serde::Deserialize::deserialize(deserializer)?; + Ok(DefaultPublicKey::try_from(&pub_key_hex).map_err(serde::de::Error::custom)?) + } else { + let pub_key: DalekPublicKey = serde::Deserialize::deserialize(deserializer)?; + Ok(DefaultPublicKey { pub_key }) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_pub_key_json() { + let pub_key_hex: PublicKeyHex = + "022e229198d957bf0c0a504e7d7bcec99a1d62cccc7861ed2452676ad0323ad8" + .try_into() + .unwrap(); + + let pub_key = DefaultPublicKey::try_from(&pub_key_hex).unwrap(); + let pub_key_str: String = serde_json::to_string(&pub_key).unwrap(); + + assert_eq!( + pub_key_str, + r#""022e229198d957bf0c0a504e7d7bcec99a1d62cccc7861ed2452676ad0323ad8""# + ); + + let deserialized: DefaultPublicKey = serde_json::from_str(&pub_key_str).unwrap(); + assert_eq!(deserialized, pub_key); + } +} From 86d1f0000ced233178d55289ac5cc75fa3c61d70 Mon Sep 17 00:00:00 2001 From: Blazej Kolad Date: Mon, 2 Oct 2023 13:44:17 +0200 Subject: [PATCH 11/12] Read `bank` genesis from a file. (#961) * Read bank genesis from a file * fix lint * add read_json_file * make lint fix * fix lint --- examples/demo-stf/src/genesis_config.rs | 47 +++++++++---------- examples/test-data/genesis/bank.json | 10 ++++ .../sov-bank/src/lib.rs | 10 ++-- .../sov-bank/src/tests.rs | 39 +++++++++++++++ 4 files changed, 77 insertions(+), 29 deletions(-) create mode 100644 examples/test-data/genesis/bank.json create mode 100644 module-system/module-implementations/sov-bank/src/tests.rs diff --git a/examples/demo-stf/src/genesis_config.rs b/examples/demo-stf/src/genesis_config.rs index 9b0a64f8d3..1b8da3e12f 100644 --- a/examples/demo-stf/src/genesis_config.rs +++ b/examples/demo-stf/src/genesis_config.rs @@ -1,7 +1,12 @@ +use std::convert::AsRef; +use std::path::Path; + use anyhow::Context as AnyhowContext; #[cfg(feature = "experimental")] use reth_primitives::Bytes; +use serde::de::DeserializeOwned; use sov_accounts::AccountConfig; +use sov_bank::BankConfig; use sov_chain_state::ChainStateConfig; use sov_cli::wallet_state::PrivateKeyAndAddress; #[cfg(feature = "experimental")] @@ -32,12 +37,9 @@ pub fn get_genesis_config( sequencer_da_address: Da::Address, #[cfg(feature = "experimental")] evm_genesis_addresses: Vec, ) -> GenesisConfig { - // This will be read from a file: #872 - let initial_sequencer_balance = 100000000; let token_deployer: PrivateKeyAndAddress = read_private_key(); create_genesis_config( - initial_sequencer_balance, token_deployer.address.clone(), sequencer_da_address, #[cfg(feature = "experimental")] @@ -47,24 +49,13 @@ pub fn get_genesis_config( } fn create_genesis_config( - initial_sequencer_balance: u64, sequencer_address: C::Address, sequencer_da_address: Da::Address, #[cfg(feature = "experimental")] evm_genesis_addresses: Vec, ) -> anyhow::Result> { - // This will be read from a file: #872 - let token_config: sov_bank::TokenConfig = sov_bank::TokenConfig { - token_name: DEMO_TOKEN_NAME.to_owned(), - address_and_balances: vec![(sequencer_address.clone(), initial_sequencer_balance)], - authorized_minters: vec![sequencer_address.clone()], - salt: 0, - }; - - // This will be read from a file: #872 - let bank_config = sov_bank::BankConfig { - tokens: vec![token_config], - }; - + // This path will be injected as a parameter: #872 + let bank_genesis_path = "../test-data/genesis/bank.json"; + let bank_config: BankConfig = read_json_file(bank_genesis_path)?; // This will be read from a file: #872 let token_address = sov_bank::get_genesis_token_address::( &bank_config.tokens[0].token_name, @@ -84,17 +75,10 @@ fn create_genesis_config( // This path will be injected as a parameter: #872 let value_setter_genesis_path = "../test-data/genesis/value_setter.json"; - let value_setter_data = std::fs::read_to_string(value_setter_genesis_path) - .with_context(|| format!("Failed to read genesis from {}", value_setter_genesis_path))?; - let value_setter_config: ValueSetterConfig = serde_json::from_str(&value_setter_data) - .with_context(|| format!("Failed to parse genesis from {}", value_setter_genesis_path))?; + let value_setter_config: ValueSetterConfig = read_json_file(value_setter_genesis_path)?; let accounts_genesis_path = "../test-data/genesis/accounts.json"; - let accounts_data = std::fs::read_to_string(accounts_genesis_path) - .with_context(|| format!("Failed to read genesis from {}", accounts_genesis_path))?; - - let accounts_config: AccountConfig = serde_json::from_str(&accounts_data) - .with_context(|| format!("Failed to parse genesis from {}", accounts_genesis_path))?; + let accounts_config: AccountConfig = read_json_file(accounts_genesis_path)?; let nft_config = sov_nft_module::NonFungibleTokenConfig {}; // This will be read from a file: #872 @@ -117,6 +101,17 @@ fn create_genesis_config( )) } +fn read_json_file>(path: P) -> anyhow::Result { + let path_str = path.as_ref().display(); + + let data = std::fs::read_to_string(&path) + .with_context(|| format!("Failed to read genesis from {}", path_str))?; + let config: T = serde_json::from_str(&data) + .with_context(|| format!("Failed to parse genesis from {}", path_str))?; + + Ok(config) +} + // TODO: #840 #[cfg(feature = "experimental")] fn get_evm_config(genesis_addresses: Vec) -> EvmConfig { diff --git a/examples/test-data/genesis/bank.json b/examples/test-data/genesis/bank.json new file mode 100644 index 0000000000..35b065659d --- /dev/null +++ b/examples/test-data/genesis/bank.json @@ -0,0 +1,10 @@ +{ + "tokens":[ + { + "token_name":"sov-demo-token", + "address_and_balances":[["sov1l6n2cku82yfqld30lanm2nfw43n2auc8clw7r5u5m6s7p8jrm4zqrr8r94",100000000]], + "authorized_minters":["sov1l6n2cku82yfqld30lanm2nfw43n2auc8clw7r5u5m6s7p8jrm4zqrr8r94"] + ,"salt":0 + } + ] +} \ No newline at end of file diff --git a/module-system/module-implementations/sov-bank/src/lib.rs b/module-system/module-implementations/sov-bank/src/lib.rs index 7f3cece428..99d194bd89 100644 --- a/module-system/module-implementations/sov-bank/src/lib.rs +++ b/module-system/module-implementations/sov-bank/src/lib.rs @@ -6,12 +6,14 @@ mod genesis; mod query; #[cfg(feature = "native")] pub use query::*; +#[cfg(test)] +mod tests; mod token; /// Util functions for bank pub mod utils; - /// Specifies the call methods using in that module. pub use call::CallMessage; +use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use sov_modules_api::{CallResponse, Error, GasUnit, ModuleInfo, WorkingSet}; use token::Token; @@ -22,7 +24,8 @@ pub use utils::{get_genesis_token_address, get_token_address}; /// [`TokenConfig`] specifies a configuration used when generating a token for the bank /// module. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +#[serde(bound = "C::Address: Serialize + DeserializeOwned")] pub struct TokenConfig { /// The name of the token. pub token_name: String, @@ -35,7 +38,8 @@ pub struct TokenConfig { } /// Initial configuration for sov-bank module. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +#[serde(bound = "C::Address: Serialize + DeserializeOwned")] pub struct BankConfig { /// A list of configurations for the initial tokens. pub tokens: Vec>, diff --git a/module-system/module-implementations/sov-bank/src/tests.rs b/module-system/module-implementations/sov-bank/src/tests.rs new file mode 100644 index 0000000000..0e5661a107 --- /dev/null +++ b/module-system/module-implementations/sov-bank/src/tests.rs @@ -0,0 +1,39 @@ +use std::str::FromStr; + +use sov_modules_api::default_context::DefaultContext; +use sov_modules_api::{AddressBech32, Spec}; + +use crate::{BankConfig, TokenConfig}; + +#[test] +fn test_config_serialization() { + let address: ::Address = + AddressBech32::from_str("sov1l6n2cku82yfqld30lanm2nfw43n2auc8clw7r5u5m6s7p8jrm4zqrr8r94") + .unwrap() + .into(); + + let config = BankConfig:: { + tokens: vec![TokenConfig { + token_name: "sov-demo-token".to_owned(), + address_and_balances: vec![(address, 100000000)], + authorized_minters: vec![address], + salt: 0, + }], + }; + + let data = r#" + { + "tokens":[ + { + "token_name":"sov-demo-token", + "address_and_balances":[["sov1l6n2cku82yfqld30lanm2nfw43n2auc8clw7r5u5m6s7p8jrm4zqrr8r94",100000000]], + "authorized_minters":["sov1l6n2cku82yfqld30lanm2nfw43n2auc8clw7r5u5m6s7p8jrm4zqrr8r94"] + ,"salt":0 + } + ] + }"#; + + let parsed_config: BankConfig = serde_json::from_str(data).unwrap(); + + assert_eq!(config, parsed_config) +} From baebd034797b7df37d036f199348cbe0fa862efe Mon Sep 17 00:00:00 2001 From: Blazej Kolad Date: Mon, 2 Oct 2023 18:53:12 +0200 Subject: [PATCH 12/12] Read `chain_state` genesis from a file (#973) * chain state config * remove nft config * Add unit test * fix imports * fix lint --- examples/demo-stf/src/genesis_config.rs | 12 ++++------ examples/test-data/genesis/chain_state.json | 7 ++++++ .../sov-chain-state/src/lib.rs | 4 +++- .../sov-chain-state/src/tests.rs | 24 +++++++++++++++++++ 4 files changed, 39 insertions(+), 8 deletions(-) create mode 100644 examples/test-data/genesis/chain_state.json create mode 100644 module-system/module-implementations/sov-chain-state/src/tests.rs diff --git a/examples/demo-stf/src/genesis_config.rs b/examples/demo-stf/src/genesis_config.rs index 1b8da3e12f..c28b2aa220 100644 --- a/examples/demo-stf/src/genesis_config.rs +++ b/examples/demo-stf/src/genesis_config.rs @@ -13,6 +13,7 @@ use sov_cli::wallet_state::PrivateKeyAndAddress; use sov_evm::{AccountData, EvmConfig, SpecId}; pub use sov_modules_api::default_context::DefaultContext; use sov_modules_api::Context; +use sov_nft_module::NonFungibleTokenConfig; use sov_rollup_interface::da::DaSpec; pub use sov_state::config::Config as StorageConfig; use sov_value_setter::ValueSetterConfig; @@ -80,13 +81,10 @@ fn create_genesis_config( let accounts_genesis_path = "../test-data/genesis/accounts.json"; let accounts_config: AccountConfig = read_json_file(accounts_genesis_path)?; - let nft_config = sov_nft_module::NonFungibleTokenConfig {}; - // This will be read from a file: #872 - let chain_state_config = ChainStateConfig { - // TODO: Put actual value - initial_slot_height: 0, - current_time: Default::default(), - }; + let nft_config: NonFungibleTokenConfig = NonFungibleTokenConfig {}; + + let chain_state_path = "../test-data/genesis/chain_state.json"; + let chain_state_config: ChainStateConfig = read_json_file(chain_state_path)?; Ok(GenesisConfig::new( bank_config, diff --git a/examples/test-data/genesis/chain_state.json b/examples/test-data/genesis/chain_state.json new file mode 100644 index 0000000000..2e7f97e93a --- /dev/null +++ b/examples/test-data/genesis/chain_state.json @@ -0,0 +1,7 @@ +{ + "initial_slot_height":0, + "current_time":{ + "secs":0, + "nanos":0 + } +} \ No newline at end of file diff --git a/module-system/module-implementations/sov-chain-state/src/lib.rs b/module-system/module-implementations/sov-chain-state/src/lib.rs index 4de0fbe14c..ea8647628e 100644 --- a/module-system/module-implementations/sov-chain-state/src/lib.rs +++ b/module-system/module-implementations/sov-chain-state/src/lib.rs @@ -3,6 +3,8 @@ /// Contains the call methods used by the module pub mod call; +#[cfg(test)] +mod tests; /// Genesis state configuration pub mod genesis; @@ -147,7 +149,7 @@ pub struct ChainState } /// Initial configuration of the chain state -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] pub struct ChainStateConfig { /// Initial slot height pub initial_slot_height: TransitionHeight, diff --git a/module-system/module-implementations/sov-chain-state/src/tests.rs b/module-system/module-implementations/sov-chain-state/src/tests.rs new file mode 100644 index 0000000000..b13ce48f4b --- /dev/null +++ b/module-system/module-implementations/sov-chain-state/src/tests.rs @@ -0,0 +1,24 @@ +use sov_rollup_interface::da::{NanoSeconds, Time}; + +use crate::ChainStateConfig; + +#[test] +fn test_config_serialization() { + let time = Time::new(2, NanoSeconds::new(3).unwrap()); + let config = ChainStateConfig { + initial_slot_height: 1, + current_time: time, + }; + + let data = r#" + { + "initial_slot_height":1, + "current_time":{ + "secs":2, + "nanos":3 + } + }"#; + + let parsed_config: ChainStateConfig = serde_json::from_str(data).unwrap(); + assert_eq!(config, parsed_config) +}