From 4a89f213ac429ffa10c52c01386adc5dc965a57b Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Mon, 4 Mar 2024 10:42:04 +0000 Subject: [PATCH 01/12] FIX: Re-publish finality vote after a minute (#747) --- fendermint/app/config/default.toml | 5 ++ fendermint/app/settings/src/lib.rs | 3 ++ fendermint/app/src/cmd/run.rs | 1 + fendermint/vm/topdown/src/voting.rs | 78 +++++++++++++++++++---------- 4 files changed, 61 insertions(+), 26 deletions(-) diff --git a/fendermint/app/config/default.toml b/fendermint/app/config/default.toml index 276212f73..0053a6976 100644 --- a/fendermint/app/config/default.toml +++ b/fendermint/app/config/default.toml @@ -200,3 +200,8 @@ subnet_id = "/r0" # The minimum is 1 seconds which is about the minimum target block time as well; # ideally one round of gossip per block should be as frequent as we would go. vote_interval = 1 +# Voting timeout about top-down finality, in seconds. After this time, if there is +# no new height to vote on, the previous vote is re-published. This is to avoid +# potential stalling because peers missed an important vote and the cache is full, +# pausing the syncer, preventing new events to trigger votes. +vote_timeout = 60 diff --git a/fendermint/app/settings/src/lib.rs b/fendermint/app/settings/src/lib.rs index af59a9e0d..60347490a 100644 --- a/fendermint/app/settings/src/lib.rs +++ b/fendermint/app/settings/src/lib.rs @@ -149,6 +149,9 @@ pub struct IpcSettings { /// Interval with which votes can be gossiped. #[serde_as(as = "DurationSeconds")] pub vote_interval: Duration, + /// Timeout after which the last vote is re-published. + #[serde_as(as = "DurationSeconds")] + pub vote_timeout: Duration, /// The config for top down checkpoint. It's None if subnet id is root or not activating /// any top down checkpoint related operations pub topdown: Option, diff --git a/fendermint/app/src/cmd/run.rs b/fendermint/app/src/cmd/run.rs index a55fe4b5d..5bc134f0d 100644 --- a/fendermint/app/src/cmd/run.rs +++ b/fendermint/app/src/cmd/run.rs @@ -160,6 +160,7 @@ async fn run(settings: Settings) -> anyhow::Result<()> { publish_vote_loop( parent_finality_votes, settings.ipc.vote_interval, + settings.ipc.vote_timeout, key, own_subnet_id, client, diff --git a/fendermint/vm/topdown/src/voting.rs b/fendermint/vm/topdown/src/voting.rs index a11c040db..0e43b7a95 100644 --- a/fendermint/vm/topdown/src/voting.rs +++ b/fendermint/vm/topdown/src/voting.rs @@ -322,7 +322,10 @@ where /// Poll the vote tally for new finalized blocks and publish a vote about them if the validator is part of the power table. pub async fn publish_vote_loop( vote_tally: VoteTally, + // Throttle votes to maximum 1/interval vote_interval: Duration, + // Publish a vote after a timeout even if it's the same as before. + vote_timeout: Duration, key: libp2p::identity::Keypair, subnet_id: ipc_api::subnet_id::SubnetID, client: ipc_ipld_resolver::Client, @@ -336,46 +339,69 @@ pub async fn publish_vote_loop( let mut vote_interval = tokio::time::interval(vote_interval); vote_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); - let mut prev_height = 0; + let mut prev = None; loop { - let result = atomically_or_err(|| { - let next_height = vote_tally.latest_height()?; - - if next_height == prev_height { - retry()?; - } + let prev_height = prev + .as_ref() + .map(|(height, _, _)| *height) + .unwrap_or_default(); + + let result = tokio::time::timeout( + vote_timeout, + atomically_or_err(|| { + let next_height = vote_tally.latest_height()?; + + if next_height == prev_height { + retry()?; + } - let next_hash = match vote_tally.block_hash(next_height)? { - Some(next_hash) => next_hash, - None => retry()?, - }; + let next_hash = match vote_tally.block_hash(next_height)? { + Some(next_hash) => next_hash, + None => retry()?, + }; - let has_power = vote_tally.has_power(&validator_key)?; + let has_power = vote_tally.has_power(&validator_key)?; - if has_power { - // Add our own vote to the tally directly rather than expecting a message from the gossip channel. - // TODO (ENG-622): I'm not sure gossip messages published by this node would be delivered to it, so this might be the only way. - // NOTE: We should not see any other error from this as we just checked that the validator had power, - // but for piece of mind let's return and log any potential errors, rather than ignore them. - vote_tally.add_vote(validator_key.clone(), next_height, next_hash.clone())?; - } + if has_power { + // Add our own vote to the tally directly rather than expecting a message from the gossip channel. + // TODO (ENG-622): I'm not sure gossip messages published by this node would be delivered to it, so this might be the only way. + // NOTE: We should not see any other error from this as we just checked that the validator had power, + // but for piece of mind let's return and log any potential errors, rather than ignore them. + vote_tally.add_vote(validator_key.clone(), next_height, next_hash.clone())?; + } - Ok((next_height, next_hash, has_power)) - }) + Ok((next_height, next_hash, has_power)) + }), + ) .await; let (next_height, next_hash, has_power) = match result { - Ok(vs) => vs, - Err(e) => { - tracing::error!(error = e.to_string(), "faled to get next height to vote on"); + Ok(Ok(vs)) => vs, + Err(_) => { + if let Some(ref vs) = prev { + tracing::debug!("vote timeout; re-publishing previous vote"); + vs.clone() + } else { + tracing::debug!("vote timeout, but no previous vote to re-publish"); + continue; + } + } + Ok(Err(e)) => { + tracing::error!( + error = e.to_string(), + "failed to get next height to vote on" + ); continue; } }; if has_power && prev_height > 0 { tracing::debug!(block_height = next_height, "publishing finality vote"); - match VoteRecord::signed(&key, subnet_id.clone(), to_vote(next_height, next_hash)) { + + let vote = to_vote(next_height, next_hash.clone()); + + match VoteRecord::signed(&key, subnet_id.clone(), vote) { Ok(vote) => { if let Err(e) = client.publish_vote(vote) { tracing::error!(error = e.to_string(), "failed to publish vote"); @@ -393,6 +419,6 @@ pub async fn publish_vote_loop( vote_interval.tick().await; } - prev_height = next_height; + prev = Some((next_height, next_hash, has_power)); } } From 926114c48caabfa7e51d3af68762b2bd1a14d228 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Mon, 4 Mar 2024 11:23:17 +0000 Subject: [PATCH 02/12] ENG-578: IT (Part 6) - Materializer CLI (#753) --- Cargo.lock | 2 + fendermint/app/Cargo.toml | 11 +-- fendermint/app/options/Cargo.toml | 1 + fendermint/app/options/src/lib.rs | 5 ++ fendermint/app/options/src/materializer.rs | 69 ++++++++++++++ fendermint/app/src/cmd/materializer.rs | 89 +++++++++++++++++++ fendermint/app/src/cmd/mod.rs | 2 + fendermint/docker/runner.Dockerfile | 2 +- fendermint/testing/materializer/Cargo.toml | 2 +- .../materializer/src/docker/container.rs | 19 ++-- .../materializer/src/docker/dropper.rs | 60 ++++++++++++- .../testing/materializer/src/docker/mod.rs | 36 ++++++-- .../materializer/src/docker/network.rs | 53 ++++++----- .../testing/materializer/src/docker/node.rs | 58 +++++++++--- .../testing/materializer/src/docker/runner.rs | 13 ++- .../testing/materializer/src/logging.rs | 17 ---- .../testing/materializer/src/manifest.rs | 32 ++++++- .../testing/materializer/tests/docker.rs | 17 ++-- .../tests/docker_tests/root_only.rs | 6 +- 19 files changed, 401 insertions(+), 93 deletions(-) create mode 100644 fendermint/app/options/src/materializer.rs create mode 100644 fendermint/app/src/cmd/materializer.rs diff --git a/Cargo.lock b/Cargo.lock index 70bea0a96..396991dbf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2851,6 +2851,7 @@ dependencies = [ "fendermint_app_settings", "fendermint_crypto", "fendermint_eth_api", + "fendermint_materializer", "fendermint_rocksdb", "fendermint_rpc", "fendermint_storage", @@ -2907,6 +2908,7 @@ dependencies = [ "bytes", "cid", "clap 4.5.1", + "fendermint_materializer", "fendermint_vm_actor_interface", "fendermint_vm_genesis", "fvm_ipld_encoding", diff --git a/fendermint/app/Cargo.toml b/fendermint/app/Cargo.toml index 50ec16140..f139da0c4 100644 --- a/fendermint/app/Cargo.toml +++ b/fendermint/app/Cargo.toml @@ -38,22 +38,23 @@ literally = { workspace = true } fendermint_abci = { path = "../abci" } fendermint_app_options = { path = "./options" } -fendermint_crypto = { path = "../crypto" } fendermint_app_settings = { path = "./settings" } -fendermint_storage = { path = "../storage" } +fendermint_crypto = { path = "../crypto" } +fendermint_eth_api = { path = "../eth/api" } +fendermint_materializer = { path = "../testing/materializer" } fendermint_rocksdb = { path = "../rocksdb" } fendermint_rpc = { path = "../rpc" } -fendermint_eth_api = { path = "../eth/api" } +fendermint_storage = { path = "../storage" } fendermint_vm_actor_interface = { path = "../vm/actor_interface" } fendermint_vm_core = { path = "../vm/core" } -fendermint_vm_event = { path = "../vm/event" } fendermint_vm_encoding = { path = "../vm/encoding" } +fendermint_vm_event = { path = "../vm/event" } fendermint_vm_genesis = { path = "../vm/genesis" } fendermint_vm_interpreter = { path = "../vm/interpreter", features = ["bundle"] } fendermint_vm_message = { path = "../vm/message" } fendermint_vm_resolver = { path = "../vm/resolver" } -fendermint_vm_topdown = { path = "../vm/topdown" } fendermint_vm_snapshot = { path = "../vm/snapshot" } +fendermint_vm_topdown = { path = "../vm/topdown" } fvm = { workspace = true } fvm_ipld_blockstore = { workspace = true } diff --git a/fendermint/app/options/Cargo.toml b/fendermint/app/options/Cargo.toml index 06eaa9291..b939777d5 100644 --- a/fendermint/app/options/Cargo.toml +++ b/fendermint/app/options/Cargo.toml @@ -26,3 +26,4 @@ url = { workspace = true } fendermint_vm_genesis = { path = "../../vm/genesis" } fendermint_vm_actor_interface = { path = "../../vm/actor_interface" } +fendermint_materializer = { path = "../../testing/materializer" } diff --git a/fendermint/app/options/src/lib.rs b/fendermint/app/options/src/lib.rs index d9cee8626..f2a1ee787 100644 --- a/fendermint/app/options/src/lib.rs +++ b/fendermint/app/options/src/lib.rs @@ -5,12 +5,14 @@ use std::path::PathBuf; use clap::{Args, Parser, Subcommand, ValueEnum}; use fvm_shared::address::Network; +use materializer::MaterializerArgs; use self::{eth::EthArgs, genesis::GenesisArgs, key::KeyArgs, rpc::RpcArgs, run::RunArgs}; pub mod eth; pub mod genesis; pub mod key; +pub mod materializer; pub mod rpc; pub mod run; @@ -136,6 +138,9 @@ pub enum Commands { Rpc(RpcArgs), /// Subcommands related to the Ethereum API facade. Eth(EthArgs), + /// Subcommands related to the Testnet Materializer. + #[clap(aliases = &["mat", "matr", "mate"])] + Materializer(MaterializerArgs), } #[cfg(test)] diff --git a/fendermint/app/options/src/materializer.rs b/fendermint/app/options/src/materializer.rs new file mode 100644 index 000000000..d0f5c1e51 --- /dev/null +++ b/fendermint/app/options/src/materializer.rs @@ -0,0 +1,69 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use std::path::PathBuf; + +use clap::{Args, Subcommand}; +use fendermint_materializer::TestnetId; + +#[derive(Args, Debug)] +pub struct MaterializerArgs { + /// Path to the directory where the materializer can store its artifacts. + /// + /// This must be the same between materializer invocations. + #[arg( + long, + short, + env = "FM_MATERIALIZER__DATA_DIR", + default_value = "~/.ipc/materializer" + )] + pub data_dir: PathBuf, + + /// Seed for random values in the materialized testnet. + #[arg(long, short, env = "FM_MATERIALIZER__SEED", default_value = "0")] + pub seed: u64, + + #[command(subcommand)] + pub command: MaterializerCommands, +} + +#[derive(Subcommand, Debug)] +pub enum MaterializerCommands { + /// Validate a testnet manifest. + Validate(MaterializerValidateArgs), + /// Setup a testnet. + Setup(MaterializerSetupArgs), + /// Tear down a testnet. + Remove(MaterializerRemoveArgs), +} + +#[derive(Args, Debug)] +pub struct MaterializerValidateArgs { + /// Path to the manifest file. + /// + /// The format of the manifest (e.g. JSON or YAML) will be determined based on the file extension. + #[arg(long, short)] + pub manifest_file: PathBuf, +} + +#[derive(Args, Debug)] +pub struct MaterializerSetupArgs { + /// Path to the manifest file. + /// + /// The format of the manifest (e.g. JSON or YAML) will be determined based on the file extension. + /// + /// The name of the manifest (without the extension) will act as the testnet ID. + #[arg(long, short)] + pub manifest_file: PathBuf, + + /// Run validation before attempting to set up the testnet. + #[arg(long, short, default_value = "false")] + pub validate: bool, +} + +#[derive(Args, Debug)] +pub struct MaterializerRemoveArgs { + /// ID of the testnet to remove. + #[arg(long, short)] + pub testnet_id: TestnetId, +} diff --git a/fendermint/app/src/cmd/materializer.rs b/fendermint/app/src/cmd/materializer.rs new file mode 100644 index 000000000..400c3ec46 --- /dev/null +++ b/fendermint/app/src/cmd/materializer.rs @@ -0,0 +1,89 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use std::path::Path; + +use anyhow::anyhow; +use fendermint_app_options::materializer::*; +use fendermint_app_settings::utils::expand_tilde; +use fendermint_materializer::{ + docker::{DockerMaterializer, DropPolicy}, + manifest::Manifest, + testnet::Testnet, + TestnetId, TestnetName, +}; + +use crate::cmd; + +cmd! { + MaterializerArgs(self) { + let d = expand_tilde(&self.data_dir); + let m = || DockerMaterializer::new(&d, self.seed).map(|m| m.with_policy(DropPolicy::PERSISTENT)); + match &self.command { + MaterializerCommands::Validate(args) => args.exec(()).await, + MaterializerCommands::Setup(args) => args.exec(m()?).await, + MaterializerCommands::Remove(args) => args.exec(m()?).await, + } + } +} + +cmd! { + MaterializerValidateArgs(self) { + validate(&self.manifest_file).await + } +} + +cmd! { + MaterializerSetupArgs(self, m: DockerMaterializer) { + setup(m, &self.manifest_file, self.validate).await + } +} + +cmd! { + MaterializerRemoveArgs(self, m: DockerMaterializer) { + remove(m, self.testnet_id.clone()).await + } +} + +/// Validate a manifest. +async fn validate(manifest_file: &Path) -> anyhow::Result<()> { + let (name, manifest) = read_manifest(manifest_file)?; + manifest.validate(&name).await +} + +/// Setup a testnet. +async fn setup( + mut m: DockerMaterializer, + manifest_file: &Path, + validate: bool, +) -> anyhow::Result<()> { + let (name, manifest) = read_manifest(manifest_file)?; + + if validate { + manifest.validate(&name).await?; + } + + let _testnet = Testnet::setup(&mut m, &name, &manifest).await?; + + Ok(()) +} + +/// Remove a testnet. +async fn remove(mut m: DockerMaterializer, id: TestnetId) -> anyhow::Result<()> { + m.remove(&TestnetName::new(id)).await +} + +/// Read a manifest file; use its file name as the testnet name. +fn read_manifest(manifest_file: &Path) -> anyhow::Result<(TestnetName, Manifest)> { + let testnet_id = manifest_file + .file_stem() + .ok_or_else(|| anyhow!("manifest file has no stem"))? + .to_string_lossy() + .to_string(); + + let name = TestnetName::new(testnet_id); + + let manifest = Manifest::from_file(manifest_file)?; + + Ok((name, manifest)) +} diff --git a/fendermint/app/src/cmd/mod.rs b/fendermint/app/src/cmd/mod.rs index c1a521d5a..e1276be8b 100644 --- a/fendermint/app/src/cmd/mod.rs +++ b/fendermint/app/src/cmd/mod.rs @@ -13,6 +13,7 @@ use async_trait::async_trait; pub mod eth; pub mod genesis; pub mod key; +pub mod materializer; pub mod rpc; pub mod run; @@ -64,6 +65,7 @@ pub async fn exec(opts: &Options) -> anyhow::Result<()> { Commands::Genesis(args) => args.exec(()).await, Commands::Rpc(args) => args.exec(()).await, Commands::Eth(args) => args.exec(settings(opts)?.eth).await, + Commands::Materializer(args) => args.exec(()).await, } } diff --git a/fendermint/docker/runner.Dockerfile b/fendermint/docker/runner.Dockerfile index 487b7b38f..9e124a75c 100644 --- a/fendermint/docker/runner.Dockerfile +++ b/fendermint/docker/runner.Dockerfile @@ -7,7 +7,7 @@ FROM debian:bookworm-slim RUN apt-get update && \ - apt-get install -y libssl3 ca-certificates && \ + apt-get install -y libssl3 ca-certificates curl && \ rm -rf /var/lib/apt/lists/* ENV FM_HOME_DIR=/fendermint diff --git a/fendermint/testing/materializer/Cargo.toml b/fendermint/testing/materializer/Cargo.toml index 7808f15c1..5e44b34a4 100644 --- a/fendermint/testing/materializer/Cargo.toml +++ b/fendermint/testing/materializer/Cargo.toml @@ -25,6 +25,7 @@ serde_json = { workspace = true } serde_yaml = { workspace = true } tendermint-rpc = { workspace = true } tokio = { workspace = true } +toml = { workspace = true } tracing = { workspace = true } arbitrary = { workspace = true, optional = true } @@ -51,7 +52,6 @@ serde_yaml = { workspace = true } serial_test = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true } -toml = { workspace = true } # Enable arb on self for tests. fendermint_materializer = { path = ".", features = ["arb"] } diff --git a/fendermint/testing/materializer/src/docker/container.rs b/fendermint/testing/materializer/src/docker/container.rs index 6a0106834..ca911f406 100644 --- a/fendermint/testing/materializer/src/docker/container.rs +++ b/fendermint/testing/materializer/src/docker/container.rs @@ -13,7 +13,7 @@ use bollard::{ }; use super::{ - dropper::{DropCommand, DropHandle}, + dropper::{DropChute, DropCommand, DropPolicy}, DockerConstruct, }; @@ -22,12 +22,12 @@ const KILL_TIMEOUT_SECS: i64 = 5; pub struct DockerContainer { docker: Docker, - dropper: DropHandle, + dropper: DropChute, container: DockerConstruct, } impl DockerContainer { - pub fn new(docker: Docker, dropper: DropHandle, container: DockerConstruct) -> Self { + pub fn new(docker: Docker, dropper: DropChute, container: DockerConstruct) -> Self { Self { docker, dropper, @@ -42,7 +42,8 @@ impl DockerContainer { /// Get a container by name, if it exists. pub async fn get( docker: Docker, - dropper: DropHandle, + dropper: DropChute, + drop_policy: &DropPolicy, name: String, ) -> anyhow::Result> { let mut filters = HashMap::new(); @@ -65,15 +66,15 @@ impl DockerContainer { .clone() .ok_or_else(|| anyhow!("docker container {name} has no id"))?; - Ok(Some(Self { + Ok(Some(Self::new( docker, dropper, - container: DockerConstruct { + DockerConstruct { id, name, - external: true, + keep: drop_policy.keep(false), }, - })) + ))) } } } @@ -137,7 +138,7 @@ impl DockerContainer { impl Drop for DockerContainer { fn drop(&mut self) { - if self.container.external { + if self.container.keep { return; } if self diff --git a/fendermint/testing/materializer/src/docker/dropper.rs b/fendermint/testing/materializer/src/docker/dropper.rs index 279edce53..e02e6c4f8 100644 --- a/fendermint/testing/materializer/src/docker/dropper.rs +++ b/fendermint/testing/materializer/src/docker/dropper.rs @@ -15,15 +15,67 @@ pub enum DropCommand { DropContainer(String), } -pub type DropHandle = tokio::sync::mpsc::UnboundedSender; +pub type DropChute = tokio::sync::mpsc::UnboundedSender; +pub type DropHandle = tokio::task::JoinHandle<()>; + +/// Decide whether to keep or discard constructs when they go out of scope. +#[derive(Clone, Debug)] +pub struct DropPolicy { + pub keep_existing: bool, + pub keep_created: bool, +} + +impl DropPolicy { + /// A network meant to be ephemeral, which aims to drop even what exists, + /// assuming it only exists because it was created by itself earlier, + /// but due to some error it failed to be removed. + pub const EPHEMERAL: DropPolicy = DropPolicy { + keep_existing: false, + keep_created: false, + }; + + /// Keep everything around, which is good for CLI applications that + /// set up networks that should exist until explicitly removed. + pub const PERSISTENT: DropPolicy = DropPolicy { + keep_existing: true, + keep_created: true, + }; + + /// Policy which only tries to remove artifacts which were created + /// by this materializer, but leaves existing resources around. + /// This can be useful for reading manifests for networks that + /// exists outside the tests, run tests agains the containers, + /// then leave them around for another round of testing, while + /// still maintaining the option of adding some ephemeral resources + /// form the test itself. + pub const DROP_CREATED: DropPolicy = DropPolicy { + keep_created: false, + keep_existing: true, + }; + + /// Decide if something should be kept when it's out of scope. + pub fn keep(&self, is_new: bool) -> bool { + if is_new { + self.keep_created + } else { + self.keep_existing + } + } +} + +impl Default for DropPolicy { + fn default() -> Self { + Self::DROP_CREATED + } +} /// Start a background task to remove docker constructs. /// /// The loop will exit when all clones of the sender channel have been dropped. -pub fn start(docker: Docker) -> DropHandle { +pub fn start(docker: Docker) -> (DropHandle, DropChute) { let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); - tokio::task::spawn(async move { + let handle = tokio::task::spawn(async move { while let Some(cmd) = rx.recv().await { match cmd { DropCommand::DropNetwork(id) => { @@ -80,5 +132,5 @@ pub fn start(docker: Docker) -> DropHandle { } }); - tx + (handle, tx) } diff --git a/fendermint/testing/materializer/src/docker/mod.rs b/fendermint/testing/materializer/src/docker/mod.rs index ae6cba86f..eee26e896 100644 --- a/fendermint/testing/materializer/src/docker/mod.rs +++ b/fendermint/testing/materializer/src/docker/mod.rs @@ -45,10 +45,13 @@ mod node; mod relayer; mod runner; +pub use dropper::DropPolicy; pub use network::DockerNetwork; pub use node::DockerNode; pub use relayer::DockerRelayer; +use self::dropper::DropHandle; + const STATE_JSON_FILE_NAME: &str = "materializer-state.json"; const DOCKER_ENTRY_SCRIPT: &str = include_str!("../../scripts/docker-entry.sh"); @@ -89,7 +92,7 @@ pub struct DockerConstruct { pub name: String, /// Indicate whether the thing was created outside the test, /// or it can be destroyed when it goes out of scope. - pub external: bool, + pub keep: bool, } /// Allocated (inclusive) range we can use to expose containers' ports on the host. @@ -136,7 +139,9 @@ pub struct DockerMaterializer { dir: PathBuf, rng: StdRng, docker: bollard::Docker, - dropper: dropper::DropHandle, + drop_handle: dropper::DropHandle, + drop_chute: dropper::DropChute, + drop_policy: dropper::DropPolicy, state: DockerMaterializerState, } @@ -148,7 +153,7 @@ impl DockerMaterializer { Docker::connect_with_local_defaults().context("failed to connect to Docker")?; // Create a runtime for the execution of drop tasks. - let dropper = dropper::start(docker.clone()); + let (drop_handle, drop_chute) = dropper::start(docker.clone()); // Read in the state if it exists, otherwise create a default one. let state = import_json(dir.join(STATE_JSON_FILE_NAME)) @@ -159,8 +164,10 @@ impl DockerMaterializer { dir: dir.into(), rng: StdRng::seed_from_u64(seed), docker, - dropper, + drop_handle, + drop_chute, state, + drop_policy: DropPolicy::default(), }; m.save_state().context("failed to save state")?; @@ -169,6 +176,11 @@ impl DockerMaterializer { Ok(m) } + pub fn with_policy(mut self, policy: DropPolicy) -> Self { + self.drop_policy = policy; + self + } + /// Remove all traces of a testnet. pub async fn remove(&mut self, testnet_name: &TestnetName) -> anyhow::Result<()> { let testnet = testnet_name.path_string(); @@ -235,6 +247,16 @@ impl DockerMaterializer { Ok(()) } + /// Replace the dropper with a new one and return the existing one so that we can await all the drop tasks being completed. + pub fn take_dropper(&mut self) -> DropHandle { + let (mut drop_handle, mut drop_chute) = dropper::start(self.docker.clone()); + std::mem::swap(&mut drop_handle, &mut self.drop_handle); + std::mem::swap(&mut drop_chute, &mut self.drop_chute); + // By dropping the `drop_chute` the only the existing docker constructs will keep a reference to it. + // The caller can decide when it's time to wait on the handle, when the testnet have been dropped. + drop_handle + } + /// Path to a directory based on a resource name. fn path>(&self, name: T) -> PathBuf { let name: &ResourceName = name.as_ref(); @@ -328,8 +350,9 @@ impl Materializer for DockerMaterializer { ) -> anyhow::Result<::Network> { DockerNetwork::get_or_create( self.docker.clone(), - self.dropper.clone(), + self.drop_chute.clone(), testnet_name.clone(), + &self.drop_policy, ) .await } @@ -455,7 +478,8 @@ impl Materializer for DockerMaterializer { DockerNode::get_or_create( &self.dir, self.docker.clone(), - self.dropper.clone(), + self.drop_chute.clone(), + &self.drop_policy, node_name, node_config, port_range, diff --git a/fendermint/testing/materializer/src/docker/network.rs b/fendermint/testing/materializer/src/docker/network.rs index ca2e59863..579286f30 100644 --- a/fendermint/testing/materializer/src/docker/network.rs +++ b/fendermint/testing/materializer/src/docker/network.rs @@ -13,13 +13,13 @@ use bollard::{ use crate::TestnetName; use super::{ - dropper::{DropCommand, DropHandle}, + dropper::{DropChute, DropCommand, DropPolicy}, DockerConstruct, }; pub struct DockerNetwork { docker: Docker, - dropper: DropHandle, + dropper: DropChute, /// There is a single docker network created for the entire testnet. testnet_name: TestnetName, network: DockerConstruct, @@ -38,8 +38,9 @@ impl DockerNetwork { /// if not, create a new docker network for the testnet. pub async fn get_or_create( docker: Docker, - dropper: DropHandle, + dropper: DropChute, testnet_name: TestnetName, + drop_policy: &DropPolicy, ) -> anyhow::Result { let network_name = testnet_name.path_string(); @@ -51,7 +52,7 @@ impl DockerNetwork { .await .context("failed to list docker networks")?; - let (id, external) = match networks.first() { + let (id, is_new) = match networks.first() { None => { let network: NetworkCreateResponse = docker .create_network(CreateNetworkOptions { @@ -66,7 +67,7 @@ impl DockerNetwork { .clone() .ok_or_else(|| anyhow!("created docker network has no id"))?; - (id, false) + (id, true) } Some(network) => { let id = network @@ -74,7 +75,7 @@ impl DockerNetwork { .clone() .ok_or_else(|| anyhow!("docker network {network_name} has no id"))?; - (id, true) + (id, false) } }; @@ -85,7 +86,7 @@ impl DockerNetwork { network: DockerConstruct { id, name: network_name, - external, + keep: drop_policy.keep(is_new), }, }) } @@ -93,7 +94,7 @@ impl DockerNetwork { impl Drop for DockerNetwork { fn drop(&mut self) { - if self.network.external { + if self.network.keep { return; } if self @@ -115,30 +116,39 @@ mod tests { use std::time::Duration; use super::DockerNetwork; - use crate::{docker::dropper, TestnetName}; + use crate::{ + docker::dropper::{self, DropPolicy}, + TestnetName, + }; #[tokio::test] async fn test_network() { let tn = TestnetName::new("test-network"); let docker = Docker::connect_with_local_defaults().expect("failed to connect to docker"); - let dropper = dropper::start(docker.clone()); - - let n1 = DockerNetwork::get_or_create(docker.clone(), dropper.clone(), tn.clone()) - .await - .expect("failed to create network"); - - let n2 = DockerNetwork::get_or_create(docker.clone(), dropper.clone(), tn.clone()) + let (drop_handle, drop_chute) = dropper::start(docker.clone()); + let drop_policy = DropPolicy::default(); + + let n1 = DockerNetwork::get_or_create( + docker.clone(), + drop_chute.clone(), + tn.clone(), + &drop_policy, + ) + .await + .expect("failed to create network"); + + let n2 = DockerNetwork::get_or_create(docker.clone(), drop_chute, tn.clone(), &drop_policy) .await .expect("failed to get network"); assert!( - !n1.network.external, - "when created, the network should not be external" + !n1.network.keep, + "when created, the network should not be marked to keep" ); assert!( - n2.network.external, - "when already exists, the network should be external" + n2.network.keep, + "when already exists, the network should be kept" ); assert_eq!(n1.network.id, n2.network.id); assert_eq!(n1.network.name, n2.network.name); @@ -156,6 +166,9 @@ mod tests { assert!(exists().await, "network still exists after n2 dropped"); drop(n1); + + let _ = drop_handle.await; + assert!( !exists().await, "network should be removed when n1 is dropped" diff --git a/fendermint/testing/materializer/src/docker/node.rs b/fendermint/testing/materializer/src/docker/node.rs index 1863171cd..0b3730190 100644 --- a/fendermint/testing/materializer/src/docker/node.rs +++ b/fendermint/testing/materializer/src/docker/node.rs @@ -14,8 +14,10 @@ use ethers::types::H160; use lazy_static::lazy_static; use super::{ - container::DockerContainer, dropper::DropHandle, runner::DockerRunner, DockerMaterials, - DockerPortRange, EnvVars, Volumes, + container::DockerContainer, + dropper::{DropChute, DropPolicy}, + runner::DockerRunner, + DockerMaterials, DockerPortRange, EnvVars, Volumes, }; use crate::{ docker::DOCKER_ENTRY_FILE_NAME, @@ -72,7 +74,8 @@ impl DockerNode { pub async fn get_or_create<'a>( root: impl AsRef, docker: Docker, - dropper: DropHandle, + dropper: DropChute, + drop_policy: &DropPolicy, node_name: &NodeName, node_config: NodeConfig<'a, DockerMaterials>, port_range: DockerPortRange, @@ -81,12 +84,29 @@ impl DockerNode { let cometbft_name = container_name(node_name, "cometbft"); let ethapi_name = container_name(node_name, "ethapi"); - let fendermint = - DockerContainer::get(docker.clone(), dropper.clone(), fendermint_name.clone()).await?; - let cometbft = - DockerContainer::get(docker.clone(), dropper.clone(), cometbft_name.clone()).await?; - let ethapi = - DockerContainer::get(docker.clone(), dropper.clone(), ethapi_name.clone()).await?; + let fendermint = DockerContainer::get( + docker.clone(), + dropper.clone(), + drop_policy, + fendermint_name.clone(), + ) + .await?; + + let cometbft = DockerContainer::get( + docker.clone(), + dropper.clone(), + drop_policy, + cometbft_name.clone(), + ) + .await?; + + let ethapi = DockerContainer::get( + docker.clone(), + dropper.clone(), + drop_policy, + ethapi_name.clone(), + ) + .await?; // Directory for the node's data volumes let node_dir = root.as_ref().join(node_name); @@ -99,6 +119,7 @@ impl DockerNode { DockerRunner::new( docker.clone(), dropper.clone(), + drop_policy.clone(), node_name.clone(), user, image, @@ -554,7 +575,10 @@ fn parse_fendermint_peer_id(value: impl AsRef) -> anyhow::Result { mod tests { use super::{DockerRunner, COMETBFT_IMAGE}; use crate::{ - docker::{dropper, node::parse_cometbft_node_id}, + docker::{ + dropper::{self, DropPolicy}, + node::parse_cometbft_node_id, + }, TestnetName, }; use bollard::Docker; @@ -562,8 +586,18 @@ mod tests { fn make_runner() -> DockerRunner { let nn = TestnetName::new("test-network").root().node("test-node"); let docker = Docker::connect_with_local_defaults().expect("failed to connect to docker"); - let dropper = dropper::start(docker.clone()); - DockerRunner::new(docker, dropper, nn, 0, COMETBFT_IMAGE, Vec::new()) + let (_drop_handle, drop_chute) = dropper::start(docker.clone()); + let drop_policy = DropPolicy::EPHEMERAL; + + DockerRunner::new( + docker, + drop_chute, + drop_policy, + nn, + 0, + COMETBFT_IMAGE, + Vec::new(), + ) } #[tokio::test] diff --git a/fendermint/testing/materializer/src/docker/runner.rs b/fendermint/testing/materializer/src/docker/runner.rs index 40052337d..469aec617 100644 --- a/fendermint/testing/materializer/src/docker/runner.rs +++ b/fendermint/testing/materializer/src/docker/runner.rs @@ -18,12 +18,15 @@ use futures::StreamExt; use crate::NodeName; use super::{ - container::DockerContainer, dropper::DropHandle, DockerConstruct, DockerNetwork, Volumes, + container::DockerContainer, + dropper::{DropChute, DropPolicy}, + DockerConstruct, DockerNetwork, Volumes, }; pub struct DockerRunner { docker: Docker, - dropper: DropHandle, + dropper: DropChute, + drop_policy: DropPolicy, node_name: NodeName, user: u32, image: String, @@ -33,7 +36,8 @@ pub struct DockerRunner { impl DockerRunner { pub fn new( docker: Docker, - dropper: DropHandle, + dropper: DropChute, + drop_policy: DropPolicy, node_name: NodeName, user: u32, image: &str, @@ -42,6 +46,7 @@ impl DockerRunner { Self { docker, dropper, + drop_policy, node_name, user, image: image.to_string(), @@ -234,7 +239,7 @@ impl DockerRunner { DockerConstruct { id, name, - external: false, + keep: self.drop_policy.keep(true), }, )) } diff --git a/fendermint/testing/materializer/src/logging.rs b/fendermint/testing/materializer/src/logging.rs index e47b265bd..2bb925f31 100644 --- a/fendermint/testing/materializer/src/logging.rs +++ b/fendermint/testing/materializer/src/logging.rs @@ -40,13 +40,11 @@ where M::Relayer: Display, { async fn create_network(&mut self, testnet_name: &TestnetName) -> anyhow::Result { - eprintln!("create_network({testnet_name:?}"); tracing::info!(self.tag, %testnet_name, "create_network"); self.inner.create_network(testnet_name).await } fn create_account(&mut self, account_name: &AccountName) -> anyhow::Result { - eprintln!("create_account({account_name})"); tracing::info!(self.tag, %account_name, "create_account"); self.inner.create_account(account_name) } @@ -59,7 +57,6 @@ where where 's: 'a, { - eprintln!("fund_from_faucet({account})"); tracing::info!(self.tag, %account, "fund_from_faucet"); self.inner.fund_from_faucet(account, reference).await } @@ -73,7 +70,6 @@ where where 's: 'a, { - eprintln!("new_deployment({subnet_name}, {deployer})"); tracing::info!(self.tag, %subnet_name, %deployer, "new_deployment"); self.inner.new_deployment(subnet_name, deployer, urls).await } @@ -84,14 +80,12 @@ where gateway: H160, registry: H160, ) -> anyhow::Result { - eprintln!("existing_deployment({subnet_name})"); tracing::info!(self.tag, %subnet_name, "existing_deployment"); self.inner .existing_deployment(subnet_name, gateway, registry) } fn default_deployment(&mut self, subnet_name: &SubnetName) -> anyhow::Result { - eprintln!("default_deployment({subnet_name})"); tracing::info!(self.tag, %subnet_name, "default_deployment"); self.inner.default_deployment(subnet_name) } @@ -102,7 +96,6 @@ where validators: BTreeMap<&'a M::Account, Collateral>, balances: BTreeMap<&'a M::Account, Balance>, ) -> anyhow::Result { - eprintln!("create_root_genesis({subnet_name})"); tracing::info!(self.tag, %subnet_name, "create_root_genesis"); self.inner .create_root_genesis(subnet_name, validators, balances) @@ -116,7 +109,6 @@ where where 's: 'a, { - eprintln!("create_node({node_name})"); tracing::info!(self.tag, %node_name, "create_node"); self.inner.create_node(node_name, node_config).await } @@ -129,7 +121,6 @@ where where 's: 'a, { - eprintln!("start_node({node}"); tracing::info!(self.tag, %node, "start_node"); self.inner.start_node(node, seed_nodes).await } @@ -143,7 +134,6 @@ where where 's: 'a, { - eprintln!("create_subnet({subnet_name})"); tracing::info!(self.tag, %subnet_name, "create_subnet"); self.inner .create_subnet(parent_submit_config, subnet_name, subnet_config) @@ -161,7 +151,6 @@ where where 's: 'a, { - eprintln!("fund_subnet({subnet}, {account}, {amount})"); tracing::info!(self.tag, %subnet, %account, "fund_subnet"); self.inner .fund_subnet(parent_submit_config, account, subnet, amount, reference) @@ -180,10 +169,6 @@ where where 's: 'a, { - eprintln!( - "join_subnet({subnet}, {account}, {}, {})", - collateral.0, balance.0 - ); tracing::info!(self.tag, %subnet, %account, "join_subnet"); self.inner .join_subnet( @@ -205,7 +190,6 @@ where where 's: 'a, { - eprintln!("create_subnet_genesis({subnet})"); tracing::info!(self.tag, %subnet, "create_subnet_genesis"); self.inner .create_subnet_genesis(parent_submit_config, subnet) @@ -223,7 +207,6 @@ where where 's: 'a, { - eprintln!("create_relayer({relayer_name})"); tracing::info!(self.tag, %relayer_name, "create_relayer"); self.inner .create_relayer( diff --git a/fendermint/testing/materializer/src/manifest.rs b/fendermint/testing/materializer/src/manifest.rs index e74863a31..9474f44d5 100644 --- a/fendermint/testing/materializer/src/manifest.rs +++ b/fendermint/testing/materializer/src/manifest.rs @@ -3,16 +3,17 @@ // See https://github.com/cometbft/cometbft/blob/v0.38.5/test/e2e/pkg/manifest.go for inspiration. +use anyhow::{bail, Context}; use fvm_shared::econ::TokenAmount; use serde::{Deserialize, Serialize}; use serde_with::serde_as; -use std::collections::BTreeMap; +use std::{collections::BTreeMap, path::Path}; use tendermint_rpc::Url; use fendermint_vm_encoding::IsHumanReadable; use fendermint_vm_genesis::Collateral; -use crate::{AccountId, NodeId, RelayerId, SubnetId}; +use crate::{validation::validate_manifest, AccountId, NodeId, RelayerId, SubnetId, TestnetName}; pub type SubnetMap = BTreeMap; pub type BalanceMap = BTreeMap; @@ -43,6 +44,33 @@ pub struct Manifest { pub subnets: SubnetMap, } +impl Manifest { + /// Read a manifest from file. It chooses the format based on the extension. + pub fn from_file(path: &Path) -> anyhow::Result { + let Some(ext) = path + .extension() + .map(|e| e.to_string_lossy().to_ascii_lowercase()) + else { + bail!("manifest file has no extension, cannot determine format"); + }; + + let manifest = std::fs::read_to_string(path) + .with_context(|| format!("failed to read manifest from {}", path.to_string_lossy()))?; + + match ext.as_str() { + "yaml" => serde_yaml::from_str(&manifest).context("failed to parse manifest YAML"), + "json" => serde_json::from_str(&manifest).context("failed to parse manifest JSON"), + "toml" => toml::from_str(&manifest).context("failed to parse manifest TOML"), + other => bail!("unknown manifest format: {other}"), + } + } + + /// Perform sanity checks. + pub async fn validate(&self, name: &TestnetName) -> anyhow::Result<()> { + validate_manifest(name, self).await + } +} + /// Any potential attributes of an account. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct Account {} diff --git a/fendermint/testing/materializer/tests/docker.rs b/fendermint/testing/materializer/tests/docker.rs index 0bbac4c9a..405b223cf 100644 --- a/fendermint/testing/materializer/tests/docker.rs +++ b/fendermint/testing/materializer/tests/docker.rs @@ -20,7 +20,6 @@ use fendermint_materializer::{ docker::{DockerMaterializer, DockerMaterials}, manifest::Manifest, testnet::Testnet, - validation::validate_manifest, HasCometBftApi, HasEthApi, TestnetName, }; use futures::Future; @@ -32,7 +31,7 @@ pub type DockerTestnet = Testnet; lazy_static! { static ref CI_PROFILE: bool = std::env::var("PROFILE").unwrap_or_default() == "ci"; static ref STARTUP_TIMEOUT: Duration = Duration::from_secs(60); - static ref TEARDOWN_TIMEOUT: Duration = Duration::from_secs(5); + static ref TEARDOWN_TIMEOUT: Duration = Duration::from_secs(30); static ref PRINT_LOGS_ON_ERROR: bool = *CI_PROFILE; } @@ -54,13 +53,7 @@ fn test_data_dir() -> PathBuf { /// Parse a manifest from the `tests/manifests` directory. fn read_manifest(file_name: &str) -> anyhow::Result { let manifest = tests_dir().join("manifests").join(file_name); - let manifest = std::fs::read_to_string(&manifest).with_context(|| { - format!( - "failed to read manifest from {}", - manifest.to_string_lossy() - ) - })?; - let manifest = serde_yaml::from_str(&manifest).context("failed to parse manifest")?; + let manifest = Manifest::from_file(&manifest)?; Ok(manifest) } @@ -86,7 +79,8 @@ where let manifest = read_manifest(manifest_file_name)?; // First make sure it's a sound manifest. - validate_manifest(&testnet_name, &manifest) + manifest + .validate(&testnet_name) .await .context("failed to validate manifest")?; @@ -136,7 +130,8 @@ where // otherwise the system shuts down too quick, but // at least we can inspect the containers. // If they don't all get dropped, `docker system prune` helps. - tokio::time::sleep(*TEARDOWN_TIMEOUT).await; + let drop_handle = materializer.take_dropper(); + let _ = tokio::time::timeout(*TEARDOWN_TIMEOUT, drop_handle).await; res } diff --git a/fendermint/testing/materializer/tests/docker_tests/root_only.rs b/fendermint/testing/materializer/tests/docker_tests/root_only.rs index d74892a75..45f4401c3 100644 --- a/fendermint/testing/materializer/tests/docker_tests/root_only.rs +++ b/fendermint/testing/materializer/tests/docker_tests/root_only.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT use anyhow::{anyhow, bail}; @@ -14,6 +16,8 @@ const MANIFEST: &str = "root-only.yaml"; async fn test_full_node_sync() { with_testnet(MANIFEST, |_materializer, _manifest, testnet| { let test = async { + // Allow a little bit of time for node-2 to catch up with node-1. + tokio::time::sleep(Duration::from_secs(1)).await; // Check that node2 is following node1. let node2 = testnet.root().node("node-2"); let dnode2 = testnet.node(&node2)?; @@ -25,7 +29,7 @@ async fn test_full_node_sync() { let bn = provider.get_block_number().await?; if bn <= U64::one() { - bail!("expected positive block number"); + bail!("expected a block beyond genesis"); } Ok(()) From d801b4485ffe7f59d895ba0766341789292685f7 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Mon, 4 Mar 2024 12:52:18 +0000 Subject: [PATCH 03/12] ENG-770: Accept log filter expressions (#763) --- Cargo.lock | 3 + Cargo.toml | 2 +- fendermint/.gitignore | 1 + fendermint/app/options/Cargo.toml | 3 + fendermint/app/options/src/lib.rs | 98 +++++++++++++++++++++++-------- fendermint/app/options/src/log.rs | 81 +++++++++++++++++++++++++ fendermint/app/src/main.rs | 86 +++++++++++++-------------- 7 files changed, 203 insertions(+), 71 deletions(-) create mode 100644 fendermint/app/options/src/log.rs diff --git a/Cargo.lock b/Cargo.lock index 396991dbf..c1d6af6bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2905,6 +2905,7 @@ dependencies = [ name = "fendermint_app_options" version = "0.1.0" dependencies = [ + "anyhow", "bytes", "cid", "clap 4.5.1", @@ -2916,9 +2917,11 @@ dependencies = [ "hex", "ipc-api", "ipc-types", + "lazy_static", "num-traits", "tendermint-rpc", "tracing", + "tracing-subscriber", "url", ] diff --git a/Cargo.toml b/Cargo.toml index 7903a1b71..b02c394a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,7 +55,7 @@ bollard = "0.15" blake2b_simd = "1.0" bloom = "0.3" bytes = "1.4" -clap = { version = "4.1", features = ["derive", "env"] } +clap = { version = "4.1", features = ["derive", "env", "string"] } config = "0.13" dirs = "5.0" dircpy = "0.3" diff --git a/fendermint/.gitignore b/fendermint/.gitignore index 51b2a211f..a036c683f 100644 --- a/fendermint/.gitignore +++ b/fendermint/.gitignore @@ -8,3 +8,4 @@ testing/materializer/tests/docker-materializer-data .idea .make .contracts-gen +.vscode diff --git a/fendermint/app/options/Cargo.toml b/fendermint/app/options/Cargo.toml index b939777d5..9aaf29ce7 100644 --- a/fendermint/app/options/Cargo.toml +++ b/fendermint/app/options/Cargo.toml @@ -10,12 +10,15 @@ license.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +anyhow = { workspace = true } bytes = { workspace = true } clap = { workspace = true } hex = { workspace = true } +lazy_static = { workspace = true } num-traits = { workspace = true } tendermint-rpc = { workspace = true } tracing = { workspace = true } +tracing-subscriber = { workspace = true } cid = { workspace = true } fvm_ipld_encoding = { workspace = true } diff --git a/fendermint/app/options/src/lib.rs b/fendermint/app/options/src/lib.rs index f2a1ee787..793f6330e 100644 --- a/fendermint/app/options/src/lib.rs +++ b/fendermint/app/options/src/lib.rs @@ -3,11 +3,14 @@ use std::path::PathBuf; -use clap::{Args, Parser, Subcommand, ValueEnum}; +use clap::{Args, Parser, Subcommand}; use fvm_shared::address::Network; -use materializer::MaterializerArgs; +use tracing_subscriber::EnvFilter; -use self::{eth::EthArgs, genesis::GenesisArgs, key::KeyArgs, rpc::RpcArgs, run::RunArgs}; +use self::{ + eth::EthArgs, genesis::GenesisArgs, key::KeyArgs, materializer::MaterializerArgs, rpc::RpcArgs, + run::RunArgs, +}; pub mod eth; pub mod genesis; @@ -16,8 +19,10 @@ pub mod materializer; pub mod rpc; pub mod run; +mod log; mod parse; +use log::{parse_log_level, LogLevel}; use parse::parse_network; /// Parse the main arguments by: @@ -31,16 +36,6 @@ pub fn parse() -> Options { opts } -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] -pub enum LogLevel { - Off, - Error, - Warn, - Info, - Debug, - Trace, -} - #[derive(Args, Debug)] pub struct GlobalArgs { /// Set the FVM Address Network. It's value affects whether `f` (main) or `t` (test) prefixed addresses are accepted. @@ -89,15 +84,30 @@ pub struct Options { #[arg(short, long, default_value = "dev")] pub mode: String, - /// Set the logging level. + /// Set the logging level of the console. #[arg( short = 'l', long, default_value = "info", value_enum, - env = "LOG_LEVEL" + env = "FM_LOG_LEVEL", + help = "Standard log levels, or a comma separated list of filters, e.g. 'debug,tower_abci=warn,libp2p::gossipsub=info'", + value_parser = parse_log_level, + )] + log_level: LogLevel, + + /// Fallback for the `log_level` with a legacy env var name. + #[arg(hide = true, value_enum, env = "LOG_LEVEL", value_parser = parse_log_level,)] + _log_level: Option, + + /// Set the logging level of the log file. If missing, it defaults to the same level as the console. + #[arg( + long, + value_enum, + env = "FM_LOG_FILE_LEVEL", + value_parser = parse_log_level, )] - pub log_level: LogLevel, + log_file_level: Option, /// Global options repeated here for discoverability, so they show up in `--help` among the others. #[command(flatten)] @@ -108,15 +118,24 @@ pub struct Options { } impl Options { - /// Tracing level, unless it's turned off. - pub fn tracing_level(&self) -> Option { - match self.log_level { - LogLevel::Off => None, - LogLevel::Error => Some(tracing::Level::ERROR), - LogLevel::Warn => Some(tracing::Level::WARN), - LogLevel::Info => Some(tracing::Level::INFO), - LogLevel::Debug => Some(tracing::Level::DEBUG), - LogLevel::Trace => Some(tracing::Level::TRACE), + /// Tracing filter for the console. + /// + /// Coalescing everything into a filter instead of either a level or a filter + /// because the `tracing_subscriber` setup methods like `with_filter` and `with_level` + /// produce different static types and it's not obvious how to use them as alternatives. + pub fn log_console_filter(&self) -> anyhow::Result { + self._log_level + .as_ref() + .unwrap_or(&self.log_level) + .to_filter() + } + + /// Tracing filter for the log file. + pub fn log_file_filter(&self) -> anyhow::Result { + if let Some(ref level) = self.log_file_level { + level.to_filter() + } else { + self.log_console_filter() } } @@ -148,6 +167,7 @@ mod tests { use crate::*; use clap::Parser; use fvm_shared::address::Network; + use tracing::level_filters::LevelFilter; #[test] fn parse_global() { @@ -174,4 +194,32 @@ mod tests { assert!(e.to_string().contains("Usage:"), "unexpected help: {e}"); } + + #[test] + fn parse_log_level() { + let parse_filter = |cmd: &str| { + let opts: Options = Options::parse_from(cmd.split_ascii_whitespace()); + opts.log_console_filter().expect("filter should parse") + }; + + let assert_level = |cmd: &str, level: LevelFilter| { + let filter = parse_filter(cmd); + assert_eq!(filter.max_level_hint(), Some(level)) + }; + + assert_level("fendermint --log-level debug run", LevelFilter::DEBUG); + assert_level("fendermint --log-level off run", LevelFilter::OFF); + assert_level( + "fendermint --log-level libp2p=warn,error run", + LevelFilter::WARN, + ); + assert_level("fendermint --log-level info run", LevelFilter::INFO); + } + + #[test] + fn parse_invalid_log_level() { + // NOTE: `nonsense` in itself is interpreted as a target. Maybe we should mandate at least `=` in it? + let cmd = "fendermint --log-level nonsense/123 run"; + Options::try_parse_from(cmd.split_ascii_whitespace()).expect_err("should not parse"); + } } diff --git a/fendermint/app/options/src/log.rs b/fendermint/app/options/src/log.rs new file mode 100644 index 000000000..e10415cb8 --- /dev/null +++ b/fendermint/app/options/src/log.rs @@ -0,0 +1,81 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use clap::{builder::PossibleValue, ValueEnum}; + +use lazy_static::lazy_static; +use tracing_subscriber::EnvFilter; + +/// Standard log levels, or something we can pass to +/// +/// To be fair all of these could be handled by the `EnvFilter`, even `off`, +/// however I also wanted to leave it here as an example of implementing `ValueEnum` manually, +/// and perhaps we have simpler usecases where we only want to simply match levels. +#[derive(Debug, Clone)] +pub enum LogLevel { + Off, + Error, + Warn, + Info, + Debug, + Trace, + Filter(String), +} + +impl LogLevel { + pub fn as_str(&self) -> &str { + match self { + LogLevel::Off => "off", + LogLevel::Error => "error", + LogLevel::Warn => "warn", + LogLevel::Info => "info", + LogLevel::Debug => "debug", + LogLevel::Trace => "trace", + LogLevel::Filter(s) => s.as_str(), + } + } + + pub fn to_filter(&self) -> anyhow::Result { + // At this point the filter should have been parsed before, + // but if we created a log level directly, it can fail. + // We fail if it doesn't parse because presumably we _want_ to see those things. + Ok(EnvFilter::try_new(self.as_str())?) + } +} + +impl ValueEnum for LogLevel { + fn value_variants<'a>() -> &'a [Self] { + lazy_static! { + static ref VARIANTS: Vec = vec![ + LogLevel::Off, + LogLevel::Error, + LogLevel::Warn, + LogLevel::Info, + LogLevel::Debug, + LogLevel::Trace, + ]; + } + + &VARIANTS + } + + fn to_possible_value(&self) -> Option { + if let LogLevel::Filter(_) = self { + None + } else { + Some(PossibleValue::new(self.as_str().to_string())) + } + } +} + +pub fn parse_log_level(s: &str) -> Result { + if let Ok(lvl) = ValueEnum::from_str(s, true) { + return Ok(lvl); + } + // `EnvFilter` is not `Clone`, so we can't store it, but we can use it to validate early. + if let Err(e) = EnvFilter::try_new(s) { + Err(e.to_string()) + } else { + Ok(LogLevel::Filter(s.to_string())) + } +} diff --git a/fendermint/app/src/main.rs b/fendermint/app/src/main.rs index 3a1e45d9f..a27793f2e 100644 --- a/fendermint/app/src/main.rs +++ b/fendermint/app/src/main.rs @@ -8,65 +8,61 @@ use tracing_appender::{ rolling::{RollingFileAppender, Rotation}, }; use tracing_subscriber::fmt::format::FmtSpan; -use tracing_subscriber::{ - fmt::{self, writer::MakeWriterExt}, - layer::SubscriberExt, -}; +use tracing_subscriber::{fmt, layer::SubscriberExt, Layer}; mod cmd; fn init_tracing(opts: &options::Options) -> Option { - let mut guard = None; - - let Some(log_level) = opts.tracing_level() else { - return guard; - }; + let console_filter = opts.log_console_filter().expect("invalid filter"); + let file_filter = opts.log_file_filter().expect("invalid filter"); - let registry = tracing_subscriber::registry(); + // log all traces to stderr (reserving stdout for any actual output such as from the CLI commands) + let console_layer = tracing_subscriber::fmt::layer() + .with_writer(std::io::stderr) + .with_target(false) + .with_file(true) + .with_line_number(true) + .with_filter(console_filter); // add a file layer if log_dir is set - let registry = registry.with(if let Some(log_dir) = &opts.log_dir { - let filename = match &opts.log_file_prefix { - Some(prefix) => format!("{}-{}", prefix, "fendermint"), - None => "fendermint".to_string(), - }; - - let appender = RollingFileAppender::builder() - .filename_prefix(filename) - .filename_suffix("log") - .rotation(Rotation::DAILY) - .max_log_files(5) - .build(log_dir) - .expect("failed to initialize rolling file appender"); - - let (non_blocking, g) = tracing_appender::non_blocking(appender); - guard = Some(g); - - Some( - fmt::Layer::new() + let (file_layer, file_guard) = match &opts.log_dir { + Some(log_dir) => { + let filename = match &opts.log_file_prefix { + Some(prefix) => format!("{}-{}", prefix, "fendermint"), + None => "fendermint".to_string(), + }; + + let appender = RollingFileAppender::builder() + .filename_prefix(filename) + .filename_suffix("log") + .rotation(Rotation::DAILY) + .max_log_files(7) + .build(log_dir) + .expect("failed to initialize rolling file appender"); + + let (non_blocking, file_guard) = tracing_appender::non_blocking(appender); + + let file_layer = fmt::Layer::new() .json() - .with_writer(non_blocking.with_max_level(log_level)) + .with_writer(non_blocking) .with_span_events(FmtSpan::CLOSE) .with_target(false) .with_file(true) - .with_line_number(true), - ) - } else { - None - }); - - // we also log all traces with level INFO or higher to stdout - let registry = registry.with( - tracing_subscriber::fmt::layer() - .with_writer(std::io::stdout.with_max_level(tracing::Level::INFO)) - .with_target(false) - .with_file(true) - .with_line_number(true), - ); + .with_line_number(true) + .with_filter(file_filter); + + (Some(file_layer), Some(file_guard)) + } + None => (None, None), + }; + + let registry = tracing_subscriber::registry() + .with(console_layer) + .with(file_layer); tracing::subscriber::set_global_default(registry).expect("Unable to set a global collector"); - guard + file_guard } /// Install a panic handler that prints stuff to the logs, otherwise it only shows up in the console. From b718e8fc86ef91f9e4c2226a145aeca4d0b139ea Mon Sep 17 00:00:00 2001 From: Jie Hou <143860049+mb1896@users.noreply.github.com> Date: Tue, 5 Mar 2024 05:37:01 +0800 Subject: [PATCH 04/12] Use Personal Access Token in auto contract deployment workflow (#770) --- .github/workflows/auto-deploy-contracts.yaml | 6 ++++-- fendermint/docker/.Dockerfile.swp | Bin 12288 -> 0 bytes 2 files changed, 4 insertions(+), 2 deletions(-) delete mode 100644 fendermint/docker/.Dockerfile.swp diff --git a/.github/workflows/auto-deploy-contracts.yaml b/.github/workflows/auto-deploy-contracts.yaml index 9bed6d117..efd39804a 100644 --- a/.github/workflows/auto-deploy-contracts.yaml +++ b/.github/workflows/auto-deploy-contracts.yaml @@ -24,12 +24,13 @@ jobs: RPC_URL: https://calibration.filfox.info/rpc/v1 PRIVATE_KEY: ${{ secrets.CONTRACTS_DEPLOYER_PRIVATE_KEY }} steps: - - name: Checkout cd/contract branch + - name: Checkout cd/contracts branch uses: actions/checkout@v4 with: ref: cd/contracts submodules: recursive fetch-depth: 0 + token: ${{ secrets.WORKFLOW_PAT_JIE }} - name: (Dry run) Try merge from main branch to see if there's any conflicts that can't be resolved itself run: | @@ -86,12 +87,13 @@ jobs: echo "gateway_address: $gateway_address" echo "registry_address: $registry_address" - - name: Switch code repo to cd/contract branch + - name: Switch code repo to cd/contracts branch uses: actions/checkout@v4 with: ref: cd/contracts submodules: recursive fetch-depth: 0 + token: ${{ secrets.WORKFLOW_PAT_JIE }} - name: Merge from main branch and update cd/contracts branch run: | diff --git a/fendermint/docker/.Dockerfile.swp b/fendermint/docker/.Dockerfile.swp deleted file mode 100644 index 06acd221c3afb14eb358f2b70e3d0cecbce77648..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI2O>Y}T7{{j|p;9OyID;ONs=UZ+rwt^KVx+`z8$%siwo`}zS>xTYz4h+Sc4n5; z2jGPI6}WKV2;U%F;L3?3J@fKC_|Mv|O(K_6Jyn`b{+iu+d1mJKJkQ!urnh#hR;Txi zYYf**jD1)CLu5zySm|5FZiiwhGu!a+>!VO;ug?7)DMwyY$zmjZ9-SWZ^~7K)O#M<{ zB!N(In3z%^{gF_m-Mr|BkvKil)i?rhfo<>g;t;o(EaBy7jz7L58Z8RuVh^AWnqMaj8x)jT9TPbGgIn?$-?SNuds&@G$=KE{x(|ic8Qy_XFPqunE;(H`j?CB_s@$H>%!|Bu$eW?VQp`bh;if9Ov`7`n(5GhXl zhRJ|rLPtY^zKV_nag58yOq(h-l8W+diN*yq8Ea;o_d_Jx^(Ih&89KA+V2LLI!4HYO zg+)xRMaq@k#Qa<_X$fQ8*m-@PP^JY1VZSewNDONHpPDR;HE(!+r zgehODY`1PS@>DP6FBt{M1uZk>i^WDb?V)B`2kp*5bD5om)~WZGuV}I`bOR7ca5ni$ zYCZ9s2Gmd!ep4lTycdlr!S*=~B$71@1h?_f=rr~yH1LavH6n2&5D9|Ph#T08C#G_@ z^biqKl17cKON=D$-^oIFmfP4E*&uolZSbKI_SG~lV4k{#n)@*oBG8njlW3Z*brMoo zM<9hx-Y{fSB&Q!BjLne{$!u4cqf#p(%kAsFNVuD&Ztg*_f8+$rBi$2PBj7?Fksonv p&yP810G^S(tnc|UP7z`1ENN*Y<3UmqpL#?nfY_OQGW|~w{|ir+hCKiP From c3e4c020d52decfbc57eb6e2618c947f3f7b29a7 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Tue, 5 Mar 2024 10:21:13 +0000 Subject: [PATCH 05/12] ENG-578: IT (Part 7) - Import secret key into testnet (#768) --- fendermint/app/options/src/materializer.rs | 21 +++++- fendermint/app/src/cmd/key.rs | 8 +-- fendermint/app/src/cmd/materializer.rs | 39 ++++++++++- .../testing/materializer/src/docker/mod.rs | 2 +- .../materializer/src/materials/defaults.rs | 64 +++++++++++++------ 5 files changed, 107 insertions(+), 27 deletions(-) diff --git a/fendermint/app/options/src/materializer.rs b/fendermint/app/options/src/materializer.rs index d0f5c1e51..2916fe409 100644 --- a/fendermint/app/options/src/materializer.rs +++ b/fendermint/app/options/src/materializer.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; use clap::{Args, Subcommand}; -use fendermint_materializer::TestnetId; +use fendermint_materializer::{AccountId, TestnetId}; #[derive(Args, Debug)] pub struct MaterializerArgs { @@ -35,6 +35,8 @@ pub enum MaterializerCommands { Setup(MaterializerSetupArgs), /// Tear down a testnet. Remove(MaterializerRemoveArgs), + /// Import an existing secret key into a testnet; for example to use an already funded account on Calibration net. + ImportKey(MaterializerImportKeyArgs), } #[derive(Args, Debug)] @@ -67,3 +69,20 @@ pub struct MaterializerRemoveArgs { #[arg(long, short)] pub testnet_id: TestnetId, } + +#[derive(Args, Debug)] +pub struct MaterializerImportKeyArgs { + /// Path to the manifest file. + /// + /// This is used to determine the testnet ID as well as to check that the account exists. + #[arg(long, short)] + pub manifest_file: PathBuf, + + /// Path to the Secp256k1 private key exported in base64 or hexadecimal format. + #[arg(long, short)] + pub secret_key: PathBuf, + + /// Run validation before attempting to set up the testnet. + #[arg(long, short)] + pub account_id: AccountId, +} diff --git a/fendermint/app/src/cmd/key.rs b/fendermint/app/src/cmd/key.rs index 6d818930e..e5891ae0f 100644 --- a/fendermint/app/src/cmd/key.rs +++ b/fendermint/app/src/cmd/key.rs @@ -8,7 +8,7 @@ use fendermint_vm_actor_interface::eam::EthAddress; use fvm_shared::address::Address; use rand_chacha::{rand_core::SeedableRng, ChaCha20Rng}; use serde_json::json; -use std::path::{Path, PathBuf}; +use std::path::Path; use tendermint_config::NodeKey; use crate::{ @@ -159,13 +159,13 @@ fn b64_to_secret(b64: &str) -> anyhow::Result { Ok(sk) } -pub fn read_public_key(public_key: &PathBuf) -> anyhow::Result { +pub fn read_public_key(public_key: &Path) -> anyhow::Result { let b64 = std::fs::read_to_string(public_key).context("failed to read public key")?; let pk = b64_to_public(&b64).context("failed to parse public key")?; Ok(pk) } -pub fn read_secret_key_hex(private_key: &PathBuf) -> anyhow::Result { +pub fn read_secret_key_hex(private_key: &Path) -> anyhow::Result { let hex_str = std::fs::read_to_string(private_key).context("failed to read private key")?; let mut hex_str = hex_str.trim(); if hex_str.starts_with("0x") { @@ -176,7 +176,7 @@ pub fn read_secret_key_hex(private_key: &PathBuf) -> anyhow::Result { Ok(sk) } -pub fn read_secret_key(secret_key: &PathBuf) -> anyhow::Result { +pub fn read_secret_key(secret_key: &Path) -> anyhow::Result { let b64 = std::fs::read_to_string(secret_key).context("failed to read secret key")?; let sk = b64_to_secret(&b64).context("failed to parse secret key")?; Ok(sk) diff --git a/fendermint/app/src/cmd/materializer.rs b/fendermint/app/src/cmd/materializer.rs index 400c3ec46..dc24d9617 100644 --- a/fendermint/app/src/cmd/materializer.rs +++ b/fendermint/app/src/cmd/materializer.rs @@ -1,20 +1,23 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -use std::path::Path; +use std::path::{Path, PathBuf}; -use anyhow::anyhow; +use anyhow::{anyhow, bail}; use fendermint_app_options::materializer::*; use fendermint_app_settings::utils::expand_tilde; use fendermint_materializer::{ docker::{DockerMaterializer, DropPolicy}, manifest::Manifest, + materials::DefaultAccount, testnet::Testnet, - TestnetId, TestnetName, + AccountId, TestnetId, TestnetName, }; use crate::cmd; +use super::key::{read_secret_key, read_secret_key_hex}; + cmd! { MaterializerArgs(self) { let d = expand_tilde(&self.data_dir); @@ -23,6 +26,7 @@ cmd! { MaterializerCommands::Validate(args) => args.exec(()).await, MaterializerCommands::Setup(args) => args.exec(m()?).await, MaterializerCommands::Remove(args) => args.exec(m()?).await, + MaterializerCommands::ImportKey(args) => args.exec(d).await, } } } @@ -45,6 +49,12 @@ cmd! { } } +cmd! { + MaterializerImportKeyArgs(self, data_dir: PathBuf) { + import_key(&data_dir, &self.secret_key, &self.manifest_file, &self.account_id) + } +} + /// Validate a manifest. async fn validate(manifest_file: &Path) -> anyhow::Result<()> { let (name, manifest) = read_manifest(manifest_file)?; @@ -87,3 +97,26 @@ fn read_manifest(manifest_file: &Path) -> anyhow::Result<(TestnetName, Manifest) Ok((name, manifest)) } + +/// Import a secret key as one of the accounts in a manifest. +fn import_key( + data_dir: &Path, + secret_key: &Path, + manifest_file: &Path, + account_id: &AccountId, +) -> anyhow::Result<()> { + let (testnet_name, manifest) = read_manifest(manifest_file)?; + + if !manifest.accounts.contains_key(account_id) { + bail!( + "account {account_id} cannot be found in the manifest at {}", + manifest_file.to_string_lossy() + ); + } + + let sk = read_secret_key(secret_key).or_else(|_| read_secret_key_hex(secret_key))?; + + let _acc = DefaultAccount::create(data_dir, &testnet_name.account(account_id), sk)?; + + Ok(()) +} diff --git a/fendermint/testing/materializer/src/docker/mod.rs b/fendermint/testing/materializer/src/docker/mod.rs index eee26e896..c39ad343b 100644 --- a/fendermint/testing/materializer/src/docker/mod.rs +++ b/fendermint/testing/materializer/src/docker/mod.rs @@ -429,7 +429,7 @@ impl Materializer for DockerMaterializer { validators: validators .into_iter() .map(|(v, c)| Validator { - public_key: ValidatorKey(v.public_key), + public_key: ValidatorKey(*v.public_key()), power: c, }) .collect(), diff --git a/fendermint/testing/materializer/src/materials/defaults.rs b/fendermint/testing/materializer/src/materials/defaults.rs index 5f118b477..515476013 100644 --- a/fendermint/testing/materializer/src/materials/defaults.rs +++ b/fendermint/testing/materializer/src/materials/defaults.rs @@ -50,11 +50,11 @@ pub struct DefaultSubnet { #[derive(PartialEq, Eq)] pub struct DefaultAccount { - pub name: AccountName, - pub secret_key: SecretKey, - pub public_key: PublicKey, + name: AccountName, + secret_key: SecretKey, + public_key: PublicKey, /// Path to the directory where the keys are exported. - pub path: PathBuf, + path: PathBuf, } impl PartialOrd for DefaultAccount { @@ -96,18 +96,17 @@ impl DefaultAccount { let dir = root.as_ref().join(name.path()); let sk = dir.join("secret.hex"); - let (sk, pk, is_new) = if sk.exists() { + let (sk, is_new) = if sk.exists() { let sk = std::fs::read_to_string(sk).context("failed to read private key")?; let sk = hex::decode(sk).context("cannot decode hex private key")?; let sk = SecretKey::try_from(sk).context("failed to parse secret key")?; - let pk = sk.public_key(); - (sk, pk, false) + (sk, false) } else { let sk = SecretKey::random(rng); - let pk = sk.public_key(); - (sk, pk, true) + (sk, true) }; + let pk = sk.public_key(); let acc = Self { name: name.clone(), secret_key: sk, @@ -116,23 +115,52 @@ impl DefaultAccount { }; if is_new { - let sk = acc.secret_key.serialize(); - let pk = acc.public_key.serialize(); - - export(&acc.path, "secret", "b64", to_b64(sk.as_ref()))?; - export(&acc.path, "secret", "hex", hex::encode(sk))?; - export(&acc.path, "public", "b64", to_b64(pk.as_ref()))?; - export(&acc.path, "public", "hex", hex::encode(pk))?; - export(&acc.path, "eth-addr", "", format!("{:?}", acc.eth_addr()))?; - export(&acc.path, "fvm-addr", "", acc.fvm_addr().to_string())?; + acc.export()?; } Ok(acc) } + /// Create (or overwrite) an account with a given secret key. + pub fn create( + root: impl AsRef, + name: &AccountName, + sk: SecretKey, + ) -> anyhow::Result { + let pk = sk.public_key(); + let dir = root.as_ref().join(name.path()); + let acc = Self { + name: name.clone(), + secret_key: sk, + public_key: pk, + path: dir, + }; + acc.export()?; + Ok(acc) + } + + /// Write the keys to files. + fn export(&self) -> anyhow::Result<()> { + let sk = self.secret_key.serialize(); + let pk = self.public_key.serialize(); + + export(&self.path, "secret", "b64", to_b64(sk.as_ref()))?; + export(&self.path, "secret", "hex", hex::encode(sk))?; + export(&self.path, "public", "b64", to_b64(pk.as_ref()))?; + export(&self.path, "public", "hex", hex::encode(pk))?; + export(&self.path, "eth-addr", "", format!("{:?}", self.eth_addr()))?; + export(&self.path, "fvm-addr", "", self.fvm_addr().to_string())?; + + Ok(()) + } + pub fn secret_key_path(&self) -> PathBuf { self.path.join("secret.b64") } + + pub fn public_key(&self) -> &PublicKey { + &self.public_key + } } #[cfg(test)] From f9bda9e83dcd6c7175a25df604f16aec40ffa973 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fri=C3=B0rik=20=C3=81smundsson?= Date: Tue, 5 Mar 2024 20:09:12 +0000 Subject: [PATCH 06/12] Add an Upgrade Scheduler (#717) Co-authored-by: Akosh Farkash --- Cargo.lock | 13 + Cargo.toml | 1 + fendermint/app/Cargo.toml | 4 +- fendermint/app/src/app.rs | 8 +- fendermint/app/src/cmd/genesis.rs | 5 +- fendermint/app/src/cmd/rpc.rs | 6 +- fendermint/app/src/cmd/run.rs | 2 + fendermint/app/src/lib.rs | 1 - fendermint/eth/api/examples/common/mod.rs | 4 +- fendermint/eth/api/src/apis/eth.rs | 4 +- fendermint/rpc/examples/simplecoin.rs | 7 +- fendermint/rpc/src/client.rs | 16 +- fendermint/rpc/src/message.rs | 132 +++++++-- fendermint/rpc/src/tx.rs | 4 +- fendermint/testing/contract-test/Cargo.toml | 15 +- fendermint/testing/contract-test/src/lib.rs | 205 +++++++++++++- .../contract-test/tests/run_upgrades.rs | 253 ++++++++++++++++++ fendermint/vm/genesis/tests/golden.rs | 4 +- fendermint/vm/interpreter/Cargo.toml | 15 ++ .../fvmstateparams/cbor/fvmstateparams.cbor | 1 + .../fvmstateparams/cbor/fvmstateparams.txt | 1 + .../fvmstateparams/json/fvmstateparams.json | 10 + .../fvmstateparams/json/fvmstateparams.txt | 1 + fendermint/vm/interpreter/src/arb.rs | 26 ++ .../vm/interpreter/src/fvm/broadcast.rs | 4 +- fendermint/vm/interpreter/src/fvm/exec.rs | 18 ++ fendermint/vm/interpreter/src/fvm/genesis.rs | 11 +- fendermint/vm/interpreter/src/fvm/mod.rs | 19 +- .../vm/interpreter/src/fvm/state/exec.rs | 16 ++ .../vm/interpreter/src/fvm/state/genesis.rs | 1 + .../vm/interpreter/src/fvm/state/snapshot.rs | 1 + fendermint/vm/interpreter/src/fvm/upgrades.rs | 166 ++++++++++++ fendermint/vm/interpreter/src/lib.rs | 3 + fendermint/vm/interpreter/tests/golden.rs | 18 ++ .../golden/manifest/cbor/manifest.cbor | 2 +- .../golden/manifest/cbor/manifest.txt | 2 +- .../golden/manifest/json/manifest.json | 5 +- .../golden/manifest/json/manifest.txt | 2 +- fendermint/vm/snapshot/src/manager.rs | 13 +- fendermint/vm/snapshot/src/manifest.rs | 1 + 40 files changed, 944 insertions(+), 76 deletions(-) create mode 100644 fendermint/testing/contract-test/tests/run_upgrades.rs create mode 100644 fendermint/vm/interpreter/golden/fvmstateparams/cbor/fvmstateparams.cbor create mode 100644 fendermint/vm/interpreter/golden/fvmstateparams/cbor/fvmstateparams.txt create mode 100644 fendermint/vm/interpreter/golden/fvmstateparams/json/fvmstateparams.json create mode 100644 fendermint/vm/interpreter/golden/fvmstateparams/json/fvmstateparams.txt create mode 100644 fendermint/vm/interpreter/src/arb.rs create mode 100644 fendermint/vm/interpreter/src/fvm/upgrades.rs create mode 100644 fendermint/vm/interpreter/tests/golden.rs diff --git a/Cargo.lock b/Cargo.lock index c1d6af6bc..f79f23426 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2954,8 +2954,13 @@ dependencies = [ "anyhow", "arbitrary", "arbtest", + "async-trait", + "byteorder", + "bytes", + "cid", "ethers", "fendermint_crypto", + "fendermint_rpc", "fendermint_testing", "fendermint_vm_actor_interface", "fendermint_vm_core", @@ -2964,10 +2969,13 @@ dependencies = [ "fendermint_vm_message", "fvm", "fvm_ipld_blockstore", + "fvm_ipld_encoding", "fvm_shared", "hex", "ipc-api", "ipc_actors_abis", + "lazy_static", + "multihash 0.18.1", "rand", "tendermint-rpc", "tokio", @@ -3266,6 +3274,7 @@ name = "fendermint_vm_interpreter" version = "0.1.0" dependencies = [ "anyhow", + "arbitrary", "async-stm", "async-trait", "cid", @@ -3276,11 +3285,13 @@ dependencies = [ "fendermint_crypto", "fendermint_eth_hardhat", "fendermint_rpc", + "fendermint_testing", "fendermint_vm_actor_interface", "fendermint_vm_core", "fendermint_vm_encoding", "fendermint_vm_event", "fendermint_vm_genesis", + "fendermint_vm_interpreter", "fendermint_vm_message", "fendermint_vm_resolver", "fendermint_vm_topdown", @@ -3295,10 +3306,12 @@ dependencies = [ "ipc-api", "ipc_actors_abis", "libipld", + "multihash 0.18.1", "num-traits", "pin-project", "quickcheck", "quickcheck_macros", + "rand", "serde", "serde_json", "serde_with 2.3.3", diff --git a/Cargo.toml b/Cargo.toml index b02c394a4..53dacb4b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,7 @@ blake2b_simd = "1.0" bloom = "0.3" bytes = "1.4" clap = { version = "4.1", features = ["derive", "env", "string"] } +byteorder = "1.5.0" config = "0.13" dirs = "5.0" dircpy = "0.3" diff --git a/fendermint/app/Cargo.toml b/fendermint/app/Cargo.toml index f139da0c4..d9ea1496e 100644 --- a/fendermint/app/Cargo.toml +++ b/fendermint/app/Cargo.toml @@ -50,7 +50,9 @@ fendermint_vm_core = { path = "../vm/core" } fendermint_vm_encoding = { path = "../vm/encoding" } fendermint_vm_event = { path = "../vm/event" } fendermint_vm_genesis = { path = "../vm/genesis" } -fendermint_vm_interpreter = { path = "../vm/interpreter", features = ["bundle"] } +fendermint_vm_interpreter = { path = "../vm/interpreter", features = [ + "bundle", +] } fendermint_vm_message = { path = "../vm/message" } fendermint_vm_resolver = { path = "../vm/resolver" } fendermint_vm_snapshot = { path = "../vm/snapshot" } diff --git a/fendermint/app/src/app.rs b/fendermint/app/src/app.rs index 878d47043..fcfb1f120 100644 --- a/fendermint/app/src/app.rs +++ b/fendermint/app/src/app.rs @@ -43,8 +43,8 @@ use tendermint::abci::request::CheckTxKind; use tendermint::abci::{request, response}; use tracing::instrument; +use crate::BlockHeight; use crate::{tmconv::*, VERSION}; -use crate::{BlockHeight, APP_VERSION}; #[derive(Serialize)] #[repr(u8)] @@ -235,6 +235,7 @@ where circ_supply: TokenAmount::zero(), chain_id: 0, power_scale: 0, + app_version: 0, }, }; self.set_committed_state(state)?; @@ -430,7 +431,7 @@ where let info = response::Info { data: "fendermint".to_string(), version: VERSION.to_owned(), - app_version: APP_VERSION, + app_version: state.state_params.app_version, last_block_height: height, last_block_app_hash: state.app_hash(), }; @@ -502,6 +503,7 @@ where circ_supply: out.circ_supply, chain_id: out.chain_id.into(), power_scale: out.power_scale, + app_version: 0, }, }; @@ -785,6 +787,7 @@ where FvmUpdatableParams { power_scale, circ_supply, + app_version, }, _, ) = exec_state.commit().context("failed to commit FVM")?; @@ -792,6 +795,7 @@ where state.state_params.state_root = state_root; state.state_params.power_scale = power_scale; state.state_params.circ_supply = circ_supply; + state.state_params.app_version = app_version; let app_hash = state.app_hash(); let block_height = state.block_height; diff --git a/fendermint/app/src/cmd/genesis.rs b/fendermint/app/src/cmd/genesis.rs index 261ceb111..10cdf1d13 100644 --- a/fendermint/app/src/cmd/genesis.rs +++ b/fendermint/app/src/cmd/genesis.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0, MIT use anyhow::{anyhow, Context}; -use fendermint_app::APP_VERSION; use fendermint_crypto::PublicKey; use fvm_shared::address::Address; use ipc_provider::config::subnet::{EVMSubnet, SubnetConfig}; @@ -47,7 +46,7 @@ cmd! { validators: Vec::new(), accounts: Vec::new(), eam_permission_mode: PermissionMode::Unrestricted, - ipc: None + ipc: None, }; let json = serde_json::to_string_pretty(&genesis)?; @@ -241,7 +240,7 @@ fn into_tendermint(genesis_file: &PathBuf, args: &GenesisIntoTendermintArgs) -> validator: tendermint::consensus::params::ValidatorParams { pub_key_types: vec![tendermint::public_key::Algorithm::Secp256k1], }, - version: Some(tendermint::consensus::params::VersionParams { app: APP_VERSION }), + version: Some(tendermint::consensus::params::VersionParams { app: 0 }), }, // Validators will be returnd from `init_chain`. validators: Vec::new(), diff --git a/fendermint/app/src/cmd/rpc.rs b/fendermint/app/src/cmd/rpc.rs index 9fc36b45f..a4012555b 100644 --- a/fendermint/app/src/cmd/rpc.rs +++ b/fendermint/app/src/cmd/rpc.rs @@ -28,7 +28,7 @@ use tendermint::abci::response::DeliverTx; use tendermint::block::Height; use tendermint_rpc::HttpClient; -use fendermint_rpc::message::{GasParams, MessageFactory}; +use fendermint_rpc::message::{GasParams, SignedMessageFactory}; use fendermint_rpc::{client::FendermintClient, query::QueryClient}; use fendermint_vm_actor_interface::eam::{self, CreateReturn, EthAddress}; @@ -330,7 +330,7 @@ impl TransClient { let sk = read_secret_key(&args.secret_key)?; let addr = to_address(&sk, &args.account_kind)?; let chain_id = chainid::from_str_hashed(&args.chain_name)?; - let mf = MessageFactory::new(sk, addr, args.sequence, chain_id); + let mf = SignedMessageFactory::new(sk, addr, args.sequence, chain_id); let client = client.bind(mf); let client = Self { inner: client, @@ -341,7 +341,7 @@ impl TransClient { } impl BoundClient for TransClient { - fn message_factory_mut(&mut self) -> &mut MessageFactory { + fn message_factory_mut(&mut self) -> &mut SignedMessageFactory { self.inner.message_factory_mut() } } diff --git a/fendermint/app/src/cmd/run.rs b/fendermint/app/src/cmd/run.rs index 5bc134f0d..4f5d9784f 100644 --- a/fendermint/app/src/cmd/run.rs +++ b/fendermint/app/src/cmd/run.rs @@ -11,6 +11,7 @@ use fendermint_crypto::SecretKey; use fendermint_rocksdb::{blockstore::NamespaceBlockstore, namespaces, RocksDb, RocksDbConfig}; use fendermint_vm_actor_interface::eam::EthAddress; use fendermint_vm_interpreter::chain::ChainEnv; +use fendermint_vm_interpreter::fvm::upgrades::UpgradeScheduler; use fendermint_vm_interpreter::{ bytes::{BytesMessageInterpreter, ProposalPrepareMode}, chain::{ChainMessageInterpreter, CheckpointPool}, @@ -113,6 +114,7 @@ async fn run(settings: Settings) -> anyhow::Result<()> { settings.fvm.gas_overestimation_rate, settings.fvm.gas_search_step, settings.fvm.exec_in_check, + UpgradeScheduler::new(), ); let interpreter = SignedMessageInterpreter::new(interpreter); let interpreter = ChainMessageInterpreter::<_, NamespaceBlockstore>::new(interpreter); diff --git a/fendermint/app/src/lib.rs b/fendermint/app/src/lib.rs index 882c3272a..3dad51397 100644 --- a/fendermint/app/src/lib.rs +++ b/fendermint/app/src/lib.rs @@ -12,4 +12,3 @@ pub use store::{AppStore, BitswapBlockstore}; pub type BlockHeight = u64; pub const VERSION: &str = env!("CARGO_PKG_VERSION"); -pub const APP_VERSION: u64 = 0; diff --git a/fendermint/eth/api/examples/common/mod.rs b/fendermint/eth/api/examples/common/mod.rs index 642a942b9..d89a1f106 100644 --- a/fendermint/eth/api/examples/common/mod.rs +++ b/fendermint/eth/api/examples/common/mod.rs @@ -30,7 +30,7 @@ use ethers_core::{ }, }; use fendermint_crypto::SecretKey; -use fendermint_rpc::message::MessageFactory; +use fendermint_rpc::message::SignedMessageFactory; use fendermint_vm_actor_interface::eam::EthAddress; pub type TestMiddleware = SignerMiddleware, Wallet>; @@ -43,7 +43,7 @@ pub struct TestAccount { impl TestAccount { pub fn new(sk: &Path) -> anyhow::Result { - let sk = MessageFactory::read_secret_key(sk)?; + let sk = SignedMessageFactory::read_secret_key(sk)?; let ea = EthAddress::from(sk.public_key()); let h = Address::from_slice(&ea.0); diff --git a/fendermint/eth/api/src/apis/eth.rs b/fendermint/eth/api/src/apis/eth.rs index bde41c331..0138f5295 100644 --- a/fendermint/eth/api/src/apis/eth.rs +++ b/fendermint/eth/api/src/apis/eth.rs @@ -12,7 +12,7 @@ use anyhow::Context; use ethers_core::types::transaction::eip2718::TypedTransaction; use ethers_core::types::{self as et, BlockNumber}; use ethers_core::utils::rlp; -use fendermint_rpc::message::MessageFactory; +use fendermint_rpc::message::SignedMessageFactory; use fendermint_rpc::query::QueryClient; use fendermint_rpc::response::{decode_data, decode_fevm_invoke, decode_fevm_return_data}; use fendermint_vm_actor_interface::eam::{EthAddress, EAM_ACTOR_ADDR}; @@ -624,7 +624,7 @@ where signature: Signature::new_secp256k1(sig.to_vec()), }; let msg = ChainMessage::Signed(msg); - let bz: Vec = MessageFactory::serialize(&msg)?; + let bz: Vec = SignedMessageFactory::serialize(&msg)?; // Use the broadcast version which waits for basic checks to complete, // but not the execution results - those will have to be polled with get_transaction_receipt. diff --git a/fendermint/rpc/examples/simplecoin.rs b/fendermint/rpc/examples/simplecoin.rs index ae4aa64b5..75a2c1127 100644 --- a/fendermint/rpc/examples/simplecoin.rs +++ b/fendermint/rpc/examples/simplecoin.rs @@ -33,7 +33,7 @@ use tracing::Level; use fvm_shared::econ::TokenAmount; use fendermint_rpc::client::FendermintClient; -use fendermint_rpc::message::{GasParams, MessageFactory}; +use fendermint_rpc::message::{GasParams, SignedMessageFactory}; use fendermint_rpc::tx::{CallClient, TxClient, TxCommit}; type MockProvider = ethers::providers::Provider; @@ -108,7 +108,8 @@ async fn main() { let client = FendermintClient::new_http(opts.url, None).expect("error creating client"); - let sk = MessageFactory::read_secret_key(&opts.secret_key).expect("error reading secret key"); + let sk = + SignedMessageFactory::read_secret_key(&opts.secret_key).expect("error reading secret key"); // Query the account nonce from the state, so it doesn't need to be passed as an arg. let sn = sequence(&client, &sk) @@ -124,7 +125,7 @@ async fn main() { .value .chain_id; - let mf = MessageFactory::new_secp256k1(sk, sn, ChainID::from(chain_id)); + let mf = SignedMessageFactory::new_secp256k1(sk, sn, ChainID::from(chain_id)); let mut client = client.bind(mf); diff --git a/fendermint/rpc/src/client.rs b/fendermint/rpc/src/client.rs index 37f06aef9..ef520c7bd 100644 --- a/fendermint/rpc/src/client.rs +++ b/fendermint/rpc/src/client.rs @@ -14,7 +14,7 @@ use tendermint_rpc::{WebSocketClient, WebSocketClientDriver, WebSocketClientUrl} use fendermint_vm_message::query::{FvmQuery, FvmQueryHeight}; -use crate::message::MessageFactory; +use crate::message::SignedMessageFactory; use crate::query::QueryClient; use crate::tx::{ AsyncResponse, BoundClient, CommitResponse, SyncResponse, TxAsync, TxClient, TxCommit, TxSync, @@ -97,7 +97,7 @@ impl FendermintClient { } /// Attach a message factory to the client. - pub fn bind(self, message_factory: MessageFactory) -> BoundFendermintClient { + pub fn bind(self, message_factory: SignedMessageFactory) -> BoundFendermintClient { BoundFendermintClient::new(self.inner, message_factory) } } @@ -134,11 +134,11 @@ where /// Fendermint client capable of signing transactions. pub struct BoundFendermintClient { inner: C, - message_factory: MessageFactory, + message_factory: SignedMessageFactory, } impl BoundFendermintClient { - pub fn new(inner: C, message_factory: MessageFactory) -> Self { + pub fn new(inner: C, message_factory: SignedMessageFactory) -> Self { Self { inner, message_factory, @@ -147,7 +147,7 @@ impl BoundFendermintClient { } impl BoundClient for BoundFendermintClient { - fn message_factory_mut(&mut self) -> &mut MessageFactory { + fn message_factory_mut(&mut self) -> &mut SignedMessageFactory { &mut self.message_factory } } @@ -177,7 +177,7 @@ where where F: FnOnce(&DeliverTx) -> anyhow::Result + Sync + Send, { - let data = MessageFactory::serialize(&msg)?; + let data = SignedMessageFactory::serialize(&msg)?; let response = self.inner.broadcast_tx_async(data).await?; let response = AsyncResponse { response, @@ -200,7 +200,7 @@ where where F: FnOnce(&DeliverTx) -> anyhow::Result + Sync + Send, { - let data = MessageFactory::serialize(&msg)?; + let data = SignedMessageFactory::serialize(&msg)?; let response = self.inner.broadcast_tx_sync(data).await?; let response = SyncResponse { response, @@ -223,7 +223,7 @@ where where F: FnOnce(&DeliverTx) -> anyhow::Result + Sync + Send, { - let data = MessageFactory::serialize(&msg)?; + let data = SignedMessageFactory::serialize(&msg)?; let response = self.inner.broadcast_tx_commit(data).await?; // We have a fully `DeliverTx` with default fields even if `CheckTx` indicates failure. let return_data = if response.check_tx.code.is_err() || response.deliver_tx.code.is_err() { diff --git a/fendermint/rpc/src/message.rs b/fendermint/rpc/src/message.rs index f92bc7bae..2dffbb94f 100644 --- a/fendermint/rpc/src/message.rs +++ b/fendermint/rpc/src/message.rs @@ -16,24 +16,121 @@ use fvm_shared::{ use crate::B64_ENGINE; -/// Factory methods for signed transaction payload construction. +/// Factory methods for transaction payload construction. /// /// It assumes the sender is an `f1` type address, it won't work with `f410` addresses. /// For those one must use the Ethereum API, with a suitable client library such as [ethers]. pub struct MessageFactory { - sk: SecretKey, addr: Address, sequence: u64, - chain_id: ChainID, } impl MessageFactory { + pub fn new(addr: Address, sequence: u64) -> Self { + Self { addr, sequence } + } + + pub fn address(&self) -> &Address { + &self.addr + } + + /// Set the sequence to an arbitrary value. + pub fn set_sequence(&mut self, sequence: u64) { + self.sequence = sequence; + } + + pub fn transaction( + &mut self, + to: Address, + method_num: MethodNum, + params: RawBytes, + value: TokenAmount, + gas_params: GasParams, + ) -> Message { + let msg = Message { + version: Default::default(), // TODO: What does this do? + from: self.addr, + to, + sequence: self.sequence, + value, + method_num, + params, + gas_limit: gas_params.gas_limit, + gas_fee_cap: gas_params.gas_fee_cap, + gas_premium: gas_params.gas_premium, + }; + + self.sequence += 1; + + msg + } + + pub fn fevm_create( + &mut self, + contract: Bytes, + constructor_args: Bytes, + value: TokenAmount, + gas_params: GasParams, + ) -> anyhow::Result { + let initcode = [contract.to_vec(), constructor_args.to_vec()].concat(); + let initcode = RawBytes::serialize(BytesSer(&initcode))?; + Ok(self.transaction( + eam::EAM_ACTOR_ADDR, + eam::Method::CreateExternal as u64, + initcode, + value, + gas_params, + )) + } + + pub fn fevm_invoke( + &mut self, + contract: Address, + calldata: Bytes, + value: TokenAmount, + gas_params: GasParams, + ) -> anyhow::Result { + let calldata = RawBytes::serialize(BytesSer(&calldata))?; + Ok(self.transaction( + contract, + evm::Method::InvokeContract as u64, + calldata, + value, + gas_params, + )) + } + + pub fn fevm_call( + &mut self, + contract: Address, + calldata: Bytes, + value: TokenAmount, + gas_params: GasParams, + ) -> anyhow::Result { + let msg = self.fevm_invoke(contract, calldata, value, gas_params)?; + + // Roll back the sequence, we don't really want to invoke anything. + self.set_sequence(msg.sequence); + + Ok(msg) + } +} +/// Wrapper for MessageFactory which generates signed messages +/// +/// It assumes the sender is an `f1` type address, it won't work with `f410` addresses. +/// For those one must use the Ethereum API, with a suitable client library such as [ethers]. +pub struct SignedMessageFactory { + inner: MessageFactory, + sk: SecretKey, + chain_id: ChainID, +} + +impl SignedMessageFactory { /// Create a factor from a secret key and its corresponding address, which could be a delegated one. pub fn new(sk: SecretKey, addr: Address, sequence: u64, chain_id: ChainID) -> Self { Self { + inner: MessageFactory::new(addr, sequence), sk, - addr, - sequence, chain_id, } } @@ -62,12 +159,7 @@ impl MessageFactory { /// Actor address. pub fn address(&self) -> &Address { - &self.addr - } - - /// Set the sequence to an arbitrary value. - pub fn set_sequence(&mut self, sequence: u64) { - self.sequence = sequence; + self.inner.address() } /// Transfer tokens to another account. @@ -89,19 +181,9 @@ impl MessageFactory { value: TokenAmount, gas_params: GasParams, ) -> anyhow::Result { - let message = fvm_shared::message::Message { - version: Default::default(), // TODO: What does this do? - from: self.addr, - to, - sequence: self.sequence, - value, - method_num, - params, - gas_limit: gas_params.gas_limit, - gas_fee_cap: gas_params.gas_fee_cap, - gas_premium: gas_params.gas_premium, - }; - self.sequence += 1; + let message = self + .inner + .transaction(to, method_num, params, value, gas_params); let signed = SignedMessage::new_secp256k1(message, &self.sk, &self.chain_id)?; let chain = ChainMessage::Signed(signed); Ok(chain) @@ -163,7 +245,7 @@ impl MessageFactory { }; // Roll back the sequence, we don't really want to invoke anything. - self.set_sequence(msg.sequence); + self.inner.set_sequence(msg.sequence); Ok(msg) } diff --git a/fendermint/rpc/src/tx.rs b/fendermint/rpc/src/tx.rs index ff9423366..6bea2cbc9 100644 --- a/fendermint/rpc/src/tx.rs +++ b/fendermint/rpc/src/tx.rs @@ -18,7 +18,7 @@ use fvm_shared::MethodNum; use fendermint_vm_actor_interface::eam::CreateReturn; use fendermint_vm_message::chain::ChainMessage; -use crate::message::{GasParams, MessageFactory}; +use crate::message::{GasParams, SignedMessageFactory}; use crate::query::{QueryClient, QueryResponse}; use crate::response::{decode_bytes, decode_fevm_create, decode_fevm_invoke}; @@ -29,7 +29,7 @@ pub trait BroadcastMode { } pub trait BoundClient { - fn message_factory_mut(&mut self) -> &mut MessageFactory; + fn message_factory_mut(&mut self) -> &mut SignedMessageFactory; fn address(&mut self) -> Address { *self.message_factory_mut().address() diff --git a/fendermint/testing/contract-test/Cargo.toml b/fendermint/testing/contract-test/Cargo.toml index 2287be8ad..0c186f8b6 100644 --- a/fendermint/testing/contract-test/Cargo.toml +++ b/fendermint/testing/contract-test/Cargo.toml @@ -10,6 +10,7 @@ license.workspace = true [dependencies] anyhow = { workspace = true } +cid = { workspace = true } ethers = { workspace = true } fvm = { workspace = true } fvm_shared = { workspace = true } @@ -17,6 +18,8 @@ fvm_ipld_blockstore = { workspace = true } hex = { workspace = true } rand = { workspace = true } tendermint-rpc = { workspace = true } +tokio = { workspace = true } +byteorder = { workspace = true } ipc-api = { workspace = true } ipc_actors_abis = { workspace = true } @@ -27,11 +30,17 @@ fendermint_vm_actor_interface = { path = "../../vm/actor_interface" } fendermint_vm_core = { path = "../../vm/core" } fendermint_vm_genesis = { path = "../../vm/genesis" } fendermint_vm_message = { path = "../../vm/message" } -fendermint_vm_interpreter = { path = "../../vm/interpreter", features = ["bundle"] } - +fendermint_vm_interpreter = { path = "../../vm/interpreter", features = [ + "bundle", +] } [dev-dependencies] arbitrary = { workspace = true } arbtest = { workspace = true } +async-trait = { workspace = true } rand = { workspace = true } -tokio = { workspace = true } +fendermint_rpc = { path = "../../rpc" } +lazy_static = { workspace = true } +bytes = { workspace = true } +fvm_ipld_encoding = { workspace = true } +multihash = { workspace = true } diff --git a/fendermint/testing/contract-test/src/lib.rs b/fendermint/testing/contract-test/src/lib.rs index 9944168d5..feae99522 100644 --- a/fendermint/testing/contract-test/src/lib.rs +++ b/fendermint/testing/contract-test/src/lib.rs @@ -1,18 +1,24 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -use anyhow::{anyhow, Context}; -use std::sync::Arc; +use anyhow::{anyhow, Context, Result}; +use byteorder::{BigEndian, WriteBytesExt}; +use cid::Cid; +use fendermint_vm_core::Timestamp; +use fendermint_vm_interpreter::fvm::PowerUpdates; +use fvm_shared::{bigint::Zero, clock::ChainEpoch, econ::TokenAmount, version::NetworkVersion}; +use std::{future::Future, sync::Arc}; use fendermint_vm_genesis::Genesis; use fendermint_vm_interpreter::{ fvm::{ bundle::{bundle_path, contracts_path, custom_actors_bundle_path}, - state::{FvmExecState, FvmGenesisState}, + state::{FvmExecState, FvmGenesisState, FvmStateParams, FvmUpdatableParams}, store::memory::MemoryBlockstore, - FvmGenesisOutput, FvmMessageInterpreter, + upgrades::UpgradeScheduler, + FvmApplyRet, FvmGenesisOutput, FvmMessage, FvmMessageInterpreter, }, - GenesisInterpreter, + ExecInterpreter, GenesisInterpreter, }; use fvm::engine::MultiEngine; @@ -43,7 +49,15 @@ pub async fn init_exec_state( let (client, _) = tendermint_rpc::MockClient::new(tendermint_rpc::MockRequestMethodMatcher::default()); - let interpreter = FvmMessageInterpreter::new(client, None, contracts_path(), 1.05, 1.05, false); + let interpreter = FvmMessageInterpreter::new( + client, + None, + contracts_path(), + 1.05, + 1.05, + false, + UpgradeScheduler::new(), + ); let (state, out) = interpreter .init(state, genesis) @@ -56,3 +70,182 @@ pub async fn init_exec_state( Ok((state, out)) } + +pub struct Tester { + interpreter: Arc, + state_store: Arc, + multi_engine: Arc, + exec_state: Arc>>>, + state_params: FvmStateParams, +} + +impl Tester +where + I: GenesisInterpreter< + State = FvmGenesisState, + Genesis = Genesis, + Output = FvmGenesisOutput, + >, + I: ExecInterpreter< + State = FvmExecState, + Message = FvmMessage, + BeginOutput = FvmApplyRet, + DeliverOutput = FvmApplyRet, + EndOutput = PowerUpdates, + >, +{ + fn state_store_clone(&self) -> MemoryBlockstore { + self.state_store.as_ref().clone() + } + + pub fn new(interpreter: I, state_store: MemoryBlockstore) -> Self { + Self { + interpreter: Arc::new(interpreter), + state_store: Arc::new(state_store), + multi_engine: Arc::new(MultiEngine::new(1)), + exec_state: Arc::new(tokio::sync::Mutex::new(None)), + state_params: FvmStateParams { + timestamp: Timestamp(0), + state_root: Cid::default(), + network_version: NetworkVersion::V21, + base_fee: TokenAmount::zero(), + circ_supply: TokenAmount::zero(), + chain_id: 0, + power_scale: 0, + app_version: 0, + }, + } + } + + pub async fn init(&mut self, genesis: Genesis) -> anyhow::Result<()> { + let bundle_path = bundle_path(); + let bundle = std::fs::read(&bundle_path) + .with_context(|| format!("failed to read bundle: {}", bundle_path.to_string_lossy()))?; + + let custom_actors_bundle_path = custom_actors_bundle_path(); + let custom_actors_bundle = + std::fs::read(&custom_actors_bundle_path).with_context(|| { + format!( + "failed to read custom actors_bundle: {}", + custom_actors_bundle_path.to_string_lossy() + ) + })?; + + let state = FvmGenesisState::new( + self.state_store_clone(), + self.multi_engine.clone(), + &bundle, + &custom_actors_bundle, + ) + .await + .context("failed to create genesis state")?; + + let (state, out) = self + .interpreter + .init(state, genesis) + .await + .context("failed to init from genesis")?; + + let state_root = state.commit().context("failed to commit genesis state")?; + + self.state_params = FvmStateParams { + state_root, + timestamp: out.timestamp, + network_version: out.network_version, + base_fee: out.base_fee, + circ_supply: out.circ_supply, + chain_id: out.chain_id.into(), + power_scale: out.power_scale, + app_version: 0, + }; + + Ok(()) + } + + /// Take the execution state, update it, put it back, return the output. + async fn modify_exec_state(&self, f: F) -> anyhow::Result + where + F: FnOnce(FvmExecState) -> R, + R: Future, T)>>, + { + let mut guard = self.exec_state.lock().await; + let state = guard.take().expect("exec state empty"); + + let (state, ret) = f(state).await?; + + *guard = Some(state); + + Ok(ret) + } + + /// Put the execution state during block execution. Has to be empty. + async fn put_exec_state(&self, state: FvmExecState) { + let mut guard = self.exec_state.lock().await; + assert!(guard.is_none(), "exec state not empty"); + *guard = Some(state); + } + + /// Take the execution state during block execution. Has to be non-empty. + async fn take_exec_state(&self) -> FvmExecState { + let mut guard = self.exec_state.lock().await; + guard.take().expect("exec state empty") + } + + pub async fn begin_block(&self, block_height: ChainEpoch) -> Result<()> { + let mut block_hash: [u8; 32] = [0; 32]; + let _ = block_hash.as_mut().write_i64::(block_height); + + let db = self.state_store.as_ref().clone(); + let mut state_params = self.state_params.clone(); + state_params.timestamp = Timestamp(block_height as u64); + + let state = FvmExecState::new(db, self.multi_engine.as_ref(), block_height, state_params) + .context("error creating new state")? + .with_block_hash(block_hash); + + self.put_exec_state(state).await; + + let _res = self + .modify_exec_state(|s| self.interpreter.begin(s)) + .await + .unwrap(); + + Ok(()) + } + + pub async fn end_block(&self, _block_height: ChainEpoch) -> Result<()> { + let _ret = self + .modify_exec_state(|s| self.interpreter.end(s)) + .await + .context("end failed")?; + + Ok(()) + } + + pub async fn commit(&mut self) -> Result<()> { + let exec_state = self.take_exec_state().await; + + let ( + state_root, + FvmUpdatableParams { + power_scale, + circ_supply, + app_version, + }, + _, + ) = exec_state.commit().context("failed to commit FVM")?; + + self.state_params.state_root = state_root; + self.state_params.power_scale = power_scale; + self.state_params.circ_supply = circ_supply; + self.state_params.app_version = app_version; + + eprintln!("self.state_params: {:?}", self.state_params); + + Ok(()) + } + + pub fn state_params(&self) -> FvmStateParams { + self.state_params.clone() + } +} diff --git a/fendermint/testing/contract-test/tests/run_upgrades.rs b/fendermint/testing/contract-test/tests/run_upgrades.rs new file mode 100644 index 000000000..00b3f4a4d --- /dev/null +++ b/fendermint/testing/contract-test/tests/run_upgrades.rs @@ -0,0 +1,253 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +mod staking; + +use anyhow::{Context, Ok}; +use async_trait::async_trait; +use ethers::types::U256; +use fendermint_contract_test::Tester; +use fendermint_rpc::response::decode_fevm_return_data; +use rand::rngs::StdRng; +use rand::SeedableRng; +use std::str::FromStr; + +use ethers::contract::abigen; +use fvm_shared::address::Address; +use fvm_shared::bigint::Zero; +use fvm_shared::econ::TokenAmount; +use fvm_shared::version::NetworkVersion; +use tendermint_rpc::Client; + +use fendermint_crypto::SecretKey; +use fendermint_vm_actor_interface::eam; +use fendermint_vm_actor_interface::eam::EthAddress; +use fendermint_vm_core::Timestamp; +use fendermint_vm_genesis::{Account, Actor, ActorMeta, Genesis, PermissionMode, SignerAddr}; +use fendermint_vm_interpreter::fvm::store::memory::MemoryBlockstore; +use fendermint_vm_interpreter::fvm::upgrades::{Upgrade, UpgradeScheduler}; +use fendermint_vm_interpreter::fvm::{bundle::contracts_path, FvmMessageInterpreter}; + +// returns a seeded secret key which is guaranteed to be the same every time +fn my_secret_key() -> SecretKey { + SecretKey::random(&mut StdRng::seed_from_u64(123)) +} + +// this test applies a series of upgrades to the state and checks that the upgrades are applied correctly +#[tokio::test] +async fn test_applying_upgrades() { + use bytes::Bytes; + use fendermint_rpc::message::{GasParams, MessageFactory}; + use lazy_static::lazy_static; + + lazy_static! { + /// Default gas params based on the testkit. + static ref GAS_PARAMS: GasParams = GasParams { + gas_limit: 10_000_000_000, + gas_fee_cap: TokenAmount::default(), + gas_premium: TokenAmount::default(), + }; + static ref ADDR: Address = Address::new_secp256k1(&my_secret_key().public_key().serialize()).unwrap(); + } + + // this is the contract we want to deploy + const CONTRACT_HEX: &str = include_str!("../../contracts/SimpleCoin.bin"); + // generate type safe bindings in rust to this contract + abigen!(SimpleCoin, "../contracts/SimpleCoin.abi"); + // once we deploy this contract, this is the address we expect the contract to be deployed to + const CONTRACT_ADDRESS: &str = "f410fnz5jdky3zzcj6pejqkomkggw72pcuvkpihz2rwa"; + // the amount we want to send to the contract + const SEND_BALANCE_AMOUNT: u64 = 1000; + const CHAIN_NAME: &str = "mychain"; + + let mut upgrade_scheduler = UpgradeScheduler::new(); + upgrade_scheduler + .add( + Upgrade::new(CHAIN_NAME, 1, Some(1), |state| { + println!( + "[Upgrade at height {}] Deploy simple contract", + state.block_height() + ); + + // create a message for deploying the contract + let mut mf = MessageFactory::new(*ADDR, 1); + let message = mf + .fevm_create( + Bytes::from( + hex::decode(CONTRACT_HEX) + .context("error parsing contract") + .unwrap(), + ), + Bytes::default(), + TokenAmount::default(), + GAS_PARAMS.clone(), + ) + .unwrap(); + + // execute the message + let (res, _) = state.execute_implicit(message).unwrap(); + assert!( + res.msg_receipt.exit_code.is_success(), + "{:?}", + res.failure_info + ); + + // parse the message receipt data and make sure the contract was deployed to the expected address + let res = fvm_ipld_encoding::from_slice::( + &res.msg_receipt.return_data, + ) + .unwrap(); + assert_eq!( + res.delegated_address(), + Address::from_str(CONTRACT_ADDRESS).unwrap() + ); + + Ok(()) + }) + .unwrap(), + ) + .unwrap(); + + upgrade_scheduler + .add( + Upgrade::new(CHAIN_NAME, 2, None, |state| { + println!( + "[Upgrade at height {}] Sends a balance", + state.block_height() + ); + + // build the calldata for the send_coin function + let (client, _mock) = ethers::providers::Provider::mocked(); + let simple_coin = SimpleCoin::new(EthAddress::from_id(101), client.into()); + let call = simple_coin.send_coin( + // the address we are sending the balance to (which is us in this case) + EthAddress::from(my_secret_key().public_key()).into(), + // the amount we are sending + U256::from(SEND_BALANCE_AMOUNT), + ); + + // create a message for sending the balance + let mut mf = MessageFactory::new(*ADDR, 1); + let message = mf + .fevm_invoke( + Address::from_str(CONTRACT_ADDRESS).unwrap(), + call.calldata().unwrap().0, + TokenAmount::default(), + GAS_PARAMS.clone(), + ) + .unwrap(); + + // execute the message + let (res, _) = state.execute_implicit(message).unwrap(); + assert!( + res.msg_receipt.exit_code.is_success(), + "{:?}", + res.failure_info + ); + + Ok(()) + }) + .unwrap(), + ) + .unwrap(); + + upgrade_scheduler + .add( + Upgrade::new(CHAIN_NAME, 3, None, |state| { + println!( + "[Upgrade at height {}] Returns a balance", + state.block_height() + ); + + // build the calldata for the get_balance function + let (client, _mock) = ethers::providers::Provider::mocked(); + let simple_coin = SimpleCoin::new(EthAddress::from_id(0), client.into()); + let call = + simple_coin.get_balance(EthAddress::from(my_secret_key().public_key()).into()); + + let mut mf = MessageFactory::new(*ADDR, 1); + let message = mf + .fevm_invoke( + Address::from_str(CONTRACT_ADDRESS).unwrap(), + call.calldata().unwrap().0, + TokenAmount::default(), + GAS_PARAMS.clone(), + ) + .unwrap(); + + // execute the message + let (res, _) = state.execute_implicit(message).unwrap(); + assert!( + res.msg_receipt.exit_code.is_success(), + "{:?}", + res.failure_info + ); + + // parse the message receipt data and make sure the balance we sent in previous upgrade is returned + let bytes = decode_fevm_return_data(res.msg_receipt.return_data).unwrap(); + let balance = U256::from_big_endian(&bytes); + assert_eq!(balance, U256::from(SEND_BALANCE_AMOUNT)); + + Ok(()) + }) + .unwrap(), + ) + .unwrap(); + + let interpreter: FvmMessageInterpreter = FvmMessageInterpreter::new( + NeverCallClient, + None, + contracts_path(), + 1.05, + 1.05, + false, + upgrade_scheduler, + ); + + let mut tester = Tester::new(interpreter, MemoryBlockstore::new()); + + let genesis = Genesis { + chain_name: CHAIN_NAME.to_string(), + timestamp: Timestamp(0), + network_version: NetworkVersion::V21, + base_fee: TokenAmount::zero(), + power_scale: 0, + validators: Vec::new(), + accounts: vec![Actor { + meta: ActorMeta::Account(Account { + owner: SignerAddr(*ADDR), + }), + balance: TokenAmount::from_atto(0), + }], + eam_permission_mode: PermissionMode::Unrestricted, + ipc: None, + }; + + tester.init(genesis).await.unwrap(); + + // check that the app version is 0 + assert_eq!(tester.state_params().app_version, 0); + + // iterate over all the upgrades + for block_height in 1..=3 { + tester.begin_block(block_height).await.unwrap(); + tester.end_block(block_height).await.unwrap(); + tester.commit().await.unwrap(); + + // check that the app_version was upgraded to 1 + assert_eq!(tester.state_params().app_version, 1); + } +} + +#[derive(Clone)] +struct NeverCallClient; + +#[async_trait] +impl Client for NeverCallClient { + async fn perform(&self, _request: R) -> Result + where + R: tendermint_rpc::SimpleRequest, + { + todo!() + } +} diff --git a/fendermint/vm/genesis/tests/golden.rs b/fendermint/vm/genesis/tests/golden.rs index 1d587afad..dfd734207 100644 --- a/fendermint/vm/genesis/tests/golden.rs +++ b/fendermint/vm/genesis/tests/golden.rs @@ -1,7 +1,7 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -/// JSON based test in case we want to configure the Genesis by hand. +/// JSON based test so we can parse data from the disk where it's nice to be human readable. mod json { use fendermint_testing::golden_json; use fendermint_vm_genesis::Genesis; @@ -9,7 +9,7 @@ mod json { golden_json! { "genesis/json", genesis, Genesis::arbitrary } } -/// CBOR based tests in case we have to grab Genesis from on-chain storage. +/// CBOR based tests to make sure we can parse data in network format. mod cbor { use fendermint_testing::golden_cbor; use fendermint_vm_genesis::Genesis; diff --git a/fendermint/vm/interpreter/Cargo.toml b/fendermint/vm/interpreter/Cargo.toml index a0a3d7f2d..c484d2098 100644 --- a/fendermint/vm/interpreter/Cargo.toml +++ b/fendermint/vm/interpreter/Cargo.toml @@ -23,6 +23,7 @@ fendermint_rpc = { path = "../../rpc" } fendermint_actors = { path = "../../actors" } fendermint_actor_chainmetadata = { path = "../../actors/chainmetadata" } fendermint_actor_eam = { workspace = true } +fendermint_testing = { path = "../../testing", optional = true } ipc_actors_abis = { workspace = true } ipc-api = { workspace = true } @@ -56,14 +57,28 @@ pin-project = { workspace = true } tokio-stream = { workspace = true } tokio-util = { workspace = true } +arbitrary = { workspace = true, optional = true } +quickcheck = { workspace = true, optional = true } +rand = { workspace = true, optional = true } + [dev-dependencies] quickcheck = { workspace = true } quickcheck_macros = { workspace = true } tempfile = { workspace = true } +fendermint_vm_interpreter = { path = ".", features = ["arb"] } +fendermint_testing = { path = "../../testing", features = ["golden"] } fvm = { workspace = true, features = ["arb", "testing"] } fendermint_vm_genesis = { path = "../genesis", features = ["arb"] } +multihash = { workspace = true } [features] default = [] bundle = [] +arb = [ + "arbitrary", + "quickcheck", + "fvm_shared/arb", + "fendermint_testing/arb", + "rand", +] diff --git a/fendermint/vm/interpreter/golden/fvmstateparams/cbor/fvmstateparams.cbor b/fendermint/vm/interpreter/golden/fvmstateparams/cbor/fvmstateparams.cbor new file mode 100644 index 000000000..aa2cf614c --- /dev/null +++ b/fendermint/vm/interpreter/golden/fvmstateparams/cbor/fvmstateparams.cbor @@ -0,0 +1 @@ +a86a73746174655f726f6f74d82a58250001bd3ffa3d1e11163d5fa40a156e3caef9cf45f736ce99b42abede4c32ccf9d50f9018966974696d657374616d701b146ef18ccc9e55bb6f6e6574776f726b5f76657273696f6e1568626173655f6665655100c167b71a6b28cec166ff4ca208fbb3446b636972635f737570706c795100fc8ea419cbcbc401a12180e388e68f5d68636861696e5f69641b000b6b07b4af45db6b706f7765725f7363616c65006b6170705f76657273696f6e01 \ No newline at end of file diff --git a/fendermint/vm/interpreter/golden/fvmstateparams/cbor/fvmstateparams.txt b/fendermint/vm/interpreter/golden/fvmstateparams/cbor/fvmstateparams.txt new file mode 100644 index 000000000..40c0d1b2d --- /dev/null +++ b/fendermint/vm/interpreter/golden/fvmstateparams/cbor/fvmstateparams.txt @@ -0,0 +1 @@ +FvmStateParams { state_root: Cid(bag6t76r5dyirmpk7uqfbk3r4v3446rpxg3hjtnbkx3peymwm7hkq7eaysy), timestamp: Timestamp(1472379715227375035), network_version: NetworkVersion(21), base_fee: TokenAmount(257079523536971773801.541083398290518852), circ_supply: TokenAmount(335706089450661601774.571585084053688157), chain_id: 3213905584145883, power_scale: 0, app_version: 1 } \ No newline at end of file diff --git a/fendermint/vm/interpreter/golden/fvmstateparams/json/fvmstateparams.json b/fendermint/vm/interpreter/golden/fvmstateparams/json/fvmstateparams.json new file mode 100644 index 000000000..dd4b36a88 --- /dev/null +++ b/fendermint/vm/interpreter/golden/fvmstateparams/json/fvmstateparams.json @@ -0,0 +1,10 @@ +{ + "state_root": "QmQysvBaHAk7sygxwxzTN2mdvA5jqXMhXzSqTDSNDDJBnF", + "timestamp": 4888195286957380285, + "network_version": 21, + "base_fee": "19429382762560951179258988865468432764", + "circ_supply": "250860824295515106050023062062359002052", + "chain_id": 0, + "power_scale": 3, + "app_version": 1 +} \ No newline at end of file diff --git a/fendermint/vm/interpreter/golden/fvmstateparams/json/fvmstateparams.txt b/fendermint/vm/interpreter/golden/fvmstateparams/json/fvmstateparams.txt new file mode 100644 index 000000000..f3fc415ea --- /dev/null +++ b/fendermint/vm/interpreter/golden/fvmstateparams/json/fvmstateparams.txt @@ -0,0 +1 @@ +FvmStateParams { state_root: Cid(QmQysvBaHAk7sygxwxzTN2mdvA5jqXMhXzSqTDSNDDJBnF), timestamp: Timestamp(4888195286957380285), network_version: NetworkVersion(21), base_fee: TokenAmount(19429382762560951179.258988865468432764), circ_supply: TokenAmount(250860824295515106050.023062062359002052), chain_id: 0, power_scale: 3, app_version: 1 } \ No newline at end of file diff --git a/fendermint/vm/interpreter/src/arb.rs b/fendermint/vm/interpreter/src/arb.rs new file mode 100644 index 000000000..991e403cb --- /dev/null +++ b/fendermint/vm/interpreter/src/arb.rs @@ -0,0 +1,26 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use fendermint_testing::arb::{ArbCid, ArbTokenAmount}; +use fendermint_vm_core::{chainid, Timestamp}; +use fvm_shared::version::NetworkVersion; +use quickcheck::{Arbitrary, Gen}; + +use crate::fvm::state::FvmStateParams; + +impl Arbitrary for FvmStateParams { + fn arbitrary(g: &mut Gen) -> Self { + Self { + state_root: ArbCid::arbitrary(g).0, + timestamp: Timestamp(u64::arbitrary(g)), + network_version: NetworkVersion::new(*g.choose(&[21]).unwrap()), + base_fee: ArbTokenAmount::arbitrary(g).0, + circ_supply: ArbTokenAmount::arbitrary(g).0, + chain_id: chainid::from_str_hashed(String::arbitrary(g).as_str()) + .unwrap() + .into(), + power_scale: *g.choose(&[-1, 0, 3]).unwrap(), + app_version: *g.choose(&[0, 1, 2]).unwrap(), + } + } +} diff --git a/fendermint/vm/interpreter/src/fvm/broadcast.rs b/fendermint/vm/interpreter/src/fvm/broadcast.rs index d0775d8d2..5b3ae2147 100644 --- a/fendermint/vm/interpreter/src/fvm/broadcast.rs +++ b/fendermint/vm/interpreter/src/fvm/broadcast.rs @@ -16,7 +16,7 @@ use fendermint_crypto::SecretKey; use fendermint_rpc::message::GasParams; use fendermint_rpc::query::QueryClient; use fendermint_rpc::tx::{CallClient, TxClient, TxSync}; -use fendermint_rpc::{client::FendermintClient, message::MessageFactory}; +use fendermint_rpc::{client::FendermintClient, message::SignedMessageFactory}; use fendermint_vm_message::query::FvmQueryHeight; macro_rules! retry { @@ -121,7 +121,7 @@ where .context("failed to get broadcaster sequence")?; let factory = - MessageFactory::new(self.secret_key.clone(), self.addr, sequence, chain_id); + SignedMessageFactory::new(self.secret_key.clone(), self.addr, sequence, chain_id); // Using the bound client as a one-shot transaction sender. let mut client = self.client.clone().bind(factory); diff --git a/fendermint/vm/interpreter/src/fvm/exec.rs b/fendermint/vm/interpreter/src/fvm/exec.rs index 765d5d98e..1823e9311 100644 --- a/fendermint/vm/interpreter/src/fvm/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/exec.rs @@ -212,6 +212,24 @@ where PowerUpdates::default() }; + // check for upgrades in the upgrade_scheduler + let chain_id = state.chain_id(); + let block_height: u64 = state.block_height().try_into().unwrap(); + if let Some(upgrade) = self.upgrade_scheduler.get(chain_id, block_height) { + // TODO: consider using an explicit tracing enum for upgrades + tracing::info!(?chain_id, height = block_height, "Executing an upgrade"); + + // there is an upgrade scheduled for this height, lets run the migration + let res = upgrade.execute(&mut state).context("upgrade failed")?; + if let Some(new_app_version) = res { + state.update_app_version(|app_version| { + *app_version = new_app_version; + }); + + tracing::info!(app_version = state.app_version(), "upgraded app version"); + } + } + Ok((state, updates)) } } diff --git a/fendermint/vm/interpreter/src/fvm/genesis.rs b/fendermint/vm/interpreter/src/fvm/genesis.rs index 3eed0fe14..f3380a783 100644 --- a/fendermint/vm/interpreter/src/fvm/genesis.rs +++ b/fendermint/vm/interpreter/src/fvm/genesis.rs @@ -531,6 +531,7 @@ mod tests { bundle::{bundle_path, contracts_path, custom_actors_bundle_path}, state::ipc::GatewayCaller, store::memory::MemoryBlockstore, + upgrades::UpgradeScheduler, FvmMessageInterpreter, }, GenesisInterpreter, @@ -650,7 +651,15 @@ mod tests { fn make_interpreter( ) -> FvmMessageInterpreter> { let (client, _) = MockClient::new(MockRequestMethodMatcher::default()); - FvmMessageInterpreter::new(client, None, contracts_path(), 1.05, 1.05, false) + FvmMessageInterpreter::new( + client, + None, + contracts_path(), + 1.05, + 1.05, + false, + UpgradeScheduler::new(), + ) } fn read_bundle() -> Vec { diff --git a/fendermint/vm/interpreter/src/fvm/mod.rs b/fendermint/vm/interpreter/src/fvm/mod.rs index c46f46319..7ea3a80e2 100644 --- a/fendermint/vm/interpreter/src/fvm/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/mod.rs @@ -11,6 +11,7 @@ mod genesis; mod query; pub mod state; pub mod store; +pub mod upgrades; #[cfg(any(test, feature = "bundle"))] pub mod bundle; @@ -22,12 +23,13 @@ pub use exec::FvmApplyRet; use fendermint_crypto::{PublicKey, SecretKey}; use fendermint_eth_hardhat::Hardhat; pub use fendermint_vm_message::query::FvmQuery; +use fvm_ipld_blockstore::Blockstore; pub use genesis::FvmGenesisOutput; pub use query::FvmQueryRet; use tendermint_rpc::Client; pub use self::broadcast::Broadcaster; -use self::state::ipc::GatewayCaller; +use self::{state::ipc::GatewayCaller, upgrades::UpgradeScheduler}; pub type FvmMessage = fvm_shared::message::Message; @@ -56,7 +58,10 @@ impl ValidatorContext { /// Interpreter working on already verified unsigned messages. #[derive(Clone)] -pub struct FvmMessageInterpreter { +pub struct FvmMessageInterpreter +where + DB: Blockstore + 'static + Clone, +{ contracts: Hardhat, /// Tendermint client for querying the RPC. client: C, @@ -72,9 +77,14 @@ pub struct FvmMessageInterpreter { /// when they are added to the mempool, or just the most basic ones are performed. exec_in_check: bool, gateway: GatewayCaller, + /// Upgrade scheduler stores all the upgrades to be executed at given heights. + upgrade_scheduler: UpgradeScheduler, } -impl FvmMessageInterpreter { +impl FvmMessageInterpreter +where + DB: Blockstore + 'static + Clone, +{ pub fn new( client: C, validator_ctx: Option>, @@ -82,6 +92,7 @@ impl FvmMessageInterpreter { gas_overestimation_rate: f64, gas_search_step: f64, exec_in_check: bool, + upgrade_scheduler: UpgradeScheduler, ) -> Self { Self { client, @@ -91,12 +102,14 @@ impl FvmMessageInterpreter { gas_search_step, exec_in_check, gateway: GatewayCaller::default(), + upgrade_scheduler, } } } impl FvmMessageInterpreter where + DB: fvm_ipld_blockstore::Blockstore + 'static + Clone, C: Client + Sync, { /// Indicate that the node is syncing with the rest of the network and hasn't caught up with the tip yet. diff --git a/fendermint/vm/interpreter/src/fvm/state/exec.rs b/fendermint/vm/interpreter/src/fvm/state/exec.rs index 875969dd8..6a598992f 100644 --- a/fendermint/vm/interpreter/src/fvm/state/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/state/exec.rs @@ -59,6 +59,8 @@ pub struct FvmStateParams { pub chain_id: u64, /// Conversion from collateral to voting power. pub power_scale: PowerScale, + /// The application protocol version. + pub app_version: u64, } /// Parts of the state which can be updated by message execution, apart from the actor state. @@ -75,6 +77,8 @@ pub struct FvmUpdatableParams { /// Doesn't change at the moment but in theory it could, /// and it doesn't have a place within the FVM. pub power_scale: PowerScale, + /// The application protocol version. + pub app_version: u64, } pub type MachineBlockstore = > as Machine>::Blockstore; @@ -141,6 +145,7 @@ where params: FvmUpdatableParams { circ_supply: params.circ_supply, power_scale: params.power_scale, + app_version: params.app_version, }, params_dirty: false, }) @@ -206,6 +211,10 @@ where self.params.power_scale } + pub fn app_version(&self) -> u64 { + self.params.app_version + } + /// Get a mutable reference to the underlying [StateTree]. pub fn state_tree_mut(&mut self) -> &mut StateTree> { self.executor.state_tree_mut() @@ -255,6 +264,13 @@ where self.update_params(|p| f(&mut p.circ_supply)) } + pub fn update_app_version(&mut self, f: F) + where + F: FnOnce(&mut u64), + { + self.update_params(|p| f(&mut p.app_version)) + } + /// Update the parameters and mark them as dirty. fn update_params(&mut self, f: F) where diff --git a/fendermint/vm/interpreter/src/fvm/state/genesis.rs b/fendermint/vm/interpreter/src/fvm/state/genesis.rs index 0cc634cbf..f43b6c4fd 100644 --- a/fendermint/vm/interpreter/src/fvm/state/genesis.rs +++ b/fendermint/vm/interpreter/src/fvm/state/genesis.rs @@ -153,6 +153,7 @@ where circ_supply, chain_id, power_scale, + app_version: 0, }; let exec_state = diff --git a/fendermint/vm/interpreter/src/fvm/state/snapshot.rs b/fendermint/vm/interpreter/src/fvm/state/snapshot.rs index c1aedfbb2..0801ade9e 100644 --- a/fendermint/vm/interpreter/src/fvm/state/snapshot.rs +++ b/fendermint/vm/interpreter/src/fvm/state/snapshot.rs @@ -374,6 +374,7 @@ mod tests { circ_supply: Default::default(), chain_id: 1024, power_scale: 0, + app_version: 0, }; let block_height = 2048; diff --git a/fendermint/vm/interpreter/src/fvm/upgrades.rs b/fendermint/vm/interpreter/src/fvm/upgrades.rs new file mode 100644 index 000000000..d4ae0ac05 --- /dev/null +++ b/fendermint/vm/interpreter/src/fvm/upgrades.rs @@ -0,0 +1,166 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use std::collections::BTreeMap; + +use anyhow::bail; +use fendermint_vm_core::chainid; +use fvm_ipld_blockstore::Blockstore; +use fvm_shared::chainid::ChainID; +use std::collections::btree_map::Entry::{Occupied, Vacant}; + +use super::state::{snapshot::BlockHeight, FvmExecState}; + +#[derive(PartialEq, Eq, Clone)] +struct UpgradeKey(ChainID, BlockHeight); + +impl PartialOrd for UpgradeKey { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for UpgradeKey { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + if self.0 == other.0 { + self.1.cmp(&other.1) + } else { + let chain_id: u64 = self.0.into(); + chain_id.cmp(&other.0.into()) + } + } +} + +/// a function type for migration +// TODO: Add missing parameters +pub type MigrationFunc = fn(state: &mut FvmExecState) -> anyhow::Result<()>; + +/// Upgrade represents a single upgrade to be executed at a given height +#[derive(Clone)] +pub struct Upgrade +where + DB: Blockstore + 'static + Clone, +{ + /// the chain name on which the upgrade should be executed + chain_name: String, + /// the chain id is calculated from the chain_name + chain_id: ChainID, + /// the block height at which the upgrade should be executed + block_height: BlockHeight, + /// the application version after the upgrade (or None if not affected) + new_app_version: Option, + /// the migration function to be executed + migration: MigrationFunc, +} + +impl Upgrade +where + DB: Blockstore + 'static + Clone, +{ + pub fn new( + chain_name: impl ToString, + block_height: BlockHeight, + new_app_version: Option, + migration: MigrationFunc, + ) -> anyhow::Result { + let mut upgrade = Self { + chain_name: chain_name.to_string(), + chain_id: 0.into(), + block_height, + new_app_version, + migration, + }; + + upgrade.chain_id = chainid::from_str_hashed(&upgrade.chain_name)?; + + Ok(upgrade) + } + + pub fn execute(&self, state: &mut FvmExecState) -> anyhow::Result> { + (self.migration)(state)?; + + Ok(self.new_app_version) + } +} + +/// UpgradeScheduler represents a list of upgrades to be executed at given heights +/// During each block height we check if there is an upgrade scheduled at that +/// height, and if so the migration for that upgrade is performed. +#[derive(Clone)] +pub struct UpgradeScheduler +where + DB: Blockstore + 'static + Clone, +{ + upgrades: BTreeMap>, +} + +impl Default for UpgradeScheduler +where + DB: Blockstore + 'static + Clone, +{ + fn default() -> Self { + Self::new() + } +} + +impl UpgradeScheduler +where + DB: Blockstore + 'static + Clone, +{ + pub fn new() -> Self { + Self { + upgrades: BTreeMap::new(), + } + } +} + +impl UpgradeScheduler +where + DB: Blockstore + 'static + Clone, +{ + // add a new upgrade to the schedule + pub fn add(&mut self, upgrade: Upgrade) -> anyhow::Result<()> { + match self + .upgrades + .entry(UpgradeKey(upgrade.chain_id, upgrade.block_height)) + { + Vacant(entry) => { + entry.insert(upgrade); + Ok(()) + } + Occupied(_) => { + bail!("Upgrade already exists"); + } + } + } + + // check if there is an upgrade scheduled for the given chain_id at a given height + pub fn get(&self, chain_id: ChainID, height: BlockHeight) -> Option<&Upgrade> { + self.upgrades.get(&UpgradeKey(chain_id, height)) + } +} + +#[test] +fn test_validate_upgrade_schedule() { + use crate::fvm::store::memory::MemoryBlockstore; + + let mut upgrade_scheduler: UpgradeScheduler = UpgradeScheduler::new(); + + let upgrade = Upgrade::new("mychain", 10, None, |_state| Ok(())).unwrap(); + upgrade_scheduler.add(upgrade).unwrap(); + + let upgrade = Upgrade::new("mychain", 20, None, |_state| Ok(())).unwrap(); + upgrade_scheduler.add(upgrade).unwrap(); + + // adding an upgrade with the same chain_id and height should fail + let upgrade = Upgrade::new("mychain", 20, None, |_state| Ok(())).unwrap(); + let res = upgrade_scheduler.add(upgrade); + assert!(res.is_err()); + + let mychain_id = chainid::from_str_hashed("mychain").unwrap(); + let otherhain_id = chainid::from_str_hashed("otherchain").unwrap(); + + assert!(upgrade_scheduler.get(mychain_id, 9).is_none()); + assert!(upgrade_scheduler.get(mychain_id, 10).is_some()); + assert!(upgrade_scheduler.get(otherhain_id, 10).is_none()); +} diff --git a/fendermint/vm/interpreter/src/lib.rs b/fendermint/vm/interpreter/src/lib.rs index c2be91c64..42dab76b8 100644 --- a/fendermint/vm/interpreter/src/lib.rs +++ b/fendermint/vm/interpreter/src/lib.rs @@ -7,6 +7,9 @@ pub mod chain; pub mod fvm; pub mod signed; +#[cfg(feature = "arb")] +mod arb; + /// Initialize the chain state. /// /// This could be from the original genesis file, or perhaps a checkpointed snapshot. diff --git a/fendermint/vm/interpreter/tests/golden.rs b/fendermint/vm/interpreter/tests/golden.rs new file mode 100644 index 000000000..898213c76 --- /dev/null +++ b/fendermint/vm/interpreter/tests/golden.rs @@ -0,0 +1,18 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +/// JSON based test in case we want to configure the FvmStateParams by hand. +mod json { + use fendermint_testing::golden_json; + use fendermint_vm_interpreter::fvm::state::FvmStateParams; + use quickcheck::Arbitrary; + golden_json! { "fvmstateparams/json", fvmstateparams, FvmStateParams::arbitrary } +} + +/// CBOR based tests in case we have to grab FvmStateParams from on-chain storage. +mod cbor { + use fendermint_testing::golden_cbor; + use fendermint_vm_interpreter::fvm::state::FvmStateParams; + use quickcheck::Arbitrary; + golden_cbor! { "fvmstateparams/cbor", fvmstateparams, FvmStateParams::arbitrary } +} diff --git a/fendermint/vm/snapshot/golden/manifest/cbor/manifest.cbor b/fendermint/vm/snapshot/golden/manifest/cbor/manifest.cbor index 381d598d2..6fedf6601 100644 --- a/fendermint/vm/snapshot/golden/manifest/cbor/manifest.cbor +++ b/fendermint/vm/snapshot/golden/manifest/cbor/manifest.cbor @@ -1 +1 @@ -a66c626c6f636b5f6865696768741a99e5f1a76473697a651b9a0ed59575887285666368756e6b731af71159e768636865636b73756d7840453243304636444136464643463335413334334546304545394445343436353436374143443530344338463237313243364142323034343543383341313932416c73746174655f706172616d73a76a73746174655f726f6f74d82a58230012202a0ab732b4e9d85ef7dc25303b64ab527c25a4d77815ebb579f396ec6caccad36974696d657374616d701bdf399b7bb39519486f6e6574776f726b5f76657273696f6e1affffffff68626173655f66656551005eb4d601fc663685053ee078217bd77f6b636972635f737570706c795100ffffffffffffffffb5b5b138edf6bed168636861696e5f69641b000a9a18ec6436676b706f7765725f7363616c65206776657273696f6e1a33c9bad2 \ No newline at end of file +a66c626c6f636b5f6865696768741aaf63f1256473697a6501666368756e6b731a9dccd06d68636865636b73756d7840453745444646454531453036313130303546303132393030464632323343383531443139303039374230373834333842394630303937373537363543323737366c73746174655f706172616d73a86a73746174655f726f6f74d82a58290001546a2414c603d8008bf6ca87951e4d3be4e399de9f7201c5950b64b8d15c97ece1419d9279f8e06974696d657374616d701b1ca40fb7fd8528ae6f6e6574776f726b5f76657273696f6e1affffffff68626173655f666565510066f81e0eb6eb840c626b805270573e9a6b636972635f737570706c7951008d080bf582d0b68e76374f3f6fdb537a68636861696e5f69641b0009b892ec558b9e6b706f7765725f7363616c65006b6170705f76657273696f6e006776657273696f6e1af0ee764e \ No newline at end of file diff --git a/fendermint/vm/snapshot/golden/manifest/cbor/manifest.txt b/fendermint/vm/snapshot/golden/manifest/cbor/manifest.txt index b8063523b..4775e7e0e 100644 --- a/fendermint/vm/snapshot/golden/manifest/cbor/manifest.txt +++ b/fendermint/vm/snapshot/golden/manifest/cbor/manifest.txt @@ -1 +1 @@ -SnapshotManifest { block_height: 2581983655, size: 11101044969413571205, chunks: 4145109479, checksum: Hash::Sha256(E2C0F6DA6FFCF35A343EF0EE9DE4465467ACD504C8F2712C6AB20445C83A192A), state_params: FvmStateParams { state_root: Cid(QmRAmJvPSFPjeHkVJyPktbmM2SRHURjbM7xs7JRD1zCjWJ), timestamp: Timestamp(16085058499726612808), network_version: NetworkVersion(4294967295), base_fee: TokenAmount(125886385631315495367.993087794916087679), circ_supply: TokenAmount(340282366920938463458.021429707776900817), chain_id: 2984181602989671, power_scale: -1 }, version: 868858578 } \ No newline at end of file +SnapshotManifest { block_height: 2942562597, size: 1, chunks: 2647445613, checksum: Hash::Sha256(E7EDFFEE1E0611005F012900FF223C851D190097B078438B9F009775765C2776), state_params: FvmStateParams { state_root: Cid(bafkgujauyyb5qael63fipfi6ju56jy4z32pxeaofsufwjogrlsl6zykbtwjht6ha), timestamp: Timestamp(2063791812149323950), network_version: NetworkVersion(4294967295), base_fee: TokenAmount(136869554829071433973.80013913682996393), circ_supply: TokenAmount(187462928338432242809.513020207012729722), chain_id: 2736215960161182, power_scale: 0, app_version: 0 }, version: 4042159694 } \ No newline at end of file diff --git a/fendermint/vm/snapshot/golden/manifest/json/manifest.json b/fendermint/vm/snapshot/golden/manifest/json/manifest.json index 9dd8357ec..0fec03c4f 100644 --- a/fendermint/vm/snapshot/golden/manifest/json/manifest.json +++ b/fendermint/vm/snapshot/golden/manifest/json/manifest.json @@ -10,7 +10,8 @@ "base_fee": "299246354255658060378714945246048246606", "circ_supply": "93362016975129332347987662062653906832", "chain_id": 503525136242505, - "power_scale": 0 + "power_scale": 0, + "app_version": 0 }, "version": 0 -} +} \ No newline at end of file diff --git a/fendermint/vm/snapshot/golden/manifest/json/manifest.txt b/fendermint/vm/snapshot/golden/manifest/json/manifest.txt index 4c37b8188..255a02aac 100644 --- a/fendermint/vm/snapshot/golden/manifest/json/manifest.txt +++ b/fendermint/vm/snapshot/golden/manifest/json/manifest.txt @@ -1 +1 @@ -SnapshotManifest { block_height: 18446744073709551615, size: 11344242012067624990, chunks: 22076, checksum: Hash::Sha256(A3B844BB3068947681E591126B1AAC925B7BF1BB56BA6DB77D87745365B0949E), state_params: FvmStateParams { state_root: Cid(QmYbxwhLej3Te1etMuFqWb3Gwy7CpVaXAe5deWmqrphMhg), timestamp: Timestamp(1), network_version: NetworkVersion(4294967295), base_fee: TokenAmount(299246354255658060378.714945246048246606), circ_supply: TokenAmount(93362016975129332347.987662062653906832), chain_id: 503525136242505, power_scale: 0 }, version: 0 } +SnapshotManifest { block_height: 18446744073709551615, size: 11344242012067624990, chunks: 22076, checksum: Hash::Sha256(A3B844BB3068947681E591126B1AAC925B7BF1BB56BA6DB77D87745365B0949E), state_params: FvmStateParams { state_root: Cid(QmYbxwhLej3Te1etMuFqWb3Gwy7CpVaXAe5deWmqrphMhg), timestamp: Timestamp(1), network_version: NetworkVersion(4294967295), base_fee: TokenAmount(299246354255658060378.714945246048246606), circ_supply: TokenAmount(93362016975129332347.987662062653906832), chain_id: 503525136242505, power_scale: 0, app_version: 0 }, version: 0 } \ No newline at end of file diff --git a/fendermint/vm/snapshot/src/manager.rs b/fendermint/vm/snapshot/src/manager.rs index 2624c20a1..495219067 100644 --- a/fendermint/vm/snapshot/src/manager.rs +++ b/fendermint/vm/snapshot/src/manager.rs @@ -318,6 +318,7 @@ mod tests { bundle::{bundle_path, contracts_path, custom_actors_bundle_path}, state::{snapshot::Snapshot, FvmGenesisState, FvmStateParams}, store::memory::MemoryBlockstore, + upgrades::UpgradeScheduler, FvmMessageInterpreter, }, GenesisInterpreter, @@ -456,8 +457,15 @@ mod tests { .await .expect("failed to create state"); - let interpreter = - FvmMessageInterpreter::new(mock_client(), None, contracts_path(), 1.05, 1.05, false); + let interpreter = FvmMessageInterpreter::new( + mock_client(), + None, + contracts_path(), + 1.05, + 1.05, + false, + UpgradeScheduler::new(), + ); let (state, out) = interpreter .init(state, genesis) @@ -474,6 +482,7 @@ mod tests { circ_supply: out.circ_supply, chain_id: out.chain_id.into(), power_scale: out.power_scale, + app_version: 0, }; (state_params, store) diff --git a/fendermint/vm/snapshot/src/manifest.rs b/fendermint/vm/snapshot/src/manifest.rs index 7784468ef..aea771718 100644 --- a/fendermint/vm/snapshot/src/manifest.rs +++ b/fendermint/vm/snapshot/src/manifest.rs @@ -189,6 +189,7 @@ mod arb { .unwrap() .into(), power_scale: *g.choose(&[-1, 0, 3]).unwrap(), + app_version: 0, }, version: Arbitrary::arbitrary(g), } From adcbc084972d662dddfdb70229928233dac67f42 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Wed, 6 Mar 2024 19:01:27 +0000 Subject: [PATCH 07/12] DEBUG: Test that revert data is returned (#774) --- fendermint/eth/api/examples/common/mod.rs | 15 ++++++++++ fendermint/eth/api/examples/ethers.rs | 28 +++++++++++++++---- fendermint/testing/contracts/SimpleCoin.abi | 2 +- fendermint/testing/contracts/SimpleCoin.bin | 2 +- .../testing/contracts/SimpleCoin.bin-runtime | 2 +- .../testing/contracts/SimpleCoin.signatures | 1 + fendermint/testing/contracts/SimpleCoin.sol | 8 ++++++ .../tests/docker_tests/root_only.rs | 2 +- 8 files changed, 50 insertions(+), 10 deletions(-) diff --git a/fendermint/eth/api/examples/common/mod.rs b/fendermint/eth/api/examples/common/mod.rs index d89a1f106..7e05dac9e 100644 --- a/fendermint/eth/api/examples/common/mod.rs +++ b/fendermint/eth/api/examples/common/mod.rs @@ -36,6 +36,9 @@ use fendermint_vm_actor_interface::eam::EthAddress; pub type TestMiddleware = SignerMiddleware, Wallet>; pub type TestContractCall = ContractCall, T>; +/// Gas limit to set for transactions. +pub const ENOUGH_GAS: u64 = 10_000_000_000u64; + pub struct TestAccount { pub secret_key: SecretKey, pub eth_addr: H160, @@ -107,10 +110,22 @@ where pub async fn prepare_call( mw: &TestMiddleware, mut call: TestContractCall, + prevent_estimation: bool, ) -> anyhow::Result> where C: JsonRpcClient + 'static, { + if prevent_estimation { + // Set the gas based on the testkit so it doesn't trigger estimation. + let tx = call.tx.as_eip1559_mut(); + let tx = tx.expect("eip1559"); + + tx.gas = Some(ENOUGH_GAS.into()); + tx.max_fee_per_gas = Some(0.into()); + tx.max_priority_fee_per_gas = Some(0.into()); + } + + // Fill in the missing fields like `from` and `nonce` (which involves querying the API). mw.fill_transaction(&mut call.tx, Some(BlockId::Number(BlockNumber::Latest))) .await .context("failed to fill transaction")?; diff --git a/fendermint/eth/api/examples/ethers.rs b/fendermint/eth/api/examples/ethers.rs index 944a7e874..4086db27e 100644 --- a/fendermint/eth/api/examples/ethers.rs +++ b/fendermint/eth/api/examples/ethers.rs @@ -28,9 +28,9 @@ use std::{fmt::Debug, path::PathBuf, sync::Arc}; -use anyhow::Context; +use anyhow::{bail, Context}; use clap::Parser; -use common::TestMiddleware; +use common::{TestMiddleware, ENOUGH_GAS}; use ethers::providers::StreamExt; use ethers::{ prelude::{abigen, ContractFactory}, @@ -54,9 +54,6 @@ use crate::common::{ mod common; -/// Gas limit to set for transactions. -const ENOUGH_GAS: u64 = 10_000_000_000u64; - /// Disabling filters helps when inspecting docker logs. The background data received for filters is rather noisy. const FILTERS_ENABLED: bool = true; @@ -530,7 +527,7 @@ where let contract = SimpleCoin::new(contract.address(), contract.client()); let coin_balance: TestContractCall<_, U256> = - prepare_call(&mw, contract.get_balance(from.eth_addr)).await?; + prepare_call(&mw, contract.get_balance(from.eth_addr), false).await?; request("eth_call", coin_balance.call().await, |coin_balance| { *coin_balance == U256::from(10000) @@ -545,6 +542,25 @@ where |coin_balance| *coin_balance == U256::from(10000), )?; + // Call a method that does a revert, to check that the message shows up in the return value. + // Try to send more than the available balance of 10,000 + let coin_send: TestContractCall<_, ()> = prepare_call( + &mw, + contract.send_coin_or_revert(to.eth_addr, U256::from(10000 * 10)), + true, + ) + .await + .context("failed to prepare revert call")?; + + match coin_send.call().await { + Ok(_) => bail!("call should failed with a revert"), + Err(e) => { + let e = e.to_string(); + assert!(e.contains("revert"), "should say revert"); + assert!(e.contains("0x08c379a"), "should have string selector"); + } + } + // We could calculate the storage location of the balance of the owner of the contract, // but let's just see what it returns with at slot 0. See an example at // https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_getstorageat diff --git a/fendermint/testing/contracts/SimpleCoin.abi b/fendermint/testing/contracts/SimpleCoin.abi index 19482937e..6184bf78b 100644 --- a/fendermint/testing/contracts/SimpleCoin.abi +++ b/fendermint/testing/contracts/SimpleCoin.abi @@ -1 +1 @@ -[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"_from","type":"address"},{"indexed":true,"internalType":"address","name":"_to","type":"address"},{"indexed":false,"internalType":"uint256","name":"_value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"getBalance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"getBalanceInEth","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"receiver","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"sendCoin","outputs":[{"internalType":"bool","name":"sufficient","type":"bool"}],"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file +[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"_from","type":"address"},{"indexed":true,"internalType":"address","name":"_to","type":"address"},{"indexed":false,"internalType":"uint256","name":"_value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"getBalance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"getBalanceInEth","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"receiver","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"sendCoin","outputs":[{"internalType":"bool","name":"sufficient","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"receiver","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"sendCoinOrRevert","outputs":[],"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file diff --git a/fendermint/testing/contracts/SimpleCoin.bin b/fendermint/testing/contracts/SimpleCoin.bin index dd452b7b3..31e0020ab 100644 --- a/fendermint/testing/contracts/SimpleCoin.bin +++ b/fendermint/testing/contracts/SimpleCoin.bin @@ -1 +1 @@ -608060405234801561001057600080fd5b506127106000803273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550610549806100656000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c80637bd703e81461004657806390b98a1114610076578063f8b2cb4f146100a6575b600080fd5b610060600480360381019061005b91906102d1565b6100d6565b60405161006d919061036f565b60405180910390f35b610090600480360381019061008b91906102fa565b6100f4565b60405161009d9190610354565b60405180910390f35b6100c060048036038101906100bb91906102d1565b61025f565b6040516100cd919061036f565b60405180910390f35b600060026100e38361025f565b6100ed91906103e0565b9050919050565b6000816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205410156101455760009050610259565b816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206000828254610193919061043a565b92505081905550816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546101e8919061038a565b925050819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8460405161024c919061036f565b60405180910390a3600190505b92915050565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b6000813590506102b6816104e5565b92915050565b6000813590506102cb816104fc565b92915050565b6000602082840312156102e357600080fd5b60006102f1848285016102a7565b91505092915050565b6000806040838503121561030d57600080fd5b600061031b858286016102a7565b925050602061032c858286016102bc565b9150509250929050565b61033f81610480565b82525050565b61034e816104ac565b82525050565b60006020820190506103696000830184610336565b92915050565b60006020820190506103846000830184610345565b92915050565b6000610395826104ac565b91506103a0836104ac565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff038211156103d5576103d46104b6565b5b828201905092915050565b60006103eb826104ac565b91506103f6836104ac565b9250817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff048311821515161561042f5761042e6104b6565b5b828202905092915050565b6000610445826104ac565b9150610450836104ac565b925082821015610463576104626104b6565b5b828203905092915050565b60006104798261048c565b9050919050565b60008115159050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6104ee8161046e565b81146104f957600080fd5b50565b610505816104ac565b811461051057600080fd5b5056fea2646970667358221220d11fa32f457b8122876db665f066909bc482ee02a7ea3972d5a2de406fecf85b64736f6c63430008020033 \ No newline at end of file +608060405234801561001057600080fd5b506127106000803273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055506106d5806100656000396000f3fe608060405234801561001057600080fd5b506004361061004c5760003560e01c8063587cb60c146100515780637bd703e81461006d57806390b98a111461009d578063f8b2cb4f146100cd575b600080fd5b61006b600480360381019061006691906103e3565b6100fd565b005b610087600480360381019061008291906103ba565b6101bf565b604051610094919061049b565b60405180910390f35b6100b760048036038101906100b291906103e3565b6101dd565b6040516100c49190610460565b60405180910390f35b6100e760048036038101906100e291906103ba565b610348565b6040516100f4919061049b565b60405180910390f35b806000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054101561017e576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101759061047b565b60405180910390fd5b61018882826101dd565b6101bb577f4e487b7100000000000000000000000000000000000000000000000000000000600052600160045260246000fd5b5050565b600060026101cc83610348565b6101d6919061051d565b9050919050565b6000816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054101561022e5760009050610342565b816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825461027c9190610577565b92505081905550816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546102d191906104c7565b925050819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef84604051610335919061049b565b60405180910390a3600190505b92915050565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b60008135905061039f81610671565b92915050565b6000813590506103b481610688565b92915050565b6000602082840312156103cc57600080fd5b60006103da84828501610390565b91505092915050565b600080604083850312156103f657600080fd5b600061040485828601610390565b9250506020610415858286016103a5565b9150509250929050565b610428816105bd565b82525050565b600061043b6022836104b6565b915061044682610622565b604082019050919050565b61045a816105e9565b82525050565b6000602082019050610475600083018461041f565b92915050565b600060208201905081810360008301526104948161042e565b9050919050565b60006020820190506104b06000830184610451565b92915050565b600082825260208201905092915050565b60006104d2826105e9565b91506104dd836105e9565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03821115610512576105116105f3565b5b828201905092915050565b6000610528826105e9565b9150610533836105e9565b9250817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff048311821515161561056c5761056b6105f3565b5b828202905092915050565b6000610582826105e9565b915061058d836105e9565b9250828210156105a05761059f6105f3565b5b828203905092915050565b60006105b6826105c9565b9050919050565b60008115159050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b7f73656e64657220646f65736e2774206861766520656e6f7567682062616c616e60008201527f6365000000000000000000000000000000000000000000000000000000000000602082015250565b61067a816105ab565b811461068557600080fd5b50565b610691816105e9565b811461069c57600080fd5b5056fea26469706673582212206674a6503608170616f4091f4e72e5564603a5c66be515dc32ff736f9ae7e0ff64736f6c63430008020033 \ No newline at end of file diff --git a/fendermint/testing/contracts/SimpleCoin.bin-runtime b/fendermint/testing/contracts/SimpleCoin.bin-runtime index 4cd6d5231..7907a61ec 100644 --- a/fendermint/testing/contracts/SimpleCoin.bin-runtime +++ b/fendermint/testing/contracts/SimpleCoin.bin-runtime @@ -1 +1 @@ -608060405234801561001057600080fd5b50600436106100415760003560e01c80637bd703e81461004657806390b98a1114610076578063f8b2cb4f146100a6575b600080fd5b610060600480360381019061005b91906102d1565b6100d6565b60405161006d919061036f565b60405180910390f35b610090600480360381019061008b91906102fa565b6100f4565b60405161009d9190610354565b60405180910390f35b6100c060048036038101906100bb91906102d1565b61025f565b6040516100cd919061036f565b60405180910390f35b600060026100e38361025f565b6100ed91906103e0565b9050919050565b6000816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205410156101455760009050610259565b816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206000828254610193919061043a565b92505081905550816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546101e8919061038a565b925050819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8460405161024c919061036f565b60405180910390a3600190505b92915050565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b6000813590506102b6816104e5565b92915050565b6000813590506102cb816104fc565b92915050565b6000602082840312156102e357600080fd5b60006102f1848285016102a7565b91505092915050565b6000806040838503121561030d57600080fd5b600061031b858286016102a7565b925050602061032c858286016102bc565b9150509250929050565b61033f81610480565b82525050565b61034e816104ac565b82525050565b60006020820190506103696000830184610336565b92915050565b60006020820190506103846000830184610345565b92915050565b6000610395826104ac565b91506103a0836104ac565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff038211156103d5576103d46104b6565b5b828201905092915050565b60006103eb826104ac565b91506103f6836104ac565b9250817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff048311821515161561042f5761042e6104b6565b5b828202905092915050565b6000610445826104ac565b9150610450836104ac565b925082821015610463576104626104b6565b5b828203905092915050565b60006104798261048c565b9050919050565b60008115159050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6104ee8161046e565b81146104f957600080fd5b50565b610505816104ac565b811461051057600080fd5b5056fea2646970667358221220d11fa32f457b8122876db665f066909bc482ee02a7ea3972d5a2de406fecf85b64736f6c63430008020033 \ No newline at end of file +608060405234801561001057600080fd5b506004361061004c5760003560e01c8063587cb60c146100515780637bd703e81461006d57806390b98a111461009d578063f8b2cb4f146100cd575b600080fd5b61006b600480360381019061006691906103e3565b6100fd565b005b610087600480360381019061008291906103ba565b6101bf565b604051610094919061049b565b60405180910390f35b6100b760048036038101906100b291906103e3565b6101dd565b6040516100c49190610460565b60405180910390f35b6100e760048036038101906100e291906103ba565b610348565b6040516100f4919061049b565b60405180910390f35b806000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054101561017e576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101759061047b565b60405180910390fd5b61018882826101dd565b6101bb577f4e487b7100000000000000000000000000000000000000000000000000000000600052600160045260246000fd5b5050565b600060026101cc83610348565b6101d6919061051d565b9050919050565b6000816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054101561022e5760009050610342565b816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825461027c9190610577565b92505081905550816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546102d191906104c7565b925050819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef84604051610335919061049b565b60405180910390a3600190505b92915050565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b60008135905061039f81610671565b92915050565b6000813590506103b481610688565b92915050565b6000602082840312156103cc57600080fd5b60006103da84828501610390565b91505092915050565b600080604083850312156103f657600080fd5b600061040485828601610390565b9250506020610415858286016103a5565b9150509250929050565b610428816105bd565b82525050565b600061043b6022836104b6565b915061044682610622565b604082019050919050565b61045a816105e9565b82525050565b6000602082019050610475600083018461041f565b92915050565b600060208201905081810360008301526104948161042e565b9050919050565b60006020820190506104b06000830184610451565b92915050565b600082825260208201905092915050565b60006104d2826105e9565b91506104dd836105e9565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03821115610512576105116105f3565b5b828201905092915050565b6000610528826105e9565b9150610533836105e9565b9250817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff048311821515161561056c5761056b6105f3565b5b828202905092915050565b6000610582826105e9565b915061058d836105e9565b9250828210156105a05761059f6105f3565b5b828203905092915050565b60006105b6826105c9565b9050919050565b60008115159050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b7f73656e64657220646f65736e2774206861766520656e6f7567682062616c616e60008201527f6365000000000000000000000000000000000000000000000000000000000000602082015250565b61067a816105ab565b811461068557600080fd5b50565b610691816105e9565b811461069c57600080fd5b5056fea26469706673582212206674a6503608170616f4091f4e72e5564603a5c66be515dc32ff736f9ae7e0ff64736f6c63430008020033 \ No newline at end of file diff --git a/fendermint/testing/contracts/SimpleCoin.signatures b/fendermint/testing/contracts/SimpleCoin.signatures index 965c82af9..b6971f11b 100644 --- a/fendermint/testing/contracts/SimpleCoin.signatures +++ b/fendermint/testing/contracts/SimpleCoin.signatures @@ -1,3 +1,4 @@ f8b2cb4f: getBalance(address) 7bd703e8: getBalanceInEth(address) 90b98a11: sendCoin(address,uint256) +587cb60c: sendCoinOrRevert(address,uint256) diff --git a/fendermint/testing/contracts/SimpleCoin.sol b/fendermint/testing/contracts/SimpleCoin.sol index 79e124754..f16c0170f 100644 --- a/fendermint/testing/contracts/SimpleCoin.sol +++ b/fendermint/testing/contracts/SimpleCoin.sol @@ -21,6 +21,14 @@ contract SimpleCoin { return true; } + function sendCoinOrRevert( + address receiver, + uint256 amount + ) public { + require(balances[msg.sender] >= amount, "sender doesn't have enough balance"); + assert(sendCoin(receiver, amount)); + } + function getBalanceInEth(address addr) public view returns (uint256) { return getBalance(addr) * 2; } diff --git a/fendermint/testing/materializer/tests/docker_tests/root_only.rs b/fendermint/testing/materializer/tests/docker_tests/root_only.rs index 45f4401c3..eb7c0f271 100644 --- a/fendermint/testing/materializer/tests/docker_tests/root_only.rs +++ b/fendermint/testing/materializer/tests/docker_tests/root_only.rs @@ -17,7 +17,7 @@ async fn test_full_node_sync() { with_testnet(MANIFEST, |_materializer, _manifest, testnet| { let test = async { // Allow a little bit of time for node-2 to catch up with node-1. - tokio::time::sleep(Duration::from_secs(1)).await; + tokio::time::sleep(Duration::from_secs(5)).await; // Check that node2 is following node1. let node2 = testnet.root().node("node-2"); let dnode2 = testnet.node(&node2)?; From 9fe1ee01e85ef430b77aceccc90a934ec947edee Mon Sep 17 00:00:00 2001 From: Jie Hou <143860049+mb1896@users.noreply.github.com> Date: Fri, 8 Mar 2024 01:09:59 +0800 Subject: [PATCH 08/12] Cache address-to-ActorType translation (#767) --- fendermint/eth/api/src/cache.rs | 103 ++++++++++++++++++++++++++++++++ fendermint/eth/api/src/state.rs | 30 ++++++++++ 2 files changed, 133 insertions(+) diff --git a/fendermint/eth/api/src/cache.rs b/fendermint/eth/api/src/cache.rs index 22ae42cd2..79b539710 100644 --- a/fendermint/eth/api/src/cache.rs +++ b/fendermint/eth/api/src/cache.rs @@ -3,7 +3,9 @@ use std::sync::{Arc, Mutex}; +use crate::state::ActorType; use anyhow::Context; +use cid::Cid; use fendermint_rpc::client::FendermintClient; use fendermint_rpc::query::QueryClient; use fendermint_vm_message::query::FvmQueryHeight; @@ -20,6 +22,8 @@ pub struct AddressCache { client: FendermintClient, addr_to_id: Arc>>, id_to_addr: Arc>>, + addr_to_actor_type: Arc>>, + cid_to_actor_type: Arc>>, } impl AddressCache @@ -31,6 +35,8 @@ where client, addr_to_id: Arc::new(Mutex::new(LruCache::with_capacity(capacity))), id_to_addr: Arc::new(Mutex::new(LruCache::with_capacity(capacity))), + addr_to_actor_type: Arc::new(Mutex::new(LruCache::with_capacity(capacity))), + cid_to_actor_type: Arc::new(Mutex::new(LruCache::with_capacity(capacity))), } } @@ -107,4 +113,101 @@ where let mut c = self.id_to_addr.lock().unwrap(); c.insert(id, addr); } + + pub fn set_actor_type_for_addr(&self, addr: Address, actor_type: ActorType) { + let mut c = self.addr_to_actor_type.lock().unwrap(); + c.insert(addr, actor_type); + } + + pub fn get_actor_type_from_addr(&self, addr: &Address) -> Option { + let mut c = self.addr_to_actor_type.lock().unwrap(); + c.get(addr).cloned() + } + + pub fn set_actor_type_for_cid(&self, cid: Cid, actor_type: ActorType) { + let mut c = self.cid_to_actor_type.lock().unwrap(); + c.insert(cid, actor_type); + } + + pub fn get_actor_type_from_cid(&self, cid: &Cid) -> Option { + let mut c = self.cid_to_actor_type.lock().unwrap(); + c.get(cid).cloned() + } +} + +#[cfg(test)] +mod tests { + use crate::cache::AddressCache; + use crate::state::ActorType; + use cid::Cid; + use fendermint_rpc::FendermintClient; + use fvm_shared::address::Address; + use std::str::FromStr; + use tendermint_rpc::MockClient; + + #[test] + fn test_read_and_write_addr_to_actor_type() { + let client = FendermintClient::new( + MockClient::new(tendermint_rpc::MockRequestMethodMatcher::default()).0, + ); + let addr_cache = AddressCache::new(client, 1000); + + let address1 = Address::from_str("f410fivboj67m6ut4j6xx3lhc426io22r7l3h6yha5bi").unwrap(); + let address2 = Address::from_str("f410fmpohbjcmznke3e7pbxomsbg5uae6o2sfjurchxa").unwrap(); + let address3 = Address::from_str("f410fxbfwpcrgbjg2ab6fevpoi4qlcfosw2vk5kzo5ga").unwrap(); + let address4 = Address::from_str("f410fggjevhgketpz6gw6ordusynlgcd5piyug4aomuq").unwrap(); + + addr_cache.set_actor_type_for_addr(address1, ActorType::EVM); + addr_cache.set_actor_type_for_addr(address2, ActorType::Unknown(Cid::default())); + addr_cache.set_actor_type_for_addr(address3, ActorType::Inexistent); + + assert_eq!( + addr_cache.get_actor_type_from_addr(&address1).unwrap(), + ActorType::EVM + ); + assert_eq!( + addr_cache.get_actor_type_from_addr(&address2).unwrap(), + ActorType::Unknown(Cid::default()) + ); + assert_eq!( + addr_cache.get_actor_type_from_addr(&address3).unwrap(), + ActorType::Inexistent + ); + assert_eq!(addr_cache.get_actor_type_from_addr(&address4), None); + } + + #[test] + fn test_read_and_write_cid_to_actor_type() { + let client = FendermintClient::new( + MockClient::new(tendermint_rpc::MockRequestMethodMatcher::default()).0, + ); + let addr_cache = AddressCache::new(client, 1000); + + let cid1 = Cid::from_str("bafk2bzacecmnyfiwb52tkbwmm2dsd7ysi3nvuxl3lmspy7pl26wxj4zj7w4wi") + .unwrap(); + let cid2 = Cid::from_str("bafy2bzaceas2zajrutdp7ugb6w2lpmow3z3klr3gzqimxtuz22tkkqallfch4") + .unwrap(); + let cid3 = Cid::from_str("k51qzi5uqu5dlvj2baxnqndepeb86cbk3ng7n3i46uzyxzyqj2xjonzllnv0v8") + .unwrap(); + let cid4 = + Cid::from_str("bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq").unwrap(); + + addr_cache.set_actor_type_for_cid(cid1, ActorType::EVM); + addr_cache.set_actor_type_for_cid(cid2, ActorType::Unknown(Cid::default())); + addr_cache.set_actor_type_for_cid(cid3, ActorType::Inexistent); + + assert_eq!( + addr_cache.get_actor_type_from_cid(&cid1).unwrap(), + ActorType::EVM + ); + assert_eq!( + addr_cache.get_actor_type_from_cid(&cid2).unwrap(), + ActorType::Unknown(Cid::default()) + ); + assert_eq!( + addr_cache.get_actor_type_from_cid(&cid3).unwrap(), + ActorType::Inexistent + ); + assert_eq!(addr_cache.get_actor_type_from_cid(&cid4), None); + } } diff --git a/fendermint/eth/api/src/state.rs b/fendermint/eth/api/src/state.rs index 09e80bd3f..798d85ed1 100644 --- a/fendermint/eth/api/src/state.rs +++ b/fendermint/eth/api/src/state.rs @@ -442,6 +442,16 @@ where height: FvmQueryHeight, ) -> JsonRpcResult { let addr = to_fvm_address(*address); + + if let Some(actor_type) = self.addr_cache.get_actor_type_from_addr(&addr) { + tracing::debug!( + ?addr, + ?actor_type, + "addr cache hit, directly return the actor type" + ); + return Ok(actor_type); + } + let Some(( _, ActorState { @@ -452,11 +462,31 @@ where else { return Ok(ActorType::Inexistent); }; + + if let Some(actor_type) = self.addr_cache.get_actor_type_from_cid(&actor_type_cid) { + tracing::debug!( + ?actor_type_cid, + ?actor_type, + "cid cache hit, directly return the actor type" + ); + tracing::debug!(?addr, ?actor_type, "put result into addr cache"); + self.addr_cache + .set_actor_type_for_addr(addr, actor_type.clone()); + return Ok(actor_type); + } + let registry = self.client.builtin_actors(height).await?.value.registry; let ret = match registry.into_iter().find(|(_, cid)| cid == &actor_type_cid) { Some((typ, _)) => ActorType::Known(Cow::Owned(typ)), None => ActorType::Unknown(actor_type_cid), }; + + tracing::debug!(?actor_type_cid, ?ret, "put result into cid cache"); + self.addr_cache + .set_actor_type_for_cid(actor_type_cid, ret.clone()); + tracing::debug!(?addr, ?ret, "put result into addr cache"); + self.addr_cache.set_actor_type_for_addr(addr, ret.clone()); + Ok(ret) } } From d28073b6d5d92871be48d44a5ab5c35abd5bbddb Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Mon, 11 Mar 2024 10:12:02 +0000 Subject: [PATCH 09/12] FIX: Add a retry to result fetching in filters (#781) --- fendermint/eth/api/src/conv/from_tm.rs | 4 +-- fendermint/eth/api/src/filters.rs | 36 +++++++++++++++++++++++++- fendermint/eth/api/src/state.rs | 4 +-- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/fendermint/eth/api/src/conv/from_tm.rs b/fendermint/eth/api/src/conv/from_tm.rs index f89d0a400..2721f8dbc 100644 --- a/fendermint/eth/api/src/conv/from_tm.rs +++ b/fendermint/eth/api/src/conv/from_tm.rs @@ -99,7 +99,7 @@ pub fn is_block_zero(block: &tendermint::Block) -> bool { /// Convert a Tendermint block to Ethereum with only the block hashes in the body. pub fn to_eth_block( - block: tendermint::Block, + block: &tendermint::Block, block_results: tendermint_rpc::endpoint::block_results::Response, base_fee: TokenAmount, chain_id: ChainID, @@ -449,7 +449,7 @@ pub fn to_eth_block_zero(block: tendermint::Block) -> anyhow::Result> = map_rpc_block_txs(block, |tx| Ok(tx.hash())); block @@ -634,6 +640,34 @@ fn notification(subscription: FilterId, result: serde_json::Value) -> MethodNoti } } +/// It looks like it might not be true that when we receive a `NewBlock` event from Tendermint, +/// (which includes begin and end events, so we can assume it's been executed), then it's safe +/// to query the API for the block results, which is what `enrich_block` does to fill stuff +/// like gas used, etc. +async fn enrich_block_with_retry( + client: &FendermintClient, + block: &tendermint::block::Block, +) -> JsonRpcResult> { + // TODO: Assuming at ~1 block time; move this to config. + const SLEEP_SECS: u64 = 1; + const MAX_ATTEMPT: u32 = 5; + let mut attempt = 0; + loop { + match enrich_block(client, block).await { + Err(e) if attempt < MAX_ATTEMPT => { + tracing::debug!( + error = e.to_string(), + height = block.header().height.value(), + "failed to enrich block; retrying..." + ); + tokio::time::sleep(Duration::from_secs(SLEEP_SECS)).await; + attempt += 1; + } + other => return other, + } + } +} + impl PollState { /// Take all the accumulated changes. /// diff --git a/fendermint/eth/api/src/state.rs b/fendermint/eth/api/src/state.rs index 798d85ed1..b3db91b5e 100644 --- a/fendermint/eth/api/src/state.rs +++ b/fendermint/eth/api/src/state.rs @@ -303,7 +303,7 @@ where where C: Client + Sync + Send, { - let block = enrich_block(&self.client, block).await?; + let block = enrich_block(&self.client, &block).await?; let block = if full_tx { map_rpc_block_txs(block, serde_json::to_value).context("failed to convert to JSON")? @@ -609,7 +609,7 @@ impl JsonRpcState { pub async fn enrich_block( client: &FendermintClient, - block: tendermint::Block, + block: &tendermint::Block, ) -> JsonRpcResult> where C: Client + Sync + Send, From e7fdce2c119978c011437d3d0155e12a02b9136f Mon Sep 17 00:00:00 2001 From: Mikers Date: Wed, 13 Mar 2024 11:23:31 -1000 Subject: [PATCH 10/12] Fix Gateway & Registry Diamond Upgrade script (#780) - use updated config format for ganache test net and use isolated port to avoid collisions - quiet down ganache logging --- contracts/scripts/util.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/contracts/scripts/util.ts b/contracts/scripts/util.ts index ea03d797a..c28a15c0b 100644 --- a/contracts/scripts/util.ts +++ b/contracts/scripts/util.ts @@ -9,6 +9,8 @@ const fs = require('fs') export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' +const isolatedPort = 18678 + export async function deployContractWithDeployer( deployer: SignerWithAddress, contractName: string, @@ -89,9 +91,11 @@ export async function getFacets(diamondAddress: string): Promise { async function startGanache() { return new Promise((resolve, reject) => { const server = ganache.server({ - gasPrice: '0x0', // Set gas price to 0 + miner: { defaultGasPrice: '0x0' }, + chain: { hardfork: 'berlin' }, + logging: { quiet: true }, }) - server.listen(8545, (err) => { + server.listen(isolatedPort, (err) => { if (err) reject(err) else resolve(server) }) @@ -114,16 +118,16 @@ export async function getRuntimeBytecode(bytecode) { } const ganacheServer = await startGanache() - const provider = new providers.JsonRpcProvider('http://127.0.0.1:8545') + const provider = new providers.JsonRpcProvider( + `http://127.0.0.1:${isolatedPort}`, + ) const wallet = new Wallet(process.env.PRIVATE_KEY, provider) const contractFactory = new ContractFactory([], bytecode, wallet) - const contract = await contractFactory.deploy() + const contract = await contractFactory.deploy({ gasPrice: 0 }) await contract.deployed() const runtimeBytecode = await provider.getCode(contract.address) - - await stopGanache(ganacheServer) - + stopGanache(ganacheServer) return runtimeBytecode } From 0babe1bb9c5a5c96e83179b382102dc9587eb53d Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Thu, 14 Mar 2024 20:18:07 +0800 Subject: [PATCH 11/12] Set subnet creator as owner + add Ownable facet to contracts (#785) Co-authored-by: raulk Co-authored-by: raulk --- .github/workflows/contracts-sast.yaml | 2 +- contracts/.gitignore | 1 + contracts/audit-resolve.json | 4 + contracts/scripts/deploy-gateway.template.ts | 1 + contracts/scripts/deploy-registry.template.ts | 1 + contracts/scripts/deploy-sa-diamond.ts | 1 + .../scripts/python/build_selector_library.py | 1 + contracts/src/OwnershipFacet.sol | 14 ++++ contracts/src/SubnetActorDiamond.sol | 5 +- contracts/src/SubnetRegistryDiamond.sol | 1 + contracts/src/lib/LibDiamond.sol | 17 +++++ .../subnetregistry/RegisterSubnetFacet.sol | 2 +- contracts/test/IntegrationTestBase.sol | 73 ++++++++++++++++--- .../test/helpers/GatewayFacetsHelper.sol | 10 +++ contracts/test/helpers/SelectorLibrary.sol | 7 ++ .../test/integration/GatewayDiamond.t.sol | 18 ++++- .../test/integration/SubnetActorDiamond.t.sol | 7 +- .../test/integration/SubnetRegistry.t.sol | 2 +- contracts/tools/check_aderyn.sh | 60 ++++++++++----- 19 files changed, 189 insertions(+), 38 deletions(-) create mode 100644 contracts/src/OwnershipFacet.sol diff --git a/.github/workflows/contracts-sast.yaml b/.github/workflows/contracts-sast.yaml index fa2791e2d..04067a6d3 100644 --- a/.github/workflows/contracts-sast.yaml +++ b/.github/workflows/contracts-sast.yaml @@ -42,7 +42,7 @@ jobs: run: cargo install aderyn - name: Run aderyn - run: cd contracts && aderyn ./ + run: cd contracts && aderyn ./ -o report.json - name: Check results run: cd contracts && ./tools/check_aderyn.sh diff --git a/contracts/.gitignore b/contracts/.gitignore index e822d26d4..84ef71d4e 100644 --- a/contracts/.gitignore +++ b/contracts/.gitignore @@ -31,6 +31,7 @@ lcov.info # Aderyn scanner report.md +report.json #vim *.un~ diff --git a/contracts/audit-resolve.json b/contracts/audit-resolve.json index faa5cf2af..e85dcdfab 100644 --- a/contracts/audit-resolve.json +++ b/contracts/audit-resolve.json @@ -55,6 +55,10 @@ "1096544|json5": { "decision": "ignore", "madeAt": 1708963077846 + }, + "1096644|browserify-sign": { + "decision": "ignore", + "madeAt": 1710415772020 } }, "rules": {}, diff --git a/contracts/scripts/deploy-gateway.template.ts b/contracts/scripts/deploy-gateway.template.ts index 5200c668c..035aa21d9 100644 --- a/contracts/scripts/deploy-gateway.template.ts +++ b/contracts/scripts/deploy-gateway.template.ts @@ -92,6 +92,7 @@ export async function deploy(libs: { [key in string]: string }) { libs: xnetMessagingFacetLibs, }, { name: 'TopDownFinalityFacet', libs: topDownFinalityFacetLibs }, + { name: 'OwnershipFacet', libs: {} }, ] for (const facet of facets) { diff --git a/contracts/scripts/deploy-registry.template.ts b/contracts/scripts/deploy-registry.template.ts index 80cac756d..f77b1a3a8 100644 --- a/contracts/scripts/deploy-registry.template.ts +++ b/contracts/scripts/deploy-registry.template.ts @@ -90,6 +90,7 @@ export async function deploy() { { name: 'SubnetGetterFacet', libs: {} }, { name: 'DiamondLoupeFacet', libs: {} }, { name: 'DiamondCutFacet', libs: {} }, + { name: 'OwnershipFacet', libs: {} }, ] for (const facet of facets) { diff --git a/contracts/scripts/deploy-sa-diamond.ts b/contracts/scripts/deploy-sa-diamond.ts index 0cc5fa439..60dc56a90 100644 --- a/contracts/scripts/deploy-sa-diamond.ts +++ b/contracts/scripts/deploy-sa-diamond.ts @@ -42,6 +42,7 @@ async function deploySubnetActorDiamond( { name: 'SubnetActorRewardFacet', libs: rewarderFacetLibs }, { name: 'SubnetActorCheckpointingFacet', libs: checkpointerFacetLibs }, { name: 'SubnetActorPauseFacet', libs: pauserFacetLibs }, + { name: 'OwnershipFacet', libs: {} }, ] // The `facetCuts` variable is the FacetCut[] that contains the functions to add during diamond deployment const facetCuts = [] diff --git a/contracts/scripts/python/build_selector_library.py b/contracts/scripts/python/build_selector_library.py index ee8b33fb8..2b0ebd4d9 100644 --- a/contracts/scripts/python/build_selector_library.py +++ b/contracts/scripts/python/build_selector_library.py @@ -70,6 +70,7 @@ def main(): 'src/GatewayDiamond.sol', 'src/SubnetActorDiamond.sol', 'src/SubnetRegistryDiamond.sol', + 'src/OwnershipFacet.sol', 'src/diamond/DiamondCutFacet.sol', 'src/diamond/DiamondLoupeFacet.sol', 'src/gateway/GatewayGetterFacet.sol', diff --git a/contracts/src/OwnershipFacet.sol b/contracts/src/OwnershipFacet.sol new file mode 100644 index 000000000..e321661a9 --- /dev/null +++ b/contracts/src/OwnershipFacet.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {LibDiamond} from "./lib/LibDiamond.sol"; + +contract OwnershipFacet { + function transferOwnership(address _newOwner) external { + LibDiamond.transferOwnership(_newOwner); + } + + function owner() external view returns (address owner_) { + owner_ = LibDiamond.contractOwner(); + } +} diff --git a/contracts/src/SubnetActorDiamond.sol b/contracts/src/SubnetActorDiamond.sol index bde799f13..178e75d74 100644 --- a/contracts/src/SubnetActorDiamond.sol +++ b/contracts/src/SubnetActorDiamond.sol @@ -15,7 +15,6 @@ import {SubnetIDHelper} from "./lib/SubnetIDHelper.sol"; import {LibStaking} from "./lib/LibStaking.sol"; import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol"; import {SupplySourceHelper} from "./lib/SupplySourceHelper.sol"; - error FunctionNotFound(bytes4 _functionSelector); contract SubnetActorDiamond { @@ -38,7 +37,7 @@ contract SubnetActorDiamond { SubnetID parentId; } - constructor(IDiamond.FacetCut[] memory _diamondCut, ConstructorParams memory params) { + constructor(IDiamond.FacetCut[] memory _diamondCut, ConstructorParams memory params, address owner) { if (params.ipcGatewayAddr == address(0)) { revert GatewayCannotBeZero(); } @@ -58,7 +57,7 @@ contract SubnetActorDiamond { params.supplySource.validate(); - LibDiamond.setContractOwner(msg.sender); + LibDiamond.setContractOwner(owner); LibDiamond.diamondCut({_diamondCut: _diamondCut, _init: address(0), _calldata: new bytes(0)}); LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage(); diff --git a/contracts/src/SubnetRegistryDiamond.sol b/contracts/src/SubnetRegistryDiamond.sol index 244b5fcfb..5b7337e15 100644 --- a/contracts/src/SubnetRegistryDiamond.sol +++ b/contracts/src/SubnetRegistryDiamond.sol @@ -8,6 +8,7 @@ import {IERC165} from "./interfaces/IERC165.sol"; import {SubnetRegistryActorStorage} from "./lib/LibSubnetRegistryStorage.sol"; import {GatewayCannotBeZero, FacetCannotBeZero} from "./errors/IPCErrors.sol"; import {LibDiamond} from "./lib/LibDiamond.sol"; + error FunctionNotFound(bytes4 _functionSelector); contract SubnetRegistryDiamond { diff --git a/contracts/src/lib/LibDiamond.sol b/contracts/src/lib/LibDiamond.sol index 8cd100b61..e60f520c2 100644 --- a/contracts/src/lib/LibDiamond.sol +++ b/contracts/src/lib/LibDiamond.sol @@ -7,6 +7,7 @@ import {IDiamond} from "../interfaces/IDiamond.sol"; library LibDiamond { bytes32 public constant DIAMOND_STORAGE_POSITION = keccak256("libdiamond.lib.diamond.storage"); + error InvalidAddress(); error NotOwner(); error NoBytecodeAtAddress(address _contractAddress, string _message); error IncorrectFacetCutAction(IDiamondCut.FacetCutAction _action); @@ -24,6 +25,8 @@ library LibDiamond { error CannotRemoveFunctionThatDoesNotExist(bytes4 _selector); error CannotRemoveImmutableFunction(bytes4 _selector); + event OwnershipTransferred(address oldOwner, address newOwner); + struct FacetAddressAndSelectorPosition { address facetAddress; uint16 selectorPosition; @@ -38,6 +41,17 @@ library LibDiamond { address contractOwner; } + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Can only be called by the current owner. + */ + function transferOwnership(address newOwner) internal onlyOwner { + if (newOwner == address(0)) { + revert InvalidAddress(); + } + setContractOwner(newOwner); + } + function diamondStorage() internal pure returns (DiamondStorage storage ds) { bytes32 position = DIAMOND_STORAGE_POSITION; assembly { @@ -47,7 +61,10 @@ library LibDiamond { function setContractOwner(address _newOwner) internal { DiamondStorage storage ds = diamondStorage(); + + address oldOwner = ds.contractOwner; ds.contractOwner = _newOwner; + emit OwnershipTransferred(oldOwner, _newOwner); } function contractOwner() internal view returns (address contractOwner_) { diff --git a/contracts/src/subnetregistry/RegisterSubnetFacet.sol b/contracts/src/subnetregistry/RegisterSubnetFacet.sol index c3480272c..9fcb31c59 100644 --- a/contracts/src/subnetregistry/RegisterSubnetFacet.sol +++ b/contracts/src/subnetregistry/RegisterSubnetFacet.sol @@ -58,7 +58,7 @@ contract RegisterSubnetFacet is ReentrancyGuard { }); // slither-disable-next-line reentrancy-benign - subnetAddr = address(new SubnetActorDiamond(diamondCut, _params)); + subnetAddr = address(new SubnetActorDiamond(diamondCut, _params, msg.sender)); //nonces start with 1, similar to eip 161 ++s.userNonces[msg.sender]; diff --git a/contracts/test/IntegrationTestBase.sol b/contracts/test/IntegrationTestBase.sol index e71c575d2..9e72a0db0 100644 --- a/contracts/test/IntegrationTestBase.sol +++ b/contracts/test/IntegrationTestBase.sol @@ -35,6 +35,8 @@ import {SubnetRegistryDiamond} from "../src/SubnetRegistryDiamond.sol"; import {RegisterSubnetFacet} from "../src/subnetregistry/RegisterSubnetFacet.sol"; import {SubnetGetterFacet} from "../src/subnetregistry/SubnetGetterFacet.sol"; +import {OwnershipFacet} from "../src/OwnershipFacet.sol"; + import {DiamondLoupeFacet} from "../src/diamond/DiamondLoupeFacet.sol"; import {DiamondCutFacet} from "../src/diamond/DiamondCutFacet.sol"; import {SupplySourceHelper} from "../src/lib/SupplySourceHelper.sol"; @@ -89,18 +91,21 @@ contract TestRegistry is Test, TestParams { bytes4[] registerSubnetGetterFacetSelectors; bytes4[] registerCutterSelectors; bytes4[] registerLouperSelectors; + bytes4[] registerOwnershipSelectors; SubnetRegistryDiamond registryDiamond; DiamondLoupeFacet registryLouper; DiamondCutFacet registryCutter; RegisterSubnetFacet registrySubnetFacet; SubnetGetterFacet registrySubnetGetterFacet; + OwnershipFacet ownershipFacet; constructor() { registerSubnetFacetSelectors = SelectorLibrary.resolveSelectors("RegisterSubnetFacet"); registerSubnetGetterFacetSelectors = SelectorLibrary.resolveSelectors("SubnetGetterFacet"); registerCutterSelectors = SelectorLibrary.resolveSelectors("DiamondCutFacet"); registerLouperSelectors = SelectorLibrary.resolveSelectors("DiamondLoupeFacet"); + registerOwnershipSelectors = SelectorLibrary.resolveSelectors("OwnershipFacet"); } } @@ -116,6 +121,8 @@ contract TestGatewayActor is Test, TestParams { bytes4[] gwCutterSelectors; bytes4[] gwLoupeSelectors; + bytes4[] gwOwnershipSelectors; + GatewayDiamond gatewayDiamond; constructor() { @@ -128,6 +135,8 @@ contract TestGatewayActor is Test, TestParams { gwMessengerSelectors = SelectorLibrary.resolveSelectors("GatewayMessengerFacet"); gwCutterSelectors = SelectorLibrary.resolveSelectors("DiamondCutFacet"); gwLoupeSelectors = SelectorLibrary.resolveSelectors("DiamondLoupeFacet"); + + gwOwnershipSelectors = SelectorLibrary.resolveSelectors("OwnershipFacet"); } } @@ -140,6 +149,7 @@ contract TestSubnetActor is Test, TestParams { bytes4[] saManagerMockedSelectors; bytes4[] saCutterSelectors; bytes4[] saLouperSelectors; + bytes4[] saOwnershipSelectors; SubnetActorDiamond saDiamond; SubnetActorMock saMock; @@ -153,6 +163,7 @@ contract TestSubnetActor is Test, TestParams { saManagerMockedSelectors = SelectorLibrary.resolveSelectors("SubnetActorMock"); saCutterSelectors = SelectorLibrary.resolveSelectors("DiamondCutFacet"); saLouperSelectors = SelectorLibrary.resolveSelectors("DiamondLoupeFacet"); + saOwnershipSelectors = SelectorLibrary.resolveSelectors("OwnershipFacet"); } function defaultSubnetActorParamsWith( @@ -285,8 +296,9 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, GatewayMessengerFacet messenger = new GatewayMessengerFacet(); DiamondCutFacet cutter = new DiamondCutFacet(); DiamondLoupeFacet louper = new DiamondLoupeFacet(); + OwnershipFacet ownership = new OwnershipFacet(); - IDiamond.FacetCut[] memory gwDiamondCut = new IDiamond.FacetCut[](8); + IDiamond.FacetCut[] memory gwDiamondCut = new IDiamond.FacetCut[](9); gwDiamondCut[0] = ( IDiamond.FacetCut({ @@ -352,6 +364,13 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, }) ); + gwDiamondCut[8] = ( + IDiamond.FacetCut({ + facetAddress: address(ownership), + action: IDiamond.FacetCutAction.Add, + functionSelectors: gwOwnershipSelectors + }) + ); gatewayDiamond = new GatewayDiamond(gwDiamondCut, params); return gatewayDiamond; @@ -363,9 +382,10 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, address manager, address pauser, address rewarder, - address checkpointer + address checkpointer, + address ownership ) public returns (SubnetActorDiamond) { - IDiamond.FacetCut[] memory diamondCut = new IDiamond.FacetCut[](5); + IDiamond.FacetCut[] memory diamondCut = new IDiamond.FacetCut[](6); diamondCut[0] = ( IDiamond.FacetCut({ @@ -407,7 +427,15 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, }) ); - saDiamond = new SubnetActorDiamond(diamondCut, params); + diamondCut[5] = ( + IDiamond.FacetCut({ + facetAddress: ownership, + action: IDiamond.FacetCutAction.Add, + functionSelectors: saOwnershipSelectors + }) + ); + + saDiamond = new SubnetActorDiamond(diamondCut, params, address(this)); return saDiamond; } @@ -419,8 +447,9 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, SubnetActorCheckpointingFacet checkpointer = new SubnetActorCheckpointingFacet(); DiamondLoupeFacet louper = new DiamondLoupeFacet(); DiamondCutFacet cutter = new DiamondCutFacet(); + OwnershipFacet ownership = new OwnershipFacet(); - IDiamond.FacetCut[] memory diamondCut = new IDiamond.FacetCut[](7); + IDiamond.FacetCut[] memory diamondCut = new IDiamond.FacetCut[](8); diamondCut[0] = ( IDiamond.FacetCut({ @@ -478,7 +507,15 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, }) ); - SubnetActorDiamond diamond = new SubnetActorDiamond(diamondCut, params); + diamondCut[7] = ( + IDiamond.FacetCut({ + facetAddress: address(ownership), + action: IDiamond.FacetCutAction.Add, + functionSelectors: saOwnershipSelectors + }) + ); + + SubnetActorDiamond diamond = new SubnetActorDiamond(diamondCut, params, address(this)); return diamond; } @@ -534,8 +571,9 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, function createMockedSubnetActorWithGateway(address gw) public returns (SubnetActorDiamond) { SubnetActorMock mockedManager = new SubnetActorMock(); SubnetActorGetterFacet getter = new SubnetActorGetterFacet(); + OwnershipFacet ownership = new OwnershipFacet(); - IDiamond.FacetCut[] memory diamondCut = new IDiamond.FacetCut[](2); + IDiamond.FacetCut[] memory diamondCut = new IDiamond.FacetCut[](3); diamondCut[0] = ( IDiamond.FacetCut({ @@ -553,9 +591,17 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, }) ); + diamondCut[2] = ( + IDiamond.FacetCut({ + facetAddress: address(ownership), + action: IDiamond.FacetCutAction.Add, + functionSelectors: saOwnershipSelectors + }) + ); + SubnetActorDiamond.ConstructorParams memory params = defaultSubnetActorParamsWith(gw); - SubnetActorDiamond d = new SubnetActorDiamond(diamondCut, params); + SubnetActorDiamond d = new SubnetActorDiamond(diamondCut, params, address(this)); return d; } @@ -564,12 +610,13 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, function createSubnetRegistry( SubnetRegistryDiamond.ConstructorParams memory params ) public returns (SubnetRegistryDiamond) { - IDiamond.FacetCut[] memory diamondCut = new IDiamond.FacetCut[](4); + IDiamond.FacetCut[] memory diamondCut = new IDiamond.FacetCut[](5); DiamondCutFacet regCutFacet = new DiamondCutFacet(); DiamondLoupeFacet regLoupeFacet = new DiamondLoupeFacet(); RegisterSubnetFacet regSubnetFacet = new RegisterSubnetFacet(); SubnetGetterFacet regGetterFacet = new SubnetGetterFacet(); + OwnershipFacet ownership = new OwnershipFacet(); diamondCut[0] = ( IDiamond.FacetCut({ @@ -600,6 +647,14 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, }) ); + diamondCut[4] = ( + IDiamond.FacetCut({ + facetAddress: address(ownership), + action: IDiamond.FacetCutAction.Add, + functionSelectors: registerOwnershipSelectors + }) + ); + SubnetRegistryDiamond newSubnetRegistry = new SubnetRegistryDiamond(diamondCut, params); emit SubnetRegistryCreated(address(newSubnetRegistry)); return newSubnetRegistry; diff --git a/contracts/test/helpers/GatewayFacetsHelper.sol b/contracts/test/helpers/GatewayFacetsHelper.sol index dfb4efd83..2aed980f9 100644 --- a/contracts/test/helpers/GatewayFacetsHelper.sol +++ b/contracts/test/helpers/GatewayFacetsHelper.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.23; +import {OwnershipFacet} from "../../src/OwnershipFacet.sol"; import {GatewayGetterFacet} from "../../src/gateway/GatewayGetterFacet.sol"; import {GatewayManagerFacet} from "../../src/gateway/GatewayManagerFacet.sol"; import {GatewayMessengerFacet} from "../../src/gateway/GatewayMessengerFacet.sol"; @@ -12,6 +13,11 @@ import {DiamondLoupeFacet} from "../../src/diamond/DiamondLoupeFacet.sol"; import {DiamondCutFacet} from "../../src/diamond/DiamondCutFacet.sol"; library GatewayFacetsHelper { + function ownership(address gw) internal pure returns (OwnershipFacet) { + OwnershipFacet facet = OwnershipFacet(gw); + return facet; + } + function getter(address gw) internal pure returns (GatewayGetterFacet) { GatewayGetterFacet facet = GatewayGetterFacet(gw); return facet; @@ -43,6 +49,10 @@ library GatewayFacetsHelper { } // + function ownership(GatewayDiamond gw) internal pure returns (OwnershipFacet) { + OwnershipFacet facet = OwnershipFacet(address(gw)); + return facet; + } function getter(GatewayDiamond gw) internal pure returns (GatewayGetterFacet) { GatewayGetterFacet facet = GatewayGetterFacet(address(gw)); diff --git a/contracts/test/helpers/SelectorLibrary.sol b/contracts/test/helpers/SelectorLibrary.sol index 1cfe63591..b068ae5c3 100644 --- a/contracts/test/helpers/SelectorLibrary.sol +++ b/contracts/test/helpers/SelectorLibrary.sol @@ -24,6 +24,13 @@ library SelectorLibrary { (bytes4[]) ); } + if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("OwnershipFacet"))) { + return + abi.decode( + hex"000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000028da5cb5b00000000000000000000000000000000000000000000000000000000f2fde38b00000000000000000000000000000000000000000000000000000000", + (bytes4[]) + ); + } if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("DiamondCutFacet"))) { return abi.decode( diff --git a/contracts/test/integration/GatewayDiamond.t.sol b/contracts/test/integration/GatewayDiamond.t.sol index 62562af54..2ecdd66cf 100644 --- a/contracts/test/integration/GatewayDiamond.t.sol +++ b/contracts/test/integration/GatewayDiamond.t.sol @@ -50,6 +50,22 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase { super.setUp(); } + function testGatewayDiamond_TransferOwnership() public { + address owner = gatewayDiamond.ownership().owner(); + + vm.expectRevert(LibDiamond.InvalidAddress.selector); + gatewayDiamond.ownership().transferOwnership(address(0)); + + gatewayDiamond.ownership().transferOwnership(address(1)); + + address newOwner = gatewayDiamond.ownership().owner(); + require(owner != newOwner, "ownership should be updated"); + require(newOwner == address(1), "new owner not address 1"); + + vm.expectRevert(LibDiamond.NotOwner.selector); + gatewayDiamond.ownership().transferOwnership(address(1)); + } + function testGatewayDiamond_Constructor() public view { require(gatewayDiamond.getter().totalSubnets() == 0, "unexpected totalSubnets"); require(gatewayDiamond.getter().bottomUpNonce() == 0, "unexpected bottomUpNonce"); @@ -85,7 +101,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase { } function testGatewayDiamond_LoupeFunction() public view { - require(gatewayDiamond.diamondLouper().facets().length == 8, "unexpected length"); + require(gatewayDiamond.diamondLouper().facets().length == 9, "unexpected length"); require( gatewayDiamond.diamondLouper().supportsInterface(type(IERC165).interfaceId) == true, "IERC165 not supported" diff --git a/contracts/test/integration/SubnetActorDiamond.t.sol b/contracts/test/integration/SubnetActorDiamond.t.sol index e74f2e5b4..17b3f0d2a 100644 --- a/contracts/test/integration/SubnetActorDiamond.t.sol +++ b/contracts/test/integration/SubnetActorDiamond.t.sol @@ -24,6 +24,7 @@ import {SubnetIDHelper} from "../../src/lib/SubnetIDHelper.sol"; import {GatewayDiamond} from "../../src/GatewayDiamond.sol"; import {SubnetActorDiamond, FunctionNotFound} from "../../src/SubnetActorDiamond.sol"; import {SubnetActorManagerFacet} from "../../src/subnet/SubnetActorManagerFacet.sol"; +import {OwnershipFacet} from "../../src/OwnershipFacet.sol"; import {SubnetActorGetterFacet} from "../../src/subnet/SubnetActorGetterFacet.sol"; import {SubnetActorPauseFacet} from "../../src/subnet/SubnetActorPauseFacet.sol"; import {SubnetActorCheckpointingFacet} from "../../src/subnet/SubnetActorCheckpointingFacet.sol"; @@ -75,7 +76,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { } function testSubnetActorDiamondReal_LoupeFunction() public view { - require(saDiamond.diamondLouper().facets().length == 7, "unexpected length"); + require(saDiamond.diamondLouper().facets().length == 8, "unexpected length"); require( saDiamond.diamondLouper().supportsInterface(type(IERC165).interfaceId) == true, "IERC165 not supported" @@ -318,6 +319,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { SubnetActorPauseFacet saDupPauserFaucet = new SubnetActorPauseFacet(); SubnetActorRewardFacet saDupRewardFaucet = new SubnetActorRewardFacet(); SubnetActorCheckpointingFacet saDupCheckpointerFaucet = new SubnetActorCheckpointingFacet(); + OwnershipFacet saOwnershipFacet = new OwnershipFacet(); SupplySource memory native = SupplySourceHelper.native(); @@ -340,7 +342,8 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { address(saDupMangerFaucet), address(saDupPauserFaucet), address(saDupRewardFaucet), - address(saDupCheckpointerFaucet) + address(saDupCheckpointerFaucet), + address(saOwnershipFacet) ); } diff --git a/contracts/test/integration/SubnetRegistry.t.sol b/contracts/test/integration/SubnetRegistry.t.sol index 56076e56d..70ccb2b1f 100644 --- a/contracts/test/integration/SubnetRegistry.t.sol +++ b/contracts/test/integration/SubnetRegistry.t.sol @@ -88,7 +88,7 @@ contract SubnetRegistryTest is Test, TestRegistry, IntegrationTestBase { } function test_Registry_Deployment_IERC165() public view { - require(registryLouper.facets().length == 4, "unexpected length"); + require(registryLouper.facets().length == 5, "unexpected length"); require(registryLouper.facetAddresses().length == registryLouper.facets().length, "inconsistent diamond size"); require(registryLouper.supportsInterface(type(IERC165).interfaceId) == true, "IERC165 not supported"); require(registryLouper.supportsInterface(type(IDiamondCut).interfaceId) == true, "IDiamondCut not supported"); diff --git a/contracts/tools/check_aderyn.sh b/contracts/tools/check_aderyn.sh index 5c0b50ad5..debec6025 100755 --- a/contracts/tools/check_aderyn.sh +++ b/contracts/tools/check_aderyn.sh @@ -2,23 +2,43 @@ set -eu set -o pipefail -REPORT_FILE="./report.md" - -if [ ! -f $REPORT_FILE ]; then - echo "Report file not found." - exit 1; -fi - -# Check if one of `| Critical | 0 |`, `| High | 0 |`, or `| Medium | 0 |` line exist in the report. -zero_findings=`(grep -e "Critical\s*|\s*0" $REPORT_FILE && grep -e "High\s*|\s*0" $REPORT_FILE && grep -e "Medium\s*|\s*0" $REPORT_FILE) | wc -l` - -if [ $zero_findings -eq 3 ]; then - echo "No critical or high issues found" - exit 0 -else - echo "Critical, high, or medium issue found". - echo "Check $REPORT_FILE for more information". - echo "Printing here..." - cat $REPORT_FILE - exit 1; -fi +# Path to the report file +REPORT_FILE="./report.json" + +# List of severities that make us fail +SEVERITIES=(critical high medium) + +# List of vulnerability titles to ignore +IGNORE_TITLES=("Centralization Risk for trusted owners") + +containsElement() { + local e match="$1" + shift + for e; do [[ "$e" == "$match" ]] && return 0; done + return 1 +} + +# Read vulnerabilities from the report +readVulnerabilities() { + level="$1" + jq -c --argjson ignoreTitles "$(printf '%s\n' "${IGNORE_TITLES[@]}" | jq -R . | jq -s .)" ".${level}_issues.issues[] | select(.title as \$title | \$ignoreTitles | index(\$title) | not)" $REPORT_FILE +} + +# Main function to process the report +processReport() { + local hasVulnerabilities=0 + + for level in ${SEVERITIES[@]}; do + while IFS= read -r vulnerability; do + title=$(echo "$vulnerability" | jq -r ".title") + echo "Found $level vulnerability: $title" + hasVulnerabilities=1 + done < <(readVulnerabilities "$level") + done + + return $hasVulnerabilities +} + +# Process the report and exit with the code returned by processReport +processReport +exit $? \ No newline at end of file From d29b922e82b11d70fc2b09b45b229091cfe331f0 Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Sat, 16 Mar 2024 04:39:23 +0800 Subject: [PATCH 12/12] Update genesis validator lifecycle (#778) --- contracts/src/GatewayDiamond.sol | 8 +- contracts/src/lib/LibGenesis.sol | 57 ++++++++++++++ contracts/src/lib/LibStaking.sol | 33 +------- contracts/src/lib/LibSubnetActor.sol | 15 ++-- contracts/src/lib/LibSubnetActorStorage.sol | 6 +- contracts/src/structs/Subnet.sol | 12 +++ .../src/subnet/SubnetActorGetterFacet.sol | 40 +++++++++- .../src/subnet/SubnetActorManagerFacet.sol | 15 +++- contracts/test/helpers/TestUtils.sol | 2 +- contracts/test/integration/MultiSubnet.t.sol | 10 --- .../test/integration/SubnetActorDiamond.t.sol | 75 ++++++++++++++++++- 11 files changed, 215 insertions(+), 58 deletions(-) create mode 100644 contracts/src/lib/LibGenesis.sol diff --git a/contracts/src/GatewayDiamond.sol b/contracts/src/GatewayDiamond.sol index 82ac6b171..9b3d78a86 100644 --- a/contracts/src/GatewayDiamond.sol +++ b/contracts/src/GatewayDiamond.sol @@ -67,7 +67,13 @@ contract GatewayDiamond { // through the gateway constructor in the future. s.maxMsgsPerBottomUpBatch = MAX_MSGS_PER_BATCH; - LibValidatorTracking.init(s.validatorsTracker, params.activeValidatorsLimit, params.genesisValidators); + Validator[] memory validators = LibValidatorTracking.init( + s.validatorsTracker, + params.activeValidatorsLimit, + params.genesisValidators + ); + Membership memory initial = Membership({configurationNumber: 0, validators: validators}); + LibGateway.updateMembership(initial); } function _fallback() internal { diff --git a/contracts/src/lib/LibGenesis.sol b/contracts/src/lib/LibGenesis.sol new file mode 100644 index 000000000..ed28ccca0 --- /dev/null +++ b/contracts/src/lib/LibGenesis.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +import {GenesisValidator, SubnetGenesis, ValidatorInfo, ValidatorSet} from "../structs/Subnet.sol"; +import {EnumerableSet} from "openzeppelin-contracts/utils/structs/EnumerableSet.sol"; + +/// @notice Provides convinient util methods to handle genesis states of the subnet +library LibGenesis { + using EnumerableSet for EnumerableSet.AddressSet; + + /// @notice Dumps the validator's genesis information + function getValidatorInfo(SubnetGenesis storage self, address validator) internal view returns(GenesisValidator memory info){ + info = self.validatorInfo[validator]; + } + + function addValidator(SubnetGenesis storage self, address validator) internal { + self.validators.add(validator); + } + + function removeValidator(SubnetGenesis storage self, address validator) internal { + self.validators.remove(validator); + } + + /// @notice Handles the genesis state when the subnet is bootstrapped. From this point onwards, + /// no genesis state of the subnet can be changed. + /// @param validatorInfo The validator staking information from LibStaking + function bootstrap(SubnetGenesis storage self, ValidatorSet storage validatorInfo) internal { + finalizeValidatorInfo(self, validatorInfo); + } + + // ============ Interal functions ============== + + /// @notice Finalizes the genesis validator information as the subnet is bootstrapped. After + /// this point, the genesis validator info can no longer be changed. + /// @param validatorInfo The validator staking information from LibStaking + function finalizeValidatorInfo(SubnetGenesis storage self, ValidatorSet storage validatorInfo) internal { + address[] memory validators = self.validators.values(); + + for (uint256 i = 0; i < validators.length; ) { + address addr = validators[i]; + + ValidatorInfo memory info = validatorInfo.validators[addr]; + GenesisValidator memory genesis = GenesisValidator({ + collateral: info.totalCollateral, + federatedPower: info.federatedPower, + addr: addr, + metadata: info.metadata + }); + + self.validatorInfo[addr] = genesis; + + unchecked { + i++; + } + } + } +} \ No newline at end of file diff --git a/contracts/src/lib/LibStaking.sol b/contracts/src/lib/LibStaking.sol index cbf3f320f..0be13571c 100644 --- a/contracts/src/lib/LibStaking.sol +++ b/contracts/src/lib/LibStaking.sol @@ -9,6 +9,7 @@ import {LibStakingChangeLog} from "./LibStakingChangeLog.sol"; import {PermissionMode, StakingReleaseQueue, StakingChangeLog, StakingChange, StakingChangeRequest, StakingOperation, StakingRelease, ValidatorSet, AddressStakingReleases, ParentValidatorsTracker, GenesisValidator, Validator} from "../structs/Subnet.sol"; import {WithdrawExceedingCollateral, NotValidator, CannotConfirmFutureChanges, NoCollateralToWithdraw, AddressShouldBeValidator, InvalidConfigurationNumber} from "../errors/IPCErrors.sol"; import {Address} from "openzeppelin-contracts/utils/Address.sol"; +import {EnumerableSet} from "openzeppelin-contracts/utils/structs/EnumerableSet.sol"; library LibAddressStakingReleases { /// @notice Add new release to the storage. Caller makes sure the release.releasedAt is ordered @@ -173,7 +174,6 @@ library LibValidatorSet { collateral += getTotalConfirmedCollateral(validators); } - /// @notice Get the total power of the validators. /// The function reverts if at least one validator is not in the active validator set. function getTotalPowerOfValidators( @@ -377,6 +377,7 @@ library LibStaking { using LibMaxPQ for MaxPQ; using LibMinPQ for MinPQ; using Address for address payable; + using EnumerableSet for EnumerableSet.AddressSet; uint64 internal constant INITIAL_CONFIGURATION_NUMBER = 1; @@ -469,31 +470,6 @@ library LibStaking { s.validatorSet.recordDeposit(validator, amount); // confirm deposit that updates the confirmed collateral s.validatorSet.confirmDeposit(validator, amount); - - if (!s.bootstrapped) { - // add to initial validators avoiding duplicates if it - // is a genesis validator. - bool alreadyValidator; - uint256 length = s.genesisValidators.length; - for (uint256 i; i < length; ) { - if (s.genesisValidators[i].addr == validator) { - alreadyValidator = true; - break; - } - unchecked { - ++i; - } - } - if (!alreadyValidator) { - uint256 collateral = s.validatorSet.validators[validator].confirmedCollateral; - Validator memory val = Validator({ - addr: validator, - weight: collateral, - metadata: s.validatorSet.validators[validator].metadata - }); - s.genesisValidators.push(val); - } - } } /// @notice Confirm the withdraw directly without going through the confirmation process @@ -541,7 +517,6 @@ library LibStaking { } // =============== Other functions ================ - /// @notice Claim the released collateral function claimCollateral(address validator) internal { SubnetActorStorage storage s = LibSubnetActorStorage.appStorage(); @@ -611,7 +586,7 @@ library LibValidatorTracking { ParentValidatorsTracker storage self, uint16 activeValidatorsLimit, GenesisValidator[] memory validators - ) internal returns (Validator[] memory membership) { + ) internal returns (Validator[] memory) { self.validators.activeLimit = activeValidatorsLimit; // Start the next configuration number from 1, 0 is reserved for no change and the genesis membership self.changes.nextConfigurationNumber = LibStaking.INITIAL_CONFIGURATION_NUMBER; @@ -619,7 +594,7 @@ library LibValidatorTracking { // empty validator change logs self.changes.startConfigurationNumber = LibStaking.INITIAL_CONFIGURATION_NUMBER; - initValidators(self, validators); + return initValidators(self, validators); } function initValidators( diff --git a/contracts/src/lib/LibSubnetActor.sol b/contracts/src/lib/LibSubnetActor.sol index 4bbcccafc..dac41682b 100644 --- a/contracts/src/lib/LibSubnetActor.sol +++ b/contracts/src/lib/LibSubnetActor.sol @@ -5,16 +5,18 @@ import {VALIDATOR_SECP256K1_PUBLIC_KEY_LENGTH} from "../constants/Constants.sol" import {ERR_PERMISSIONED_AND_BOOTSTRAPPED} from "../errors/IPCErrors.sol"; import {NotEnoughGenesisValidators, DuplicatedGenesisValidator, NotOwnerOfPublicKey, MethodNotAllowed} from "../errors/IPCErrors.sol"; import {IGateway} from "../interfaces/IGateway.sol"; -import {Validator, ValidatorSet, PermissionMode} from "../structs/Subnet.sol"; +import {Validator, ValidatorSet, PermissionMode, SubnetGenesis} from "../structs/Subnet.sol"; import {SubnetActorModifiers} from "../lib/LibSubnetActorStorage.sol"; import {LibValidatorSet, LibStaking} from "../lib/LibStaking.sol"; +import {LibGenesis} from "../lib/LibGenesis.sol"; import {EnumerableSet} from "openzeppelin-contracts/utils/structs/EnumerableSet.sol"; import {LibSubnetActorStorage, SubnetActorStorage} from "./LibSubnetActorStorage.sol"; library LibSubnetActor { using EnumerableSet for EnumerableSet.AddressSet; + using LibGenesis for SubnetGenesis; - event SubnetBootstrapped(Validator[]); + event SubnetBootstrapped(address[]); /// @notice Ensures that the subnet is operating under Collateral-based permission mode. /// @dev Reverts if the subnet is not in Collateral mode. @@ -48,7 +50,8 @@ library LibSubnetActor { if (totalCollateral >= s.minActivationCollateral) { if (LibStaking.totalActiveValidators() >= s.minValidators) { s.bootstrapped = true; - emit SubnetBootstrapped(s.genesisValidators); + s.genesis.bootstrap(s.validatorSet); + emit SubnetBootstrapped(s.genesis.validators.values()); // register adding the genesis circulating supply (if it exists) IGateway(s.ipcGatewayAddr).register{value: totalCollateral + s.genesisCircSupply}(s.genesisCircSupply); @@ -98,8 +101,7 @@ library LibSubnetActor { LibStaking.setMetadataWithConfirm(validators[i], publicKeys[i]); LibStaking.setFederatedPowerWithConfirm(validators[i], powers[i]); - - s.genesisValidators.push(Validator({addr: validators[i], weight: powers[i], metadata: publicKeys[i]})); + s.genesis.addValidator(validators[i]); unchecked { ++i; @@ -107,7 +109,8 @@ library LibSubnetActor { } s.bootstrapped = true; - emit SubnetBootstrapped(s.genesisValidators); + s.genesis.bootstrap(s.validatorSet); + emit SubnetBootstrapped(s.genesis.validators.values()); // register adding the genesis circulating supply (if it exists) IGateway(s.ipcGatewayAddr).register{value: s.genesisCircSupply}(s.genesisCircSupply); diff --git a/contracts/src/lib/LibSubnetActorStorage.sol b/contracts/src/lib/LibSubnetActorStorage.sol index a0b3dc4b5..87bddb59d 100644 --- a/contracts/src/lib/LibSubnetActorStorage.sol +++ b/contracts/src/lib/LibSubnetActorStorage.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.23; import {ConsensusType} from "../enums/ConsensusType.sol"; import {NotGateway, SubnetAlreadyKilled} from "../errors/IPCErrors.sol"; import {BottomUpCheckpoint, BottomUpMsgBatchInfo} from "../structs/CrossNet.sol"; -import {SubnetID, ValidatorSet, StakingChangeLog, StakingReleaseQueue, SupplySource, Validator, PermissionMode} from "../structs/Subnet.sol"; +import {SubnetID, ValidatorSet, StakingChangeLog, StakingReleaseQueue, SupplySource, Validator, SubnetGenesis, PermissionMode} from "../structs/Subnet.sol"; import {EnumerableSet} from "openzeppelin-contracts/utils/structs/EnumerableSet.sol"; struct SubnetActorStorage { @@ -55,8 +55,8 @@ import {EnumerableSet} from "openzeppelin-contracts/utils/structs/EnumerableSet. EnumerableSet.AddressSet bootstrapOwners; /// @notice contains all committed bottom-up checkpoint at specific epoch mapping(uint256 => BottomUpCheckpoint) committedCheckpoints; - /// @notice initial set of validators joining in genesis - Validator[] genesisValidators; + /// @notice Tracks the subnet genesis state + SubnetGenesis genesis; /// @notice genesis balance to be allocated to the subnet in genesis. mapping(address => uint256) genesisBalance; /// @notice genesis balance addresses diff --git a/contracts/src/structs/Subnet.sol b/contracts/src/structs/Subnet.sol index 4de150ad2..102068034 100644 --- a/contracts/src/structs/Subnet.sol +++ b/contracts/src/structs/Subnet.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.23; import {FvmAddress} from "./FvmAddress.sol"; import {MaxPQ} from "../lib/priority/LibMaxPQ.sol"; import {MinPQ} from "../lib/priority/LibMinPQ.sol"; +import {EnumerableSet} from "openzeppelin-contracts/utils/structs/EnumerableSet.sol"; /// @notice A subnet identity type. struct SubnetID { @@ -153,6 +154,17 @@ struct GenesisValidator { bytes metadata; } +/// @notice Maintains the genesis information related to a particular subnet +struct SubnetGenesis { + /// The genesis validators + EnumerableSet.AddressSet validators; + /// Tracks the validator info. This is only populated when the subnet is bootstrapped + mapping(address => GenesisValidator) validatorInfo; + + /// TODO: migrating all the genesis related fields to this struct so that one can handle + /// them all in a library. +} + /// @notice Validator struct stored in the gateway. struct Validator { uint256 weight; diff --git a/contracts/src/subnet/SubnetActorGetterFacet.sol b/contracts/src/subnet/SubnetActorGetterFacet.sol index a71c1011c..0a933a453 100644 --- a/contracts/src/subnet/SubnetActorGetterFacet.sol +++ b/contracts/src/subnet/SubnetActorGetterFacet.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.23; import {ConsensusType} from "../enums/ConsensusType.sol"; import {BottomUpCheckpoint, IpcEnvelope} from "../structs/CrossNet.sol"; import {SubnetID, SupplySource} from "../structs/Subnet.sol"; -import {SubnetID, ValidatorInfo, Validator, PermissionMode} from "../structs/Subnet.sol"; +import {SubnetID, ValidatorInfo, Validator, GenesisValidator, PermissionMode} from "../structs/Subnet.sol"; import {SubnetActorStorage} from "../lib/LibSubnetActorStorage.sol"; import {SubnetIDHelper} from "../lib/SubnetIDHelper.sol"; import {Address} from "openzeppelin-contracts/utils/Address.sol"; @@ -55,8 +55,40 @@ contract SubnetActorGetterFacet { } /// @notice Returns the initial set of validators of the genesis block. - function genesisValidators() external view returns (Validator[] memory) { - return s.genesisValidators; + function genesisValidators() external view returns (GenesisValidator[] memory validators) { + uint256 total = s.genesis.validators.length(); + + validators = new GenesisValidator[](total); + + if (s.bootstrapped) { + // subnet boostrapped, that means validator information at genesis should be locked + // and cannot be changed anymore, fetch from s.genesis + + for (uint256 i = 0; i < total; ) { + address addr = s.genesis.validators.at(i); + validators[i] = s.genesis.validatorInfo[addr]; + + unchecked { + i++; + } + } + return validators; + } + + for (uint256 i = 0; i < total; ) { + address addr = s.genesis.validators.at(i); + ValidatorInfo memory info = getValidator(addr); + validators[i] = GenesisValidator({ + // for genesis validators, totalCollateral == confirmedCollateral + collateral: info.totalCollateral, + federatedPower: info.federatedPower, + addr: addr, + metadata: info.metadata + }); + unchecked { + i++; + } + } } // @notice Provides the circulating supply of the genesis block. @@ -114,7 +146,7 @@ contract SubnetActorGetterFacet { /// @notice Returns detailed information about a specific validator. /// @param validatorAddress The address of the validator to query information for. - function getValidator(address validatorAddress) external view returns (ValidatorInfo memory validator) { + function getValidator(address validatorAddress) public view returns (ValidatorInfo memory validator) { validator = s.validatorSet.validators[validatorAddress]; } diff --git a/contracts/src/subnet/SubnetActorManagerFacet.sol b/contracts/src/subnet/SubnetActorManagerFacet.sol index 9ce86f2ae..825f72d6b 100644 --- a/contracts/src/subnet/SubnetActorManagerFacet.sol +++ b/contracts/src/subnet/SubnetActorManagerFacet.sol @@ -5,11 +5,12 @@ import {VALIDATOR_SECP256K1_PUBLIC_KEY_LENGTH} from "../constants/Constants.sol" import {ERR_VALIDATOR_JOINED, ERR_VALIDATOR_NOT_JOINED} from "../errors/IPCErrors.sol"; import {InvalidFederationPayload, SubnetAlreadyBootstrapped, NotEnoughFunds, CollateralIsZero, CannotReleaseZero, NotOwnerOfPublicKey, EmptyAddress, NotEnoughBalance, NotEnoughCollateral, NotValidator, NotAllValidatorsHaveLeft, InvalidPublicKeyLength, MethodNotAllowed, SubnetNotBootstrapped} from "../errors/IPCErrors.sol"; import {IGateway} from "../interfaces/IGateway.sol"; -import {Validator, ValidatorSet} from "../structs/Subnet.sol"; +import {Validator, ValidatorSet, SubnetGenesis} from "../structs/Subnet.sol"; import {LibDiamond} from "../lib/LibDiamond.sol"; import {ReentrancyGuard} from "../lib/LibReentrancyGuard.sol"; import {SubnetActorModifiers} from "../lib/LibSubnetActorStorage.sol"; import {LibValidatorSet, LibStaking} from "../lib/LibStaking.sol"; +import {LibGenesis} from "../lib/LibGenesis.sol"; import {EnumerableSet} from "openzeppelin-contracts/utils/structs/EnumerableSet.sol"; import {Address} from "openzeppelin-contracts/utils/Address.sol"; import {LibSubnetActor} from "../lib/LibSubnetActor.sol"; @@ -18,6 +19,7 @@ import {Pausable} from "../lib/LibPausable.sol"; contract SubnetActorManagerFacet is SubnetActorModifiers, ReentrancyGuard, Pausable { using EnumerableSet for EnumerableSet.AddressSet; using LibValidatorSet for ValidatorSet; + using LibGenesis for SubnetGenesis; using Address for address payable; /// @notice method to add some initial balance into a subnet that hasn't yet bootstrapped. @@ -142,6 +144,7 @@ contract SubnetActorManagerFacet is SubnetActorModifiers, ReentrancyGuard, Pausa // confirm validators deposit immediately LibStaking.setMetadataWithConfirm(msg.sender, publicKey); LibStaking.depositWithConfirm(msg.sender, msg.value); + s.genesis.addValidator(msg.sender); LibSubnetActor.bootstrapSubnetIfNeeded(); } else { @@ -169,7 +172,6 @@ contract SubnetActorManagerFacet is SubnetActorModifiers, ReentrancyGuard, Pausa if (!s.bootstrapped) { LibStaking.depositWithConfirm(msg.sender, msg.value); - LibSubnetActor.bootstrapSubnetIfNeeded(); } else { LibStaking.deposit(msg.sender, msg.value); @@ -193,11 +195,17 @@ contract SubnetActorManagerFacet is SubnetActorModifiers, ReentrancyGuard, Pausa if (collateral == 0) { revert NotValidator(msg.sender); } - if (collateral <= amount) { + if (collateral < amount) { revert NotEnoughCollateral(); } if (!s.bootstrapped) { LibStaking.withdrawWithConfirm(msg.sender, amount); + + uint256 total = LibStaking.totalValidatorCollateral(msg.sender); + if (total == 0) { + s.genesis.removeValidator(msg.sender); + } + return; } @@ -237,6 +245,7 @@ contract SubnetActorManagerFacet is SubnetActorModifiers, ReentrancyGuard, Pausa // interaction must be performed after checks and changes LibStaking.withdrawWithConfirm(msg.sender, amount); + s.genesis.removeValidator(msg.sender); return; } LibStaking.withdraw(msg.sender, amount); diff --git a/contracts/test/helpers/TestUtils.sol b/contracts/test/helpers/TestUtils.sol index c22f367d8..6e0dece4a 100644 --- a/contracts/test/helpers/TestUtils.sol +++ b/contracts/test/helpers/TestUtils.sol @@ -194,7 +194,7 @@ contract MockIpcContractRevert is IpcHandler { bool public reverted = true; /* solhint-disable-next-line unused-vars */ - function handleIpcMessage(IpcEnvelope calldata) external payable returns (bytes memory ret) { + function handleIpcMessage(IpcEnvelope calldata) external payable returns (bytes memory) { // success execution of this methid will set reverted to false, by default it's true reverted = false; diff --git a/contracts/test/integration/MultiSubnet.t.sol b/contracts/test/integration/MultiSubnet.t.sol index df710daa0..5ae80e3db 100644 --- a/contracts/test/integration/MultiSubnet.t.sol +++ b/contracts/test/integration/MultiSubnet.t.sol @@ -491,8 +491,6 @@ contract MultiSubnetTest is Test, IntegrationTestBase { IpcEnvelope[] memory crossMsgs = new IpcEnvelope[](1); crossMsgs[0] = resultMsg; - GatewayManagerFacet manager = GatewayManagerFacet(nativeSubnet.gatewayAddr); - BottomUpCheckpoint memory checkpoint = callCreateBottomUpCheckpointFromChildSubnet( nativeSubnet.id, nativeSubnet.gateway, @@ -534,8 +532,6 @@ contract MultiSubnetTest is Test, IntegrationTestBase { IpcEnvelope[] memory crossMsgs = new IpcEnvelope[](1); crossMsgs[0] = resultMsg; - GatewayManagerFacet manager = GatewayManagerFacet(nativeSubnet.gatewayAddr); - BottomUpCheckpoint memory checkpoint = callCreateBottomUpCheckpointFromChildSubnet( nativeSubnet.id, nativeSubnet.gateway, @@ -581,8 +577,6 @@ contract MultiSubnetTest is Test, IntegrationTestBase { IpcEnvelope[] memory crossMsgs = new IpcEnvelope[](1); crossMsgs[0] = resultMsg; - GatewayManagerFacet manager = GatewayManagerFacet(nativeSubnet.gatewayAddr); - BottomUpCheckpoint memory checkpoint = callCreateBottomUpCheckpointFromChildSubnet( nativeSubnet.id, nativeSubnet.gateway, @@ -652,7 +646,6 @@ contract MultiSubnetTest is Test, IntegrationTestBase { function testMultiSubnet_Erc20_FundResultOkFromChildToParent() public { address caller = address(new MockIpcContract()); - address recipient = address(new MockIpcContractPayable()); uint256 amount = 3; token.transfer(caller, amount); @@ -698,7 +691,6 @@ contract MultiSubnetTest is Test, IntegrationTestBase { function testMultiSubnet_Erc20_FundResultSystemErrFromChildToParent() public { address caller = address(new MockIpcContract()); - address recipient = address(new MockIpcContractPayable()); uint256 amount = 3; token.transfer(caller, amount); @@ -744,7 +736,6 @@ contract MultiSubnetTest is Test, IntegrationTestBase { function testMultiSubnet_Erc20_FundResultActorErrFromChildToParent() public { address caller = address(new MockIpcContract()); - address recipient = address(new MockIpcContractPayable()); uint256 amount = 3; token.transfer(caller, amount); @@ -1220,7 +1211,6 @@ contract MultiSubnetTest is Test, IntegrationTestBase { ) internal returns (BottomUpCheckpoint memory checkpoint) { uint256 e = getNextEpoch(block.number, DEFAULT_CHECKPOINT_PERIOD); - GatewayGetterFacet getter = gw.getter(); CheckpointingFacet checkpointer = gw.checkpointer(); (, address[] memory addrs, uint256[] memory weights) = TestUtils.getFourValidators(vm); diff --git a/contracts/test/integration/SubnetActorDiamond.t.sol b/contracts/test/integration/SubnetActorDiamond.t.sol index 17b3f0d2a..0283dc84a 100644 --- a/contracts/test/integration/SubnetActorDiamond.t.sol +++ b/contracts/test/integration/SubnetActorDiamond.t.sol @@ -12,7 +12,7 @@ import {METHOD_SEND} from "../../src/constants/Constants.sol"; import {ConsensusType} from "../../src/enums/ConsensusType.sol"; import {BottomUpMsgBatch, IpcEnvelope, BottomUpCheckpoint} from "../../src/structs/CrossNet.sol"; import {FvmAddress} from "../../src/structs/FvmAddress.sol"; -import {SubnetID, PermissionMode, IPCAddress, Subnet, SupplySource, ValidatorInfo} from "../../src/structs/Subnet.sol"; +import {SubnetID, PermissionMode, IPCAddress, Subnet, SupplySource, ValidatorInfo, GenesisValidator} from "../../src/structs/Subnet.sol"; import {IERC165} from "../../src/interfaces/IERC165.sol"; import {IGateway} from "../../src/interfaces/IGateway.sol"; import {IDiamond} from "../../src/interfaces/IDiamond.sol"; @@ -91,6 +91,79 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { ); } + /// @notice Testing the genesis validator's collateral + function testSubnetActorDiamond_GenesisValidators() public { + (address validator1, , bytes memory publicKey1) = TestUtils.newValidator(100); + uint256 validator1Stake = DEFAULT_MIN_VALIDATOR_STAKE / 2; + + // initial validator joins + vm.deal(validator1, 10 ether); // gas concern free + vm.startPrank(validator1); + + saDiamond.manager().join{value: validator1Stake}(publicKey1); + + GenesisValidator[] memory validators = saDiamond.getter().genesisValidators(); + require(validators[0].addr == validator1); + require(validators[0].collateral == validator1Stake); + require(keccak256(validators[0].metadata) == keccak256(publicKey1)); + require(validators[0].federatedPower == 0); + + // subnet will be bootstrapped + saDiamond.manager().stake{value: validator1Stake + 1}(); + validators = saDiamond.getter().genesisValidators(); + require(validators[0].addr == validator1); + require(validators[0].collateral == validator1Stake * 2 + 1); + + // subnet already bootstrapped, will no longer trigger chanes + saDiamond.manager().stake{value: validator1Stake + 1}(); + validators = saDiamond.getter().genesisValidators(); + require(validators[0].addr == validator1); + require(validators[0].collateral == validator1Stake * 2 + 1); + + saDiamond.manager().leave(); + validators = saDiamond.getter().genesisValidators(); + require(validators[0].addr == validator1); + require(validators[0].collateral == validator1Stake * 2 + 1); + } + + /// @notice Testing the genesis validator's collateral + function testSubnetActorDiamond_RemoveGenesisValidators() public { + (address validator1, , bytes memory publicKey1) = TestUtils.newValidator(100); + uint256 validator1Stake = DEFAULT_MIN_VALIDATOR_STAKE / 2; + + // initial validator joins + vm.deal(validator1, 10 ether); // gas concern free + vm.startPrank(validator1); + + saDiamond.manager().join{value: validator1Stake}(publicKey1); + + saDiamond.manager().leave(); + require(saDiamond.getter().genesisValidators().length == 0); + } + + /// @notice Testing the genesis validator's collateral + function testSubnetActorDiamond_UnstakeGenesisValidators() public { + (address validator1, , bytes memory publicKey1) = TestUtils.newValidator(100); + uint256 validator1Stake = DEFAULT_MIN_VALIDATOR_STAKE / 2; + + // initial validator joins + vm.deal(validator1, 10 ether); // gas concern free + vm.startPrank(validator1); + + saDiamond.manager().join{value: validator1Stake}(publicKey1); + GenesisValidator[] memory validators = saDiamond.getter().genesisValidators(); + require(validators[0].addr == validator1); + + saDiamond.manager().unstake(validator1Stake / 2); + validators = saDiamond.getter().genesisValidators(); + require(validators[0].addr == validator1); + require(validators[0].collateral == validator1Stake / 2, "collateral not match"); + + // unstake everything else + saDiamond.manager().unstake(validator1Stake / 2); + require(saDiamond.getter().genesisValidators().length == 0, "should not be validator"); + } + /// @notice Testing the basic join, stake, leave lifecycle of validators function testSubnetActorDiamond_BasicLifeCycle() public { (address validator1, uint256 privKey1, bytes memory publicKey1) = TestUtils.newValidator(100);